# 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.fauna fauna-jvm 1.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 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 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 codec = codecProvider.get(Object.class, null); return new RetryHandler>(getRetryStrategy(), logger).execute(makeAsyncRequest( getHttpClient(), getRequestBuilder().buildRequest(fql, null, codecProvider, lastTransactionTs.get()), codec)); } /** * Sends an asynchronous Fauna Query Language (FQL) query to Fauna. *

* CompletableFuture<QuerySuccess<Document>> future = client.asyncQuery(fql, Document.class, null); * ... do some other stuff ... * Document doc = future.get().getData(); * * @param fql The FQL query to be executed. * @param resultClass The expected class of the query result. * @param options A (nullable) set of options to pass to the query. * @return QuerySuccess The successful query result. * @throws FaunaException If the query does not succeed, an exception will be thrown. * * @param The return type of the query. */ public CompletableFuture> asyncQuery(final Query fql, final Class resultClass, final QueryOptions options) { if (Objects.isNull(fql)) { throw new IllegalArgumentException( "The provided FQL query is null."); } Codec codec = codecProvider.get(resultClass, null); return new RetryHandler>(getRetryStrategy(), logger).execute(makeAsyncRequest( getHttpClient(), getRequestBuilder().buildRequest(fql, options, codecProvider, lastTransactionTs.get()), codec)); } /** * Sends an asynchronous Fauna Query Language (FQL) query to Fauna. *

* CompletableFuture<QuerySuccess<List<int>>> future = client.asyncQuery(fql, Parameterized.listOf(int.class), null); * ... do some other stuff ... * List<int>> data = future.get().getData(); * * @param fql The FQL query to be executed. * @param parameterizedType The expected class of the query result. * @param options A (nullable) set of options to pass to the query. * @return QuerySuccess The successful query result. * @throws FaunaException If the query does not succeed, an exception will be thrown. * * @param The inner type for the parameterized wrapper. */ public CompletableFuture> asyncQuery(final Query fql, final ParameterizedOf parameterizedType, final QueryOptions options) { if (Objects.isNull(fql)) { throw new IllegalArgumentException( "The provided FQL query is null."); } @SuppressWarnings("unchecked") Codec codec = codecProvider.get((Class) parameterizedType.getRawType(), parameterizedType.getActualTypeArguments()); return new RetryHandler>(getRetryStrategy(), logger).execute(makeAsyncRequest( getHttpClient(), getRequestBuilder().buildRequest(fql, options, codecProvider, lastTransactionTs.get()), codec)); } /** * Sends an asynchronous Fauna Query Language (FQL) query to Fauna. *

* CompletableFuture<QuerySuccess<Document>> future = client.asyncQuery(fql, Document.class); * ... do some other stuff ... * Document doc = future.get().getData(); * * @param fql The FQL query to be executed. * @param resultClass The expected class of the query result. * @return QuerySuccess A CompletableFuture that completes with the successful query result. * @throws FaunaException If the query does not succeed, an exception will be thrown. * * @param The return type of the query. */ public CompletableFuture> asyncQuery(final Query fql, final Class resultClass) { return asyncQuery(fql, resultClass, null); } /** * Sends an asynchronous Fauna Query Language (FQL) query to Fauna. *

* CompletableFuture<QuerySuccess<List<int>>> future = client.asyncQuery(fql, Parameterized.listOf(int.class)); * ... do some other stuff ... * List<int>> data = future.get().getData(); * * @param fql The FQL query to be executed. * @param parameterizedType The expected class of the query result. * @return QuerySuccess The successful query result. * @throws FaunaException If the query does not succeed, an exception will be thrown. * @param The inner type for the parameterized wrapper. */ public CompletableFuture> asyncQuery(final Query fql, final ParameterizedOf parameterizedType) { if (Objects.isNull(fql)) { throw new IllegalArgumentException( "The provided FQL query is null."); } @SuppressWarnings("unchecked") Codec codec = codecProvider.get((Class) parameterizedType.getRawType(), parameterizedType.getActualTypeArguments()); return new RetryHandler>(getRetryStrategy(), logger).execute(makeAsyncRequest( getHttpClient(), getRequestBuilder().buildRequest(fql, null, codecProvider, lastTransactionTs.get()), codec)); } //endregion //region Synchronous API /** * Sends a Fauna Query Language (FQL) query to Fauna and returns the result. *

* var result = client.query(fql); * var data = result.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 QuerySuccess query(final Query fql) throws FaunaException { return completeAsync(asyncQuery(fql, Object.class, null), "Unable to execute query."); } /** * Sends a Fauna Query Language (FQL) query to Fauna and returns the result. *

* QuerySuccess<Document> result = client.query(fql, Document.class); * Document doc = result.getData(); * * @param fql The FQL query to be executed. * @param resultClass The expected class of the query result. * @return QuerySuccess The successful query result. * @throws FaunaException If the query does not succeed, an exception will be thrown. * @param The return type of the query. */ public QuerySuccess query(final Query fql, final Class resultClass) throws FaunaException { return completeAsync(asyncQuery(fql, resultClass, null), QUERY_EXECUTION); } /** * Sends a Fauna Query Language (FQL) query to Fauna and returns the result. *

* QuerySuccess<List<int>>> result = client.query(fql, Parameterized.listOf(int.class)); * List<int>> data = result.getData(); * * @param fql The FQL query to be executed. * @param parameterizedType The expected class of the query result. * @return QuerySuccess The successful query result. * @throws FaunaException If the query does not succeed, an exception will be thrown. * @param The inner type for the parameterized wrapper. */ public QuerySuccess query(final Query fql, final ParameterizedOf parameterizedType) throws FaunaException { return completeAsync(asyncQuery(fql, parameterizedType), QUERY_EXECUTION); } /** * Sends a Fauna Query Language (FQL) query to Fauna and returns the result. *

* QuerySuccess result = client.query(fql, Document.class, null); * Document doc = result.getData(); * * @param fql The FQL query to be executed. * @param resultClass The expected class of the query result. * @param options A (nullable) set of options to pass to the query. * @return QuerySuccess The successful query result. * @throws FaunaException If the query does not succeed, an exception will be thrown. * @param The return type of the query. */ public QuerySuccess query(final Query fql, final Class resultClass, final QueryOptions options) throws FaunaException { return completeAsync(asyncQuery(fql, resultClass, options), QUERY_EXECUTION); } /** * Sends a Fauna Query Language (FQL) query to Fauna and returns the result. *

* QuerySuccess<List<int>>> result = client.query(fql, Parameterized.listOf(int.class), null); * List<int>> data = result.getData(); * * @param fql The FQL query to be executed. * @param parameterizedType The expected class of the query result. * @param options A (nullable) set of options to pass to the query. * @return QuerySuccess The successful query result. * @throws FaunaException If the query does not succeed, an exception will be thrown. * @param The inner type for the parameterized wrapper. */ public QuerySuccess query(final Query fql, final ParameterizedOf parameterizedType, final QueryOptions options) throws FaunaException { return completeAsync(asyncQuery(fql, parameterizedType, options), QUERY_EXECUTION); } //endregion //region Query Page API /** * Sends a query to Fauna that retrieves the Page for the given page token. * * @param after The page token (result of a previous paginated request). * @param elementClass The expected class of the query result. * @param options A (nullable) set of options to pass to the query. * @param The type of the elements of the page. * @return A CompletableFuture that returns a QuerySuccess with data of type Page. * @throws FaunaException If the query does not succeed, an exception will be thrown. */ public CompletableFuture>> asyncQueryPage( final AfterToken after, final Class elementClass, final QueryOptions options) { return this.asyncQuery(PageIterator.buildPageQuery(after), pageOf(elementClass), options); } /** * Sends a query to Fauna that retrieves the Page for the given page token. * * @param after The page token (result of a previous paginated request). * @param elementClass The expected class of the query result. * @param options A (nullable) set of options to pass to the query. * @param The type of the elements of the page. * @return A QuerySuccess with data of type Page. * @throws FaunaException If the query does not succeed, an exception will be thrown. */ public QuerySuccess> queryPage( final AfterToken after, final Class elementClass, final QueryOptions options) { return completeAsync(asyncQueryPage(after, elementClass, options), QUERY_PAGE); } //endregion //region Paginated API /** * Send a Fauna Query Language (FQL) query to Fauna and return a paginated result. * * @param fql The FQL query to be executed. * @param elementClass The expected class of the query result. * @param options A (nullable) set of options to pass to the query. * @param The type of the elements of the page. * @return QuerySuccess The successful query result. * @throws FaunaException If the query does not succeed, an exception will be thrown. */ public PageIterator paginate(final Query fql, final Class elementClass, final QueryOptions options) { return new PageIterator<>(this, fql, elementClass, options); } /** * Send a Fauna Query Language (FQL) query to Fauna and return a paginated result. * * @param fql The FQL query to be executed. * @return The successful query result. * @throws FaunaException If the query does not succeed, an exception will be thrown. */ public PageIterator paginate(final Query fql) { return paginate(fql, Object.class, null); } /** * Send a Fauna Query Language (FQL) query to Fauna and return a paginated result. * * @param fql The FQL query to be executed. * @param options A (nullable) set of options to pass to the query. * @return The successful query result. * @throws FaunaException If the query does not succeed, an exception will be thrown. */ public PageIterator paginate(final Query fql, final QueryOptions options) { return paginate(fql, Object.class, options); } /** * Send a Fauna Query Language (FQL) query to Fauna and return a paginated result. * * @param fql The FQL query to be executed. * @param elementClass The expected class of the query result. * @return QuerySuccess The successful query result. * @throws FaunaException If the query does not succeed, an exception will be thrown. * @param The type for each element in a page. */ public PageIterator paginate(final Query fql, final Class elementClass) { return paginate(fql, elementClass, null); } //endregion //region Streaming API /** * Send a request to the Fauna stream endpoint, and return a CompletableFuture that completes with the FaunaStream * publisher. * * @param eventSource The Event Source (e.g. token from `.eventSource()`). * @param streamOptions The Stream Options (including start timestamp, retry strategy). * @param elementClass The target type into which event data will be deserialized. * @return CompletableFuture A CompletableFuture of FaunaStream. * @throws FaunaException If the query does not succeed, an exception will be thrown. * @param The type for data in an event. */ public CompletableFuture> asyncStream( final EventSource eventSource, final StreamOptions streamOptions, final Class elementClass) { HttpRequest streamReq = getStreamRequestBuilder().buildStreamRequest(eventSource, streamOptions); return getHttpClient().sendAsync(streamReq, HttpResponse.BodyHandlers.ofPublisher()) .thenCompose(response -> { CompletableFuture> publisher = new CompletableFuture<>(); FaunaStream fstream = new FaunaStream<>(elementClass, this.statsCollector); response.body().subscribe(fstream); publisher.complete(fstream); return publisher; }); } /** * Send a request to the Fauna stream endpoint to start a stream, and return a FaunaStream publisher. * * @param eventSource The request object including a stream token, and optionally a cursor, or timestamp. * @param streamOptions The stream options. * @param elementClass The expected class <E> of the stream events. * @return FaunaStream A publisher, implementing Flow.Publisher<StreamEvent<E>> from the Java Flow * API. * @throws FaunaException If the query does not succeed, an exception will be thrown. * @param The type for data in an event. */ public FaunaStream stream(final EventSource eventSource, final StreamOptions streamOptions, final Class elementClass) { return completeAsync( asyncStream(eventSource, streamOptions, elementClass), STREAM_SUBSCRIPTION); } /** * Start a Fauna stream based on an FQL query, and return a CompletableFuture of the resulting FaunaStream * publisher. This method sends two requests, one to the query endpoint to get the stream token, and then another * to the stream endpoint. This method is equivalent to calling the query, then the stream methods on FaunaClient. *

* This method does not take QueryOptions, or StreamOptions as parameters. If you need specify either * query, or stream options; you can use the asyncQuery/asyncStream methods. * * @param fql The FQL query to be executed. It must return an event source, e.g. ends in `.eventSource()`. * @param elementClass The expected class <E> of the stream events. * @return FaunaStream A publisher, implementing Flow.Publisher<StreamEvent<E>> from the Java Flow * API. * @throws FaunaException If the query does not succeed, an exception will be thrown. * @param The type for data in an event. */ public CompletableFuture> asyncStream(final Query fql, final Class elementClass) { return this.asyncQuery(fql, EventSource.class) .thenApply(queryResponse -> this.stream(queryResponse.getData(), StreamOptions.builder().build(), elementClass)); } /** * Start a Fauna stream based on an FQL query. This method sends two requests, one to the query endpoint to get * the stream token, and then another request to the stream endpoint which return the FaunaStream publisher. * *

* Query = fql("Product.all().eventSource()"); * QuerySuccess<EventSource> querySuccess = client.query(fql, EventSource.class); * EventSource source = querySuccess.getData(); * FaunaStream<Product> faunaStream = client.stream(source, StreamOptions.DEFAULT, Product.class) * * @param fql The FQL query to be executed. It must return a stream, e.g. ends in `.toStream()`. * @param elementClass The expected class <E> of the stream events. * @return FaunaStream A publisher, implementing Flow.Publisher<StreamEvent<E>> from the Java Flow * API. * @throws FaunaException If the query does not succeed, an exception will be thrown. * @param The type for data in an event. */ public FaunaStream stream(final Query fql, final Class elementClass) { return completeAsync(asyncStream(fql, elementClass), STREAM_SUBSCRIPTION); } //endregion //region Event feeds /** * Send a request to the Fauna feed endpoint, and return a CompletableFuture that completes with the feed page. * * @param eventSource An EventSource object (e.g. token from `.eventSource()`) * @param feedOptions The FeedOptions object (default options will be used if null). * @param elementClass The expected class <E> of the feed events. * @param The type for data in an event. * @return CompletableFuture A CompletableFuture that completes with a FeedPage<E>. */ public CompletableFuture> poll(final EventSource eventSource, final FeedOptions feedOptions, final Class elementClass) { return new RetryHandler>(getRetryStrategy(), logger).execute(makeAsyncFeedRequest( getHttpClient(), getFeedRequestBuilder().buildFeedRequest(eventSource, feedOptions != null ? feedOptions : FeedOptions.DEFAULT), codecProvider.get(elementClass))); } /** * Return a CompletableFuture that completes with a FeedIterator based on an FQL query. This method sends two * requests, one to the query endpoint to get the event source token, and then another request to the feed endpoint * to get the first page of results. * * @param fql The FQL query to be executed. It must return a token, e.g. ends in `.changesOn()`. * @param feedOptions The FeedOptions object (must not be null). * @param elementClass The expected class <E> of the feed events. * @param The type for data in an event. * @return FeedIterator A CompletableFuture that completes with a feed iterator that returns pages of Feed events. */ public CompletableFuture> asyncFeed(final Query fql, final FeedOptions feedOptions, final Class elementClass) { return this.asyncQuery(fql, EventSource.class).thenApply( success -> this.feed( success.getData(), feedOptions, elementClass)); } /** * Return a FeedIterator based on an FQL query. This method sends two requests, one to the query endpoint to get * the stream/feed token, and then another request to the feed endpoint to get the first page of results. * * @param fql The FQL query to be executed. It must return a token, e.g. ends in `.changesOn()`. * @param feedOptions The Feed Op * @param elementClass The expected class <E> of the feed events. * @param The type for data in an event. * @return FeedIterator An iterator that returns pages of Feed events. */ public FeedIterator feed(final Query fql, final FeedOptions feedOptions, final Class elementClass) { return completeAsync(asyncFeed(fql, feedOptions, elementClass), FEED_SUBSCRIPTION); } /** * Send a request to the Feed endpoint and return a FeedIterator. * * @param eventSource The Fauna Event Source. * @param feedOptions The feed options. * @param elementClass The expected class <E> of the feed events. * @param The type for data in an event. * @return FeedIterator An iterator that returns pages of Feed events. */ public FeedIterator feed(final EventSource eventSource, final FeedOptions feedOptions, final Class elementClass) { return new FeedIterator<>(this, eventSource, feedOptions, elementClass); } //endregion } ``` ## File: src/main/java/com/fauna/client/FaunaConfig.java ```java package com.fauna.client; import java.time.Duration; import java.util.Optional; import java.util.logging.ConsoleHandler; import java.util.logging.Handler; import java.util.logging.Level; import static com.fauna.constants.Defaults.CLIENT_TIMEOUT_BUFFER; import static com.fauna.constants.Defaults.LOCAL_FAUNA_SECRET; import static com.fauna.constants.Defaults.MAX_CONTENTION_RETRIES; /** * FaunaConfig is a configuration class used to set up and configure a connection to Fauna. * It encapsulates various settings such as the endpoint URL, secret key, and more. */ public final class FaunaConfig { public static class FaunaEndpoint { public static final String DEFAULT = "https://db.fauna.com"; public static final String LOCAL = "http://localhost:8443"; } private final String endpoint; private final String secret; private final int maxContentionRetries; private final Duration clientTimeoutBuffer; private final Handler logHandler; private final StatsCollector statsCollector; public static final FaunaConfig DEFAULT = FaunaConfig.builder().build(); public static final FaunaConfig LOCAL = FaunaConfig.builder().endpoint( FaunaEndpoint.LOCAL).secret(LOCAL_FAUNA_SECRET).build(); /** * Private constructor for FaunaConfig. * * @param builder The builder used to create the FaunaConfig instance. */ private FaunaConfig(final Builder builder) { this.endpoint = builder.endpoint != null ? builder.endpoint : FaunaEndpoint.DEFAULT; this.secret = builder.secret != null ? builder.secret : ""; this.maxContentionRetries = builder.maxContentionRetries; this.clientTimeoutBuffer = builder.clientTimeoutBuffer; this.logHandler = builder.logHandler; this.statsCollector = builder.statsCollector; } /** * Gets the Fauna endpoint URL. * * @return A String representing the endpoint URL. * The default is https://db.fauna.com */ public String getEndpoint() { return endpoint; } /** * Gets the secret key used for authentication. * * @return A String representing the secret key. */ public String getSecret() { return secret; } /** * Gets the number of contention retries that the Fauna server will attempt. * * @return An integer value. */ public int getMaxContentionRetries() { return maxContentionRetries; } /** * Gets the buffer that will be added to the HTTP client timeout, in addition to any query timeout. * * @return The timeout buffer Duration. */ public Duration getClientTimeoutBuffer() { return clientTimeoutBuffer; } /** * Gets the log handler that the client will use. * * @return A log handler instance. */ public Handler getLogHandler() { return logHandler; } /** * Gets the stats collector for the client. * * @return A StatsCollector instance. */ public StatsCollector getStatsCollector() { return statsCollector; } /** * Creates a new builder for FaunaConfig. * * @return A new instance of Builder. */ public static Builder builder() { return new Builder(); } /** * Builder class for FaunaConfig. Follows the Builder Design Pattern. */ public static class Builder { private String endpoint = FaunaEnvironment.faunaEndpoint().orElse(FaunaEndpoint.DEFAULT); private String secret = FaunaEnvironment.faunaSecret().orElse(""); private int maxContentionRetries = MAX_CONTENTION_RETRIES; private Duration clientTimeoutBuffer = CLIENT_TIMEOUT_BUFFER; private Handler logHandler = defaultLogHandler(); private StatsCollector statsCollector = new StatsCollectorImpl(); static Level getLogLevel(final String debug) { if (debug == null || debug.isBlank()) { return Level.WARNING; } else { try { int debugInt = Integer.parseInt(debug); return debugInt > 0 ? Level.FINE : Level.WARNING; } catch (NumberFormatException e) { return Level.FINE; } } } private static Handler defaultLogHandler() { Handler logHandler = new ConsoleHandler(); logHandler.setLevel( getLogLevel(FaunaEnvironment.faunaDebug().orElse(null))); return logHandler; } /** * Sets the endpoint URL. * * @param endpoint A String representing the endpoint URL. * @return The current Builder instance. */ public Builder endpoint(final String endpoint) { this.endpoint = endpoint; return this; } /** * Sets the secret key. * * @param secret A String representing the secret key. * @return The current Builder instance. */ public Builder secret(final String secret) { this.secret = secret; return this; } /** * Set the Fauna max-contention-retries setting. * * @param maxContentionRetries A positive integer value. * @return The current Builder instance. */ public Builder maxContentionRetries(final int maxContentionRetries) { this.maxContentionRetries = maxContentionRetries; return this; } /** * Set the client timeout buffer. * * @param duration The timeout buffer duration. * @return The current Builder instance. */ public Builder clientTimeoutBuffer(final Duration duration) { this.clientTimeoutBuffer = duration; return this; } /** * Override the default log handler with the given log handler. * * @param handler A log handler instance. * @return The current Builder instance. */ public Builder logHandler(final Handler handler) { this.logHandler = handler; return this; } /** * Set a StatsCollector. * * @param statsCollector A stats collector instance. * @return The current Builder instance. */ public Builder statsCollector(final StatsCollector statsCollector) { this.statsCollector = statsCollector; return this; } /** * Builds and returns a new FaunaConfig instance. * * @return A new instance of FaunaConfig. */ public FaunaConfig build() { return new FaunaConfig(this); } } /** * This class handles reading Fauna environment variables for the client. */ public static class FaunaEnvironment { private static final String FAUNA_SECRET = "FAUNA_SECRET"; private static final String FAUNA_ENDPOINT = "FAUNA_ENDPOINT"; private static final String FAUNA_DEBUG = "FAUNA_DEBUG"; private static Optional environmentVariable(final String name) { Optional var = Optional.ofNullable(System.getenv(name)); return var.isPresent() && var.get().isBlank() ? Optional.empty() : var; } /** * @return The (non-empty, non-blank) value of the FAUNA_SECRET environment variable, or Optional.empty(). */ public static Optional faunaSecret() { return environmentVariable(FAUNA_SECRET); } /** * @return The (non-empty, non-blank) value of the FAUNA_ENDPOINT environment variable, or Optional.empty(). */ public static Optional faunaEndpoint() { return environmentVariable(FAUNA_ENDPOINT); } /** * @return The (non-empty, non-blank) value of the FAUNA_DEBUG environment variable, or Optional.empty(). */ public static Optional faunaDebug() { return environmentVariable(FAUNA_DEBUG); } } } ``` ## File: src/main/java/com/fauna/client/FaunaRole.java ```java package com.fauna.client; import java.text.MessageFormat; import java.util.Set; /** * Built-in roles defined at: * docs.fauna.com. */ public final class FaunaRole { private static final String ADMIN_ROLE_NAME = "admin"; private static final String SERVER_ROLE_NAME = "server"; private static final String SERVER_READ_ONLY_ROLE_NAME = "server-readonly"; private static final Set BUILT_IN_ROLE_NAMES = Set.of( ADMIN_ROLE_NAME, SERVER_ROLE_NAME, SERVER_READ_ONLY_ROLE_NAME); public static final FaunaRole ADMIN = new FaunaRole(ADMIN_ROLE_NAME); public static final FaunaRole SERVER = new FaunaRole(SERVER_ROLE_NAME); public static final FaunaRole SERVER_READ_ONLY = new FaunaRole(SERVER_READ_ONLY_ROLE_NAME); private static final String ROLE_PREFIX = "@role/"; private static final Character UNDERSCORE = '_'; private final String role; /** * Constructor is not public. You should either use one of the built-in roles (ADMIN, SERVER, SERVER_READ_ONLY), * or create a user-defined role via Role.named(name). * * @param role The role name, either @role/name or one of the built-in role names. */ FaunaRole(final String role) { this.role = role; } /** * @return A {@link String} representation of the {@code FaunaRole}. */ public String toString() { return this.role; } /** * Validates a role name. * * @param name The name of the role to validate. */ public static void validateRoleName(final String name) { if (name == null || name.isEmpty()) { throw new IllegalArgumentException( "Role name cannot be null or empty."); } if (BUILT_IN_ROLE_NAMES.contains(name)) { String msg = MessageFormat.format( "Role name {0} is reserved, but you can use it as a built-in role", name); throw new IllegalArgumentException(msg); } if (!Character.isAlphabetic(name.charAt(0))) { throw new IllegalArgumentException( "First character must be a letter."); } for (Character c : name.toCharArray()) { if (!Character.isAlphabetic(c) && !Character.isDigit(c) && !c.equals(UNDERSCORE)) { throw new IllegalArgumentException( "Role names can only contain letters, numbers, and underscores."); } } } /** * Creates a {@code FaunaRole} with the desired name prepended with {@code @role/}. * * @param name The name of the role to use. * @return A {@code FaunaRole} instance. */ public static FaunaRole named(final String name) { validateRoleName(name); return new FaunaRole(ROLE_PREFIX + name); } } ``` ## File: src/main/java/com/fauna/client/FaunaScope.java ```java package com.fauna.client; /** * Represents a FaunaScope, a structure that encapsulates a Fauna database and a role within that database. * The FaunaScope is used to generate a token that is used for authorization. */ public final class FaunaScope { private static final String DELIMITER = ":"; private final String database; private final FaunaRole role; /** * Creates a FaunaScope with the specified database and role. * * @param database the name of the database * @param role the FaunaRole associated with this scope */ public FaunaScope(final String database, final FaunaRole role) { this.database = database; this.role = role; } /** * Generates a token for this scope using the provided secret. * * @param secret the secret used to generate the token * @return a token string formed by concatenating secret, database, and role */ public String getToken(final String secret) { return String.join(DELIMITER, secret, database, role.toString()); } /** * A builder class for creating instances of FaunaScope. */ public static class Builder { private final String database; private FaunaRole role = null; /** * Constructs a Builder for FaunaScope. * * @param database the name of the database */ public Builder(final String database) { this.database = database; } /** * Sets the role for the FaunaScope. * * @param role the FaunaRole to associate with the scope * @return the Builder instance for method chaining */ public Builder withRole(final FaunaRole role) { this.role = role; return this; } /** * Builds a FaunaScope instance using the current builder settings. * * @return a newly created FaunaScope */ public FaunaScope build() { return new FaunaScope(this.database, this.role != null ? this.role : FaunaRole.SERVER); } } /** * Creates a new Builder instance for a FaunaScope. * * @param database the name of the database * @return a new Builder instance */ public static Builder builder(final String database) { return new Builder(database); } } ``` ## File: src/main/java/com/fauna/client/Logging.java ```java package com.fauna.client; import java.net.http.HttpHeaders; import java.util.stream.Collectors; /** * A utility class for logging HTTP headers. */ public final class Logging { private Logging() { } /** * Converts the given HttpHeaders to a string representation. * * @param headers The HttpHeaders to convert. * @return A string representation of the headers. */ public static String headersAsString(final HttpHeaders headers) { String hdrs = "NONE"; if (headers != null) { hdrs = headers.map().entrySet().stream().map( entry -> entry.getKey() + ": " + String.join( ",", entry.getValue())) .collect(Collectors.joining(";")); } return hdrs; } } ``` ## File: src/main/java/com/fauna/client/NoRetryStrategy.java ```java package com.fauna.client; /** * Specifies that no retries will be made. */ public final class NoRetryStrategy implements RetryStrategy { @Override public boolean canRetry(final int retryAttempt) { return false; } @Override public int getDelayMillis(final int retryAttempt) { return 0; } @Override public int getMaxRetryAttempts() { return 0; } } ``` ## File: src/main/java/com/fauna/client/package-info.java ```java /** * Classes related initializing, configuring, and using a client to interact * with Fauna. */ package com.fauna.client; ``` ## File: src/main/java/com/fauna/client/PageIterator.java ```java package com.fauna.client; import com.fauna.codec.PageOf; import com.fauna.exception.FaunaException; import com.fauna.query.AfterToken; import com.fauna.query.QueryOptions; import com.fauna.query.builder.Query; import com.fauna.response.QuerySuccess; import com.fauna.types.Page; import java.util.Iterator; import java.util.Map; import java.util.NoSuchElementException; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionException; import static com.fauna.query.builder.Query.fql; /** * PageIterator iterates over paged responses from Fauna, the default page size is 16. * * @param The type of elements in the page. */ public class PageIterator implements Iterator> { static final String TOKEN_NAME = "token"; static final String PAGINATE_QUERY = "Set.paginate(${" + TOKEN_NAME + "})"; private final FaunaClient client; private final QueryOptions options; private final PageOf pageClass; private CompletableFuture>> queryFuture; /** * Construct a new PageIterator. * * @param client A client that makes requests to Fauna. * @param fql The FQL query. * @param resultClass The class of the elements returned from Fauna (i.e., the rows). * @param options (optionally) pass in QueryOptions. */ public PageIterator(final FaunaClient client, final Query fql, final Class resultClass, final QueryOptions options) { this.client = client; this.pageClass = new PageOf<>(resultClass); this.options = options; // Initial query; this.queryFuture = client.asyncQuery(fql, this.pageClass, options); } /** * Construct a new PageIterator starting from a given page. * * @param client A client that makes requests to Fauna. * @param firstPage The first Page of elements. * @param resultClass The class of the elements returned from Fauna (i.e., the rows). * @param options (optionally) pass in QueryOptions. */ public PageIterator(final FaunaClient client, final Page firstPage, final Class resultClass, final QueryOptions options) { this.client = client; this.pageClass = new PageOf<>(resultClass); this.options = options; firstPage.getAfter() .ifPresentOrElse(this::doPaginatedQuery, this::endPagination); } /** * Check if there is a next page available. * * @return True if there is a next page, false otherwise. */ @Override public boolean hasNext() { return this.queryFuture != null; } /** * Build the page query with a specific AfterToken. * * @param afterToken The token indicating where the next page should start. * @return A Query to fetch the next page. */ public static Query buildPageQuery(final AfterToken afterToken) { return fql(PAGINATE_QUERY, Map.of(TOKEN_NAME, afterToken.getToken())); } /** * Performs a paginated query with the provided AfterToken. * * @param afterToken The token indicating where the next page should start. */ private void doPaginatedQuery(final AfterToken afterToken) { this.queryFuture = client.asyncQuery(PageIterator.buildPageQuery(afterToken), pageClass, options); } /** * Ends the pagination process when no further pages are available. */ private void endPagination() { this.queryFuture = null; } /** * Returns a CompletableFuture that will complete with the next page (or throw a FaunaException). * * @return A CompletableFuture representing the next page of elements. */ public CompletableFuture> nextAsync() { if (this.queryFuture != null) { return this.queryFuture.thenApply(qs -> { Page page = qs.getData(); page.getAfter().ifPresentOrElse(this::doPaginatedQuery, this::endPagination); return page; }); } else { throw new NoSuchElementException(); } } /** * Get the next Page. * * @return The next Page of elements E. * @throws FaunaException If there is an error getting the next page. */ @Override public Page next() { try { return nextAsync().join(); } catch (CompletionException ce) { if (ce.getCause() != null && ce.getCause() instanceof FaunaException) { throw (FaunaException) ce.getCause(); } else { throw ce; } } } /** * Return an iterator that iterates directly over the items that make up the page contents. * * @return An iterator of E. */ public Iterator flatten() { return new Iterator<>() { private final PageIterator pageIterator = PageIterator.this; private Iterator thisPage = pageIterator.hasNext() ? pageIterator.next().getData().iterator() : null; /** * Check if there are more items to iterate over. * * @return True if there are more items, false otherwise. */ @Override public boolean hasNext() { return thisPage != null && (thisPage.hasNext() || pageIterator.hasNext()); } /** * Get the next item in the iteration. * * @return The next item in the iteration. * @throws NoSuchElementException if no more items are available. */ @Override public E next() { if (thisPage == null) { throw new NoSuchElementException(); } try { return thisPage.next(); } catch (NoSuchElementException e) { if (pageIterator.hasNext()) { this.thisPage = pageIterator.next().getData().iterator(); return thisPage.next(); } else { throw e; } } } }; } } ``` ## File: src/main/java/com/fauna/client/QueryStatsSummary.java ```java package com.fauna.client; /** * A class for representing aggregate query stats. This should be used when collecting query stats * across multiple requests. *

* For a single request, use @link com.fauna.response.QueryStats instead. */ public final class QueryStatsSummary { private final long readOps; private final long computeOps; private final long writeOps; private final long queryTimeMs; private final int contentionRetries; private final long storageBytesRead; private final long storageBytesWrite; private final long processingTimeMs; private final int queryCount; private final int rateLimitedReadQueryCount; private final int rateLimitedComputeQueryCount; private final int rateLimitedWriteQueryCount; /** * @param readOps Aggregate Transactional * Read Operations (TROs) consumed * by the requests. * @param computeOps Aggregate Transactional * Compute Operations (TCOs) * consumed by the requests. * @param writeOps Aggregate Transactional * Write Operations (TWOs) * consumed by the requests. * @param queryTimeMs Aggregate query run time for the * requests in milliseconds. * @param contentionRetries Aggregate number of * retries * for contended transactions. * @param storageBytesRead Aggregate amount of data read from * storage, in bytes. * @param storageBytesWrite Aggregate amount of data written to * storage, in bytes. * @param processingTimeMs Aggregate event processing time in * milliseconds. Only applies to event * feed and event stream requests. * @param queryCount Number of requests included in the * summary. * @param rateLimitedReadQueryCount Aggregate count of requests that * exceeded * plan * throughput limits for * Transactional * Read Operations (TROs). * @param rateLimitedComputeQueryCount Aggregate count of requests that * exceeded * plan * throughput limits for * Transactional * Compute Operations (TCOs). * @param rateLimitedWriteQueryCount Aggregate count of requests that * exceeded * plan * throughput limits for * Transactional * Write Operations (TWOs). */ public QueryStatsSummary( final long readOps, final long computeOps, final long writeOps, final long queryTimeMs, final int contentionRetries, final long storageBytesRead, final long storageBytesWrite, final long processingTimeMs, final int queryCount, final int rateLimitedReadQueryCount, final int rateLimitedComputeQueryCount, final int rateLimitedWriteQueryCount ) { this.readOps = readOps; this.computeOps = computeOps; this.writeOps = writeOps; this.queryTimeMs = queryTimeMs; this.contentionRetries = contentionRetries; this.storageBytesRead = storageBytesRead; this.storageBytesWrite = storageBytesWrite; this.processingTimeMs = processingTimeMs; this.queryCount = queryCount; this.rateLimitedReadQueryCount = rateLimitedReadQueryCount; this.rateLimitedComputeQueryCount = rateLimitedComputeQueryCount; this.rateLimitedWriteQueryCount = rateLimitedWriteQueryCount; } /** * Gets the aggregate Transactional Read Operations (TROs) recorded. * * @return A long representing the aggregate read ops */ public long getReadOps() { return readOps; } /** * Gets the aggregate Transactional Compute Operations (TCOs) recorded. * * @return A long representing the aggregate compute ops */ public long getComputeOps() { return computeOps; } /** * Gets the aggregate Transactional Write Operations (TWOs)) recorded. * * @return A long representing the aggregate write ops */ public long getWriteOps() { return writeOps; } /** * Gets the aggregate query time in milliseconds. * * @return A long representing the aggregate query time in milliseconds. */ public long getQueryTimeMs() { return queryTimeMs; } /** * Gets the count of retries due to contention. * * @return An int representing the count of retries due to contention. */ public int getContentionRetries() { return contentionRetries; } /** * Gets the aggregate storage bytes read. * * @return A long representing the aggregate number of storage bytes read. */ public long getStorageBytesRead() { return storageBytesRead; } /** * Gets the aggregate storage bytes written. * * @return A long representing the aggregate number of storage bytes * written. */ public long getStorageBytesWrite() { return storageBytesWrite; } /** * Gets the aggregate event processing time in milliseconds. * Applies to event feeds and event stream requests only. * * @return A long representing the aggregate processing time in * milliseconds. */ public long getProcessingTimeMs() { return processingTimeMs; } /** * Gets the count of queries summarized on this instance. * * @return An int representing the count of queries summarized. */ public int getQueryCount() { return queryCount; } /** * Gets the count of rate limited queries due to read limits. * * @return An int representing the count of rate limited queries. */ public int getRateLimitedReadQueryCount() { return rateLimitedReadQueryCount; } /** * Gets the count of rate limited queries due to compute limits. * * @return An int representing the count of rate limited queries. */ public int getRateLimitedComputeQueryCount() { return rateLimitedComputeQueryCount; } /** * Gets the count of rate limited queries due to write limits. * * @return An int representing the count of rate limited queries. */ public int getRateLimitedWriteQueryCount() { return rateLimitedWriteQueryCount; } } ``` ## File: src/main/java/com/fauna/client/RequestBuilder.java ```java package com.fauna.client; import com.fauna.codec.Codec; import com.fauna.codec.CodecProvider; import com.fauna.codec.UTF8FaunaGenerator; import com.fauna.env.DriverEnvironment; import com.fauna.event.EventSource; import com.fauna.event.FeedOptions; import com.fauna.event.FeedRequest; import com.fauna.event.StreamOptions; import com.fauna.event.StreamRequest; import com.fauna.exception.ClientException; import com.fauna.query.QueryOptions; import com.fauna.query.builder.Query; import java.io.IOException; import java.net.URI; import java.net.http.HttpRequest; import java.text.MessageFormat; import java.time.Duration; import java.util.logging.Logger; import static com.fauna.client.Logging.headersAsString; /** * The RequestBuilder class is responsible for building HTTP requests for communicating with Fauna. */ public final class RequestBuilder { private static final String BEARER = "Bearer"; private static final String QUERY_PATH = "/query/1"; private static final String STREAM_PATH = "/stream/1"; private static final String FEED_PATH = "/feed/1"; private final HttpRequest.Builder baseRequestBuilder; private final Duration clientTimeoutBuffer; private final Logger logger; /** * Field names for HTTP requests. */ public static class FieldNames { static final String QUERY = "query"; public static final String TOKEN = "token"; public static final String CURSOR = "cursor"; public static final String START_TS = "start_ts"; public static final String PAGE_SIZE = "page_size"; } /** * HTTP headers used for Fauna requests. */ static class Headers { static final String LAST_TXN_TS = "X-Last-Txn-Ts"; static final String LINEARIZED = "X-Linearized"; static final String MAX_CONTENTION_RETRIES = "X-Max-Contention-Retries"; static final String QUERY_TIMEOUT_MS = "X-Query-Timeout-Ms"; static final String TYPE_CHECK = "X-Typecheck"; static final String QUERY_TAGS = "X-Query-Tags"; static final String TRACE_PARENT = "Traceparent"; static final String ACCEPT_ENCODING = "Accept-Encoding"; static final String AUTHORIZATION = "Authorization"; static final String CONTENT_TYPE = "Content-Type"; static final String DRIVER = "X-Driver"; static final String DRIVER_ENV = "X-Driver-Env"; static final String FORMAT = "X-Format"; } /** * Constructor for creating a RequestBuilder with the specified Fauna configuration. * * @param uri The URI for the Fauna endpoint. * @param token The secret key used for authorization. * @param maxContentionRetries The maximum retries for contention errors. * @param clientTimeoutBuffer The buffer for the client timeout. * @param logger The logger to log HTTP request details. */ public RequestBuilder(final URI uri, final String token, final int maxContentionRetries, final Duration clientTimeoutBuffer, final Logger logger) { DriverEnvironment env = new DriverEnvironment(DriverEnvironment.JvmDriver.JAVA); this.baseRequestBuilder = HttpRequest.newBuilder().uri(uri).headers( RequestBuilder.Headers.FORMAT, "tagged", RequestBuilder.Headers.ACCEPT_ENCODING, "gzip", RequestBuilder.Headers.CONTENT_TYPE, "application/json;charset=utf-8", RequestBuilder.Headers.DRIVER, "Java", RequestBuilder.Headers.DRIVER_ENV, env.toString(), RequestBuilder.Headers.MAX_CONTENTION_RETRIES, String.valueOf(maxContentionRetries), Headers.AUTHORIZATION, buildAuthHeader(token) ); this.clientTimeoutBuffer = clientTimeoutBuffer; this.logger = logger; } /** * Constructor for creating a RequestBuilder with an existing HttpRequest.Builder. * * @param builder The HttpRequest.Builder to use. * @param clientTimeoutBuffer The buffer for the client timeout. * @param logger The logger to log HTTP request details. */ public RequestBuilder(final HttpRequest.Builder builder, final Duration clientTimeoutBuffer, final Logger logger) { this.baseRequestBuilder = builder; this.clientTimeoutBuffer = clientTimeoutBuffer; this.logger = logger; } /** * Creates a new RequestBuilder for Fauna queries. * * @param config The FaunaConfig containing endpoint and secret. * @param logger The logger for logging HTTP request details. * @return A new instance of RequestBuilder. */ public static RequestBuilder queryRequestBuilder(final FaunaConfig config, final Logger logger) { return new RequestBuilder(URI.create(config.getEndpoint() + QUERY_PATH), config.getSecret(), config.getMaxContentionRetries(), config.getClientTimeoutBuffer(), logger); } /** * Creates a new RequestBuilder for Fauna streams. * * @param config The FaunaConfig containing endpoint and secret. * @param logger The logger for logging HTTP request details. * @return A new instance of RequestBuilder. */ public static RequestBuilder streamRequestBuilder(final FaunaConfig config, final Logger logger) { return new RequestBuilder( URI.create(config.getEndpoint() + STREAM_PATH), config.getSecret(), config.getMaxContentionRetries(), config.getClientTimeoutBuffer(), logger); } /** * Creates a new RequestBuilder for Fauna feed requests. * * @param config The FaunaConfig containing endpoint and secret. * @param logger The logger for logging HTTP request details. * @return A new instance of RequestBuilder. */ public static RequestBuilder feedRequestBuilder(final FaunaConfig config, final Logger logger) { return new RequestBuilder(URI.create(config.getEndpoint() + FEED_PATH), config.getSecret(), config.getMaxContentionRetries(), config.getClientTimeoutBuffer(), logger); } /** * Creates a scoped request builder with the given token. * * @param token The token to be used for the request's authorization header. * @return A new instance of RequestBuilder with the scoped token. */ public RequestBuilder scopedRequestBuilder(final String token) { HttpRequest.Builder newBuilder = this.baseRequestBuilder.copy(); newBuilder.setHeader(Headers.AUTHORIZATION, buildAuthHeader(token)); return new RequestBuilder(newBuilder, clientTimeoutBuffer, logger); } private void logRequest(final String body, final HttpRequest req) { String timeout = req.timeout().map( val -> MessageFormat.format(" (timeout: {0})", val)).orElse(""); logger.fine(MessageFormat.format( "Fauna HTTP {0} Request to {1}{2}, headers: {3}", req.method(), req.uri(), timeout, headersAsString(req.headers()))); logger.finest("Request body: " + body); } /** * Builds and returns an HTTP request for a given Fauna query string (FQL). * * @param fql The Fauna query string. * @param options The query options. * @param provider The codec provider to encode the query. * @param lastTxnTs The last transaction timestamp (optional). * @return An HttpRequest object configured for the Fauna query. */ public HttpRequest buildRequest(final Query fql, final QueryOptions options, final CodecProvider provider, final Long lastTxnTs) { HttpRequest.Builder builder = getBuilder(options, lastTxnTs); try (UTF8FaunaGenerator gen = UTF8FaunaGenerator.create()) { gen.writeStartObject(); gen.writeFieldName(FieldNames.QUERY); Codec codec = provider.get(Query.class); codec.encode(gen, fql); gen.writeEndObject(); String body = gen.serialize(); HttpRequest req = builder.POST(HttpRequest.BodyPublishers.ofString(body)) .build(); logRequest(body, req); return req; } } /** * Builds and returns an HTTP request for a Fauna stream. * * @param eventSource The event source for the stream. * @param streamOptions The stream options. * @return An HttpRequest object configured for the Fauna stream. */ public HttpRequest buildStreamRequest(final EventSource eventSource, final StreamOptions streamOptions) { HttpRequest.Builder builder = baseRequestBuilder.copy(); streamOptions.getTimeout().ifPresent(builder::timeout); try { String body = new StreamRequest(eventSource, streamOptions).serialize(); HttpRequest req = builder.POST(HttpRequest.BodyPublishers.ofString(body)).build(); logRequest(body, req); return req; } catch (IOException e) { throw new ClientException("Unable to build Fauna Stream request.", e); } } /** * Builds and returns an HTTP request for a Fauna feed. * * @param eventSource The event source for the feed. * @param options The feed options. * @return An HttpRequest object configured for the Fauna feed. */ public HttpRequest buildFeedRequest(final EventSource eventSource, final FeedOptions options) { FeedRequest request = new FeedRequest(eventSource, options); HttpRequest.Builder builder = baseRequestBuilder.copy(); options.getTimeout().ifPresent(val -> { builder.timeout(val.plus(clientTimeoutBuffer)); builder.header(Headers.QUERY_TIMEOUT_MS, String.valueOf(val.toMillis())); }); try { String body = request.serialize(); HttpRequest req = builder.POST(HttpRequest.BodyPublishers.ofString(request.serialize())).build(); logRequest(body, req); return req; } catch (IOException e) { throw new ClientException("Unable to build Fauna Feed request.", e); } } /** * Builds an authorization header for the given token. * * @param token The token to be used in the authorization header. * @return The authorization header value. */ private static String buildAuthHeader(final String token) { return String.join(" ", RequestBuilder.BEARER, token); } /** * Gets the base request builder or a copy with options applied. * * @param options The QueryOptions (must not be null). * @param lastTxnTs The last transaction timestamp (optional). * @return The HttpRequest.Builder configured with options. */ private HttpRequest.Builder getBuilder(final QueryOptions options, final Long lastTxnTs) { if (options == null && (lastTxnTs == null || lastTxnTs <= 0)) { return baseRequestBuilder; } HttpRequest.Builder builder = baseRequestBuilder.copy(); if (lastTxnTs != null) { builder.setHeader(Headers.LAST_TXN_TS, String.valueOf(lastTxnTs)); } if (options != null) { options.getTimeoutMillis().ifPresent(val -> { builder.timeout(Duration.ofMillis(val).plus(clientTimeoutBuffer)); builder.header(Headers.QUERY_TIMEOUT_MS, String.valueOf(val)); }); options.getLinearized().ifPresent( val -> builder.header(Headers.LINEARIZED, String.valueOf(val))); options.getTypeCheck().ifPresent( val -> builder.header(Headers.TYPE_CHECK, String.valueOf(val))); options.getTraceParent().ifPresent( val -> builder.header(Headers.TRACE_PARENT, val)); options.getQueryTags().ifPresent( val -> builder.headers(Headers.QUERY_TAGS, val.encode())); } return builder; } } ``` ## File: src/main/java/com/fauna/client/RetryHandler.java ```java package com.fauna.client; import com.fauna.exception.FaunaException; import com.fauna.exception.RetryableException; import java.text.MessageFormat; import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; import java.util.function.Function; import java.util.function.Supplier; import java.util.logging.Logger; /** * A retry handler controls the retries for a particular request. * * @param The return type for a successful response. */ public final class RetryHandler { private final RetryStrategy strategy; private final Logger logger; /** * Constructs a new retry handler instance. * * @param strategy The retry strategy to use. * @param logger The logger used to log retry details. */ public RetryHandler(final RetryStrategy strategy, final Logger logger) { this.strategy = strategy; this.logger = logger; } /** * Delays the request execution by a specified delay in milliseconds. * * @param action The action to be executed. * @param delayMillis The delay in milliseconds before executing the action. * @return A CompletableFuture representing the result of the action. */ public CompletableFuture delayRequest( final Supplier> action, final int delayMillis) { return CompletableFuture.supplyAsync( action, CompletableFuture.delayedExecutor(delayMillis, TimeUnit.MILLISECONDS)).join(); } /** * Checks if an exception is retryable. * * @param exc The exception to check. * @return True if the exception or its cause is retryable. */ public static boolean isRetryable(final Throwable exc) { return exc instanceof RetryableException || exc.getCause() instanceof RetryableException; } /** * Rethrows a throwable as a FaunaException. * * @param throwable The throwable to be rethrown. * @return A failed CompletableFuture containing the throwable. */ public CompletableFuture rethrow(final Throwable throwable) { if (throwable instanceof FaunaException) { throw (FaunaException) throwable; } else if (throwable.getCause() instanceof FaunaException) { throw (FaunaException) throwable.getCause(); } return CompletableFuture.failedFuture(throwable); } /** * Retries the action based on the retry strategy. * * @param throwable The throwable that caused the failure. * @param retryAttempt The current retry attempt number. * @param supplier The action to retry. * @return A CompletableFuture representing the result of the retried action. */ private CompletableFuture retry(final Throwable throwable, final int retryAttempt, final Supplier> supplier) { try { boolean retryable = isRetryable(throwable); if (retryable && this.strategy.canRetry(retryAttempt)) { int delay = this.strategy.getDelayMillis(retryAttempt); logger.fine(MessageFormat.format( "Retry attempt {0} for exception {1}", retryAttempt, throwable.getClass())); return delayRequest(supplier, delay); } else { logger.fine(MessageFormat.format( "Re-throwing {0}retryable exception: {1}", retryable ? "" : "non-", throwable.getClass())); return rethrow(throwable); } } catch (FaunaException exc) { throw exc; } catch (Exception exc) { throw new FaunaException("Unexpected exception.", exc); } } /** * Executes an action with retry logic based on the retry strategy. * * @param action The action to execute. * @return A CompletableFuture representing the result of the action. */ public CompletableFuture execute(final Supplier> action) { CompletableFuture f = action.get(); for (int i = 1; i <= this.strategy.getMaxRetryAttempts(); i++) { final int finalI = i; f = f.thenApply(CompletableFuture::completedFuture) .exceptionally(t -> retry(t, finalI, action)) .thenCompose(Function.identity()); } return f; } } ``` ## File: src/main/java/com/fauna/client/RetryStrategy.java ```java package com.fauna.client; /** * This client comes with an ExponentialRetryStrategy, and it is recommended that users stick with that. If you choose * to implement your own RetryStrategy, then it should implement this interface, and be thread-safe * (or not store state). */ public interface RetryStrategy { /** * Returns true if the given retry attempt will be allowed by this strategy. * * @param retryAttempt The retry attempt number, starting at 1 (i.e. the second overall attempt, or first retry is attempt 1). * @return True if this attempt can be retried, otherwise false. */ boolean canRetry(int retryAttempt); /** * Return the number of milliseconds to delay the next retry attempt. * * @param retryAttempt The retry attempt number, starting at 1 (i.e. the second overall attempt/first retry is #1). * @return The number of milliseconds to delay the next retry attempt. */ int getDelayMillis(int retryAttempt); /** * Return the maximum number of retry attempts for this strategy. * * @return The number of retry attempts that this strategy will attempt. */ int getMaxRetryAttempts(); } ``` ## File: src/main/java/com/fauna/client/ScopedFaunaClient.java ```java package com.fauna.client; import java.net.http.HttpClient; /** * ScopedFaunaClient is a subclass of FaunaClient that applies a scope to the client, * limiting the actions and requests to the specified scope. */ public final class ScopedFaunaClient extends FaunaClient { private final FaunaClient client; private final RequestBuilder requestBuilder; private final RequestBuilder streamRequestBuilder; private final RequestBuilder feedRequestBuilder; /** * Constructs a new ScopedFaunaClient using the provided FaunaClient and FaunaScope. * * @param client The FaunaClient instance to base the scoped client on. * @param scope The FaunaScope defining the scope for this client. */ public ScopedFaunaClient(final FaunaClient client, final FaunaScope scope) { super(client.getFaunaSecret(), client.getLogger(), client.getStatsCollector().createNew()); this.client = client; this.requestBuilder = client.getRequestBuilder() .scopedRequestBuilder(scope.getToken(client.getFaunaSecret())); this.streamRequestBuilder = client.getStreamRequestBuilder() .scopedRequestBuilder(scope.getToken(client.getFaunaSecret())); this.feedRequestBuilder = client.getFeedRequestBuilder() .scopedRequestBuilder(scope.getToken(client.getFaunaSecret())); } /** * Gets the retry strategy for the scoped client. * * @return The retry strategy used by the client. */ @Override public RetryStrategy getRetryStrategy() { return client.getRetryStrategy(); } /** * Gets the HttpClient used by the scoped client. * * @return The HttpClient used for making HTTP requests. */ @Override public HttpClient getHttpClient() { return client.getHttpClient(); } /** * Gets the RequestBuilder for the scoped client. * * @return The RequestBuilder used for constructing HTTP requests. */ @Override public RequestBuilder getRequestBuilder() { return requestBuilder; } /** * Gets the RequestBuilder for streaming requests. * * @return The RequestBuilder used for constructing streaming HTTP requests. */ @Override public RequestBuilder getStreamRequestBuilder() { return streamRequestBuilder; } /** * Gets the RequestBuilder for feed requests. * * @return The RequestBuilder used for constructing feed HTTP requests. */ @Override public RequestBuilder getFeedRequestBuilder() { return feedRequestBuilder; } } ``` ## File: src/main/java/com/fauna/client/StatsCollector.java ```java package com.fauna.client; import com.fauna.response.QueryStats; public interface StatsCollector { /** * Add the QueryStats to the current counts. * * @param stats QueryStats object */ void add(QueryStats stats); /** * Return the collected Stats. * * @return Stats object */ QueryStatsSummary read(); /** * Return the collected Stats and reset counts. * * @return Stats object */ QueryStatsSummary readAndReset(); /** * Creates a new instance of a {@code StatsCollector}. * * @return A {@code StatsCollector} instance. */ StatsCollector createNew(); } ``` ## File: src/main/java/com/fauna/client/StatsCollectorImpl.java ```java package com.fauna.client; import com.fauna.response.QueryStats; import java.util.List; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; public final class StatsCollectorImpl implements StatsCollector { private static final String RATE_LIMIT_READ_OPS = "read"; private static final String RATE_LIMIT_COMPUTE_OPS = "compute"; private static final String RATE_LIMIT_WRITE_OPS = "write"; private final AtomicLong readOps = new AtomicLong(); private final AtomicLong computeOps = new AtomicLong(); private final AtomicLong writeOps = new AtomicLong(); private final AtomicLong queryTimeMs = new AtomicLong(); private final AtomicInteger contentionRetries = new AtomicInteger(); private final AtomicLong storageBytesRead = new AtomicLong(); private final AtomicLong storageBytesWrite = new AtomicLong(); private final AtomicLong processingTimeMs = new AtomicLong(); private final AtomicInteger queryCount = new AtomicInteger(); private final AtomicInteger rateLimitedReadQueryCount = new AtomicInteger(); private final AtomicInteger rateLimitedComputeQueryCount = new AtomicInteger(); private final AtomicInteger rateLimitedWriteQueryCount = new AtomicInteger(); @Override public void add(final QueryStats stats) { readOps.addAndGet(stats.getReadOps()); computeOps.addAndGet(stats.getComputeOps()); writeOps.addAndGet(stats.getWriteOps()); queryTimeMs.addAndGet(stats.getQueryTimeMs()); contentionRetries.addAndGet(stats.getContentionRetries()); storageBytesRead.addAndGet(stats.getStorageBytesRead()); storageBytesWrite.addAndGet(stats.getStorageBytesWrite()); processingTimeMs.addAndGet(stats.getProcessingTimeMs()); List rateLimitsHit = stats.getRateLimitsHit(); rateLimitsHit.forEach(limitHit -> { switch (limitHit) { case RATE_LIMIT_READ_OPS: rateLimitedReadQueryCount.incrementAndGet(); break; case RATE_LIMIT_COMPUTE_OPS: rateLimitedComputeQueryCount.incrementAndGet(); break; case RATE_LIMIT_WRITE_OPS: rateLimitedWriteQueryCount.incrementAndGet(); break; default: break; } }); queryCount.incrementAndGet(); } @Override public QueryStatsSummary read() { return new QueryStatsSummary( readOps.get(), computeOps.get(), writeOps.get(), queryTimeMs.get(), contentionRetries.get(), storageBytesRead.get(), storageBytesWrite.get(), processingTimeMs.get(), queryCount.get(), rateLimitedReadQueryCount.get(), rateLimitedComputeQueryCount.get(), rateLimitedWriteQueryCount.get() ); } @Override public QueryStatsSummary readAndReset() { return new QueryStatsSummary( readOps.getAndSet(0), computeOps.getAndSet(0), writeOps.getAndSet(0), queryTimeMs.getAndSet(0), contentionRetries.getAndSet(0), storageBytesRead.getAndSet(0), storageBytesWrite.getAndSet(0), processingTimeMs.getAndSet(0), queryCount.getAndSet(0), rateLimitedReadQueryCount.getAndSet(0), rateLimitedComputeQueryCount.getAndSet(0), rateLimitedWriteQueryCount.getAndSet(0) ); } @Override public StatsCollector createNew() { return new StatsCollectorImpl(); } } ``` ## File: src/main/java/com/fauna/codec/codecs/BaseCodec.java ```java package com.fauna.codec.codecs; import com.fauna.codec.Codec; import com.fauna.codec.FaunaTokenType; import com.fauna.codec.FaunaType; import java.lang.reflect.Type; import java.text.MessageFormat; import java.util.Arrays; import java.util.HashSet; import java.util.Set; /** * Abstract base class for implementing codecs to handle encoding and decoding operations for specific types. * * @param the type this codec can encode or decode. */ public abstract class BaseCodec implements Codec { /** Set of known tag identifiers for Fauna's tagged data format. */ public static final Set TAGS = new HashSet<>(Arrays.asList( "@int", "@long", "@double", "@date", "@time", "@mod", "@ref", "@doc", "@set", "@object", "@bytes" )); /** * Returns a formatted message indicating an unexpected token encountered during decoding. * * @param token the unexpected token type encountered. * @return a formatted message string. */ protected String unexpectedTokenExceptionMessage(final FaunaTokenType token) { return MessageFormat.format( "Unexpected token `{0}` decoding with `{1}<{2}>`", token, this.getClass().getSimpleName(), this.getCodecClass().getSimpleName()); } /** * Returns a formatted message indicating an unsupported Fauna type encountered during decoding. * * @param type the Fauna type encountered. * @param supportedTypes an array of supported Fauna types for this codec. * @return a formatted message string. */ protected String unsupportedTypeDecodingMessage(final FaunaType type, final FaunaType[] supportedTypes) { var supportedString = Arrays.toString(supportedTypes); return MessageFormat.format( "Unable to decode `{0}` with `{1}<{2}>`. Supported types for codec are {3}.", type, this.getClass().getSimpleName(), this.getCodecClass().getSimpleName(), supportedString); } /** * Returns a formatted message indicating an unexpected Java type encountered during decoding. * * @param type the unexpected Java type encountered. * @return a formatted message string. */ protected String unexpectedTypeWhileDecoding(final Type type) { return MessageFormat.format( "Unexpected type `{0}` decoding with `{1}<{2}>`", type, this.getClass().getSimpleName(), this.getCodecClass().getSimpleName()); } /** * Returns a formatted message indicating an unsupported Java type encountered during encoding. * * @param type the unsupported Java type encountered. * @return a formatted message string. */ protected String unsupportedTypeMessage(final Type type) { return MessageFormat.format("Cannot encode `{0}` with `{1}<{2}>`", type, this.getClass(), this.getCodecClass()); } } ``` ## File: src/main/java/com/fauna/codec/codecs/BaseDocumentCodec.java ```java package com.fauna.codec.codecs; import com.fauna.codec.CodecProvider; import com.fauna.codec.FaunaTokenType; import com.fauna.codec.FaunaType; import com.fauna.codec.UTF8FaunaGenerator; import com.fauna.codec.UTF8FaunaParser; import com.fauna.exception.CodecException; import com.fauna.types.BaseDocument; import com.fauna.types.Document; import com.fauna.types.NamedDocument; /** * Codec for encoding and decoding FQL {@link BaseDocument} instances. */ public final class BaseDocumentCodec extends BaseCodec { private final CodecProvider provider; /** * Constructs a {@code BaseDocumentCodec} with the specified codec provider. * * @param provider the codec provider */ public BaseDocumentCodec(final CodecProvider provider) { this.provider = provider; } @Override public BaseDocument decode(final UTF8FaunaParser parser) throws CodecException { switch (parser.getCurrentTokenType()) { case NULL: return null; case START_REF: var o = BaseRefCodec.SINGLETON.decode(parser); throw new CodecException(unexpectedTypeWhileDecoding(o.getClass())); case START_DOCUMENT: return (BaseDocument) decodeInternal(parser); default: throw new CodecException(this.unsupportedTypeDecodingMessage( parser.getCurrentTokenType().getFaunaType(), getSupportedTypes())); } } private Object decodeInternal(final UTF8FaunaParser parser) throws CodecException { var builder = new InternalDocument.Builder(); var valueCodec = provider.get(Object.class); while (parser.read() && parser.getCurrentTokenType() != FaunaTokenType.END_DOCUMENT) { if (parser.getCurrentTokenType() != FaunaTokenType.FIELD_NAME) { throw new CodecException(unexpectedTokenExceptionMessage(parser.getCurrentTokenType())); } String fieldName = parser.getValueAsString(); parser.read(); switch (fieldName) { case "id": case "name": case "ts": case "coll": builder = builder.withDocField(fieldName, parser); break; default: var v = valueCodec.decode(parser); if (v != null) { builder = builder.withDataField(fieldName, v); } break; } } return builder.build(); } @Override public void encode(final UTF8FaunaGenerator gen, final BaseDocument obj) throws CodecException { gen.writeStartRef(); if (obj instanceof Document) { gen.writeString("id", ((Document) obj).getId()); } else if (obj instanceof NamedDocument) { gen.writeString("name", ((NamedDocument) obj).getName()); } gen.writeModule("coll", obj.getCollection()); gen.writeEndRef(); } @Override public Class getCodecClass() { return BaseDocument.class; } @Override public FaunaType[] getSupportedTypes() { return new FaunaType[]{FaunaType.Document, FaunaType.Null, FaunaType.Ref}; } } ``` ## File: src/main/java/com/fauna/codec/codecs/BaseRefCodec.java ```java package com.fauna.codec.codecs; import com.fauna.codec.FaunaTokenType; import com.fauna.codec.FaunaType; import com.fauna.codec.UTF8FaunaGenerator; import com.fauna.codec.UTF8FaunaParser; import com.fauna.exception.CodecException; import com.fauna.types.BaseRef; import com.fauna.types.DocumentRef; import com.fauna.types.NamedDocumentRef; /** * Codec for encoding and decoding FQL {@link BaseRef} instances. */ public final class BaseRefCodec extends BaseCodec { public static final BaseRefCodec SINGLETON = new BaseRefCodec(); @Override public BaseRef decode(final UTF8FaunaParser parser) throws CodecException { switch (parser.getCurrentTokenType()) { case NULL: return null; case START_REF: return (BaseRef) decodeInternal(parser); default: throw new CodecException(this.unsupportedTypeDecodingMessage( parser.getCurrentTokenType().getFaunaType(), getSupportedTypes())); } } private Object decodeInternal(final UTF8FaunaParser parser) throws CodecException { var builder = new InternalDocument.Builder(); while (parser.read() && parser.getCurrentTokenType() != FaunaTokenType.END_REF) { if (parser.getCurrentTokenType() != FaunaTokenType.FIELD_NAME) { throw new CodecException(unexpectedTokenExceptionMessage(parser.getCurrentTokenType())); } String fieldName = parser.getValueAsString(); parser.read(); switch (fieldName) { case "id": case "name": case "coll": case "exists": case "cause": builder = builder.withRefField(fieldName, parser); break; default: break; } } return builder.build(); } @Override public void encode(final UTF8FaunaGenerator gen, final BaseRef obj) throws CodecException { gen.writeStartRef(); if (obj instanceof DocumentRef) { gen.writeString("id", ((DocumentRef) obj).getId()); } else if (obj instanceof NamedDocumentRef) { gen.writeString("name", ((NamedDocumentRef) obj).getName()); } gen.writeModule("coll", obj.getCollection()); gen.writeEndRef(); } @Override public Class getCodecClass() { return BaseRef.class; } @Override public FaunaType[] getSupportedTypes() { return new FaunaType[]{FaunaType.Null, FaunaType.Ref}; } } ``` ## File: src/main/java/com/fauna/codec/codecs/BoolCodec.java ```java package com.fauna.codec.codecs; import com.fauna.codec.FaunaType; import com.fauna.codec.UTF8FaunaGenerator; import com.fauna.codec.UTF8FaunaParser; import com.fauna.exception.CodecException; /** * Codec for encoding and decoding FQL boolean values. */ public final class BoolCodec extends BaseCodec { public static final BoolCodec SINGLETON = new BoolCodec(); @Override public Boolean decode(final UTF8FaunaParser parser) throws CodecException { switch (parser.getCurrentTokenType()) { case NULL: return null; case TRUE: case FALSE: return parser.getValueAsBoolean(); default: throw new CodecException(this.unsupportedTypeDecodingMessage( parser.getCurrentTokenType().getFaunaType(), getSupportedTypes())); } } @Override public void encode(final UTF8FaunaGenerator gen, final Boolean obj) throws CodecException { if (obj == null) { gen.writeNullValue(); } else { gen.writeBooleanValue(obj); } } @Override public Class getCodecClass() { return Boolean.class; } @Override public FaunaType[] getSupportedTypes() { return new FaunaType[] {FaunaType.Boolean, FaunaType.Null}; } } ``` ## File: src/main/java/com/fauna/codec/codecs/ByteArrayCodec.java ```java package com.fauna.codec.codecs; import com.fauna.codec.FaunaType; import com.fauna.codec.UTF8FaunaGenerator; import com.fauna.codec.UTF8FaunaParser; import com.fauna.exception.CodecException; /** * Codec for encoding and decoding FQL byte arrays. */ public final class ByteArrayCodec extends BaseCodec { public static final ByteArrayCodec SINGLETON = new ByteArrayCodec(); /** * Decodes a byte array from the parser. * * @param parser the parser to read from * @return the decoded byte array, or null if the token represents a null value * @throws CodecException if decoding fails due to an unexpected type */ @Override public byte[] decode(final UTF8FaunaParser parser) throws CodecException { switch (parser.getCurrentTokenType()) { case NULL: return null; case BYTES: return parser.getValueAsByteArray(); default: throw new CodecException(this.unsupportedTypeDecodingMessage( parser.getCurrentTokenType().getFaunaType(), getSupportedTypes())); } } /** * Encodes a byte array to the generator. * * @param gen the generator to write to * @param obj the byte array to encode * @throws CodecException if encoding fails */ @Override public void encode(final UTF8FaunaGenerator gen, final byte[] obj) throws CodecException { if (obj == null) { gen.writeNullValue(); return; } gen.writeBytesValue(obj); } /** * Returns the class type this codec supports. * * @return byte array class */ @Override public Class getCodecClass() { return byte[].class; } /** * Returns the Fauna types this codec supports. * * @return supported Fauna types */ @Override public FaunaType[] getSupportedTypes() { return new FaunaType[] {FaunaType.Bytes, FaunaType.Null}; } } ``` ## File: src/main/java/com/fauna/codec/codecs/ByteCodec.java ```java package com.fauna.codec.codecs; import com.fauna.codec.FaunaType; import com.fauna.codec.UTF8FaunaGenerator; import com.fauna.codec.UTF8FaunaParser; import com.fauna.exception.CodecException; /** * Codec for encoding and decoding {@code Byte} values in Fauna's tagged data format. */ public final class ByteCodec extends BaseCodec { public static final ByteCodec SINGLETON = new ByteCodec(); /** * Decodes a {@code Byte} from the parser. * * @param parser the parser to read from * @return the decoded {@code Byte} value, or null if the token represents a null value * @throws CodecException if decoding fails due to an unexpected type */ @Override public Byte decode(final UTF8FaunaParser parser) throws CodecException { switch (parser.getCurrentTokenType()) { case NULL: return null; case INT: return parser.getValueAsByte(); default: throw new CodecException(this.unsupportedTypeDecodingMessage( parser.getCurrentTokenType().getFaunaType(), getSupportedTypes())); } } /** * Encodes a {@code Byte} value to the generator. * * @param gen the generator to write to * @param obj the {@code Byte} value to encode * @throws CodecException if encoding fails */ @Override public void encode(final UTF8FaunaGenerator gen, final Byte obj) throws CodecException { if (obj == null) { gen.writeNullValue(); } else { gen.writeIntValue(obj); } } /** * Returns the class type this codec supports. * * @return {@code Byte} class */ @Override public Class getCodecClass() { return Byte.class; } /** * Returns the Fauna types this codec supports. * * @return supported Fauna types */ @Override public FaunaType[] getSupportedTypes() { return new FaunaType[] {FaunaType.Int, FaunaType.Null}; } } ``` ## File: src/main/java/com/fauna/codec/codecs/CharCodec.java ```java package com.fauna.codec.codecs; import com.fauna.codec.FaunaType; import com.fauna.codec.UTF8FaunaGenerator; import com.fauna.codec.UTF8FaunaParser; import com.fauna.exception.CodecException; /** * Codec for encoding and decoding {@code Character} values in Fauna's tagged data format. */ public final class CharCodec extends BaseCodec { public static final CharCodec SINGLETON = new CharCodec(); /** * Decodes a {@code Character} from the parser. * * @param parser the parser to read from * @return the decoded {@code Character} value, or null if the token represents a null value * @throws CodecException if decoding fails due to an unexpected type */ @Override public Character decode(final UTF8FaunaParser parser) throws CodecException { switch (parser.getCurrentTokenType()) { case NULL: return null; case INT: return parser.getValueAsCharacter(); default: throw new CodecException(this.unsupportedTypeDecodingMessage( parser.getCurrentTokenType().getFaunaType(), getSupportedTypes())); } } /** * Encodes a {@code Character} value to the generator. * * @param gen the generator to write to * @param obj the {@code Character} value to encode * @throws CodecException if encoding fails */ @Override public void encode(final UTF8FaunaGenerator gen, final Character obj) throws CodecException { if (obj == null) { gen.writeNullValue(); } else { gen.writeCharValue(obj); } } /** * Returns the class type this codec supports. * * @return {@code Character} class */ @Override public Class getCodecClass() { return Character.class; } /** * Returns the Fauna types this codec supports. * * @return supported Fauna types */ @Override public FaunaType[] getSupportedTypes() { return new FaunaType[] {FaunaType.Int, FaunaType.Null}; } } ``` ## File: src/main/java/com/fauna/codec/codecs/ClassCodec.java ```java package com.fauna.codec.codecs; import com.fauna.annotation.FaunaColl; import com.fauna.annotation.FaunaField; import com.fauna.annotation.FaunaFieldImpl; import com.fauna.annotation.FaunaId; import com.fauna.annotation.FaunaIdImpl; import com.fauna.annotation.FaunaIgnore; import com.fauna.annotation.FaunaTs; import com.fauna.codec.Codec; import com.fauna.codec.CodecProvider; import com.fauna.codec.FaunaTokenType; import com.fauna.codec.FaunaType; import com.fauna.codec.UTF8FaunaGenerator; import com.fauna.codec.UTF8FaunaParser; import com.fauna.exception.CodecException; import com.fauna.mapping.FieldInfo; import com.fauna.mapping.FieldName; import com.fauna.mapping.FieldType; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; /** * A codec for encoding and decoding Java classes, handling Fauna-specific annotations and types. * * @param The type of the class to encode/decode. */ public final class ClassCodec extends BaseCodec { private static final String ID_FIELD = "id"; private static final String NAME_FIELD = "name"; private final Class type; private final List fields; private final Map fieldsByName; private final boolean shouldEscapeObject; /** * Constructs a {@code ClassCodec} for a given type, initializing field mappings based on Fauna annotations. * * @param ty The class type. * @param provider The codec provider for resolving codecs for field types. */ public ClassCodec(final Class ty, final CodecProvider provider) { this.type = ty; List fieldsList = new ArrayList<>(); Map byNameMap = new HashMap<>(); List fields = new ArrayList<>(); for (Class c = type; c != null; c = c.getSuperclass()) { fields.addAll(Arrays.asList(c.getDeclaredFields())); } for (Field field : fields) { if (field.getAnnotation(FaunaIgnore.class) != null) { continue; } FieldType fieldType = getFieldType(field); var attr = new FaunaFieldImpl(field.getAnnotation(FaunaField.class)); var name = attr.name() != null ? attr.name() : FieldName.canonical(field.getName()); if (byNameMap.containsKey(name)) { throw new IllegalArgumentException( "Duplicate field name " + name + " in " + ty); } Type type = field.getGenericType(); FieldInfo info; // Don't init the codec here because of potential circular references; instead use a provider. if (type instanceof ParameterizedType) { ParameterizedType pType = (ParameterizedType) type; info = new FieldInfo(field, name, (Class) pType.getRawType(), pType.getActualTypeArguments(), provider, fieldType); } else { info = new FieldInfo(field, name, field.getType(), null, provider, fieldType); } fieldsList.add(info); byNameMap.put(info.getName(), info); } this.shouldEscapeObject = TAGS.stream().anyMatch(byNameMap.keySet()::contains); this.fields = List.copyOf(fieldsList); this.fieldsByName = Map.copyOf(byNameMap); } private FieldType getFieldType(final Field field) { if (field.getAnnotation(FaunaId.class) != null) { var impl = new FaunaIdImpl(field.getAnnotation(FaunaId.class)); if (impl.isClientGenerate()) { return FieldType.ClientGeneratedId; } else { return FieldType.ServerGeneratedId; } } if (field.getAnnotation(FaunaTs.class) != null) { return FieldType.Ts; } if (field.getAnnotation(FaunaColl.class) != null) { return FieldType.Coll; } return FieldType.Field; } @Override public T decode(final UTF8FaunaParser parser) throws CodecException { switch (parser.getCurrentTokenType()) { case NULL: return null; case START_REF: case START_DOCUMENT: case START_OBJECT: try { FaunaTokenType endToken = parser.getCurrentTokenType().getEndToken(); Object instance = createInstance(); setFields(instance, parser, endToken); @SuppressWarnings("unchecked") T typed = (T) instance; return typed; } catch (IllegalAccessException | ClassNotFoundException | InvocationTargetException | InstantiationException | NoSuchMethodException e) { throw new RuntimeException(e); } default: throw new CodecException(this.unsupportedTypeDecodingMessage( parser.getCurrentTokenType().getFaunaType(), getSupportedTypes())); } } @Override public void encode(final UTF8FaunaGenerator gen, final T obj) throws CodecException { if (shouldEscapeObject) { gen.writeStartEscapedObject(); } else { gen.writeStartObject(); } for (FieldInfo fi : fields) { if (!fi.getName().startsWith("this$")) { var fieldType = fi.getFieldType(); if (fieldType == FieldType.Coll || fieldType == FieldType.Ts || fieldType == FieldType.ServerGeneratedId) { // never encode coll and ts and server generated IDs continue; } var fieldName = fi.getName(); try { fi.getField().setAccessible(true); @SuppressWarnings("unchecked") T value = obj != null ? (T) fi.getField().get(obj) : null; if (fieldType == FieldType.ClientGeneratedId && value == null) { // The field is a client generated ID but set to null, so assume they're doing something // other than creating the object. continue; } gen.writeFieldName(fieldName); @SuppressWarnings("unchecked") Codec codec = fi.getCodec(); codec.encode(gen, value); } catch (IllegalAccessException e) { throw new CodecException( "Error accessing field: " + fi.getName(), e); } } } if (shouldEscapeObject) { gen.writeEndEscapedObject(); } else { gen.writeEndObject(); } } @Override public Class getCodecClass() { return this.type; } @Override public FaunaType[] getSupportedTypes() { return new FaunaType[] {FaunaType.Document, FaunaType.Null, FaunaType.Object, FaunaType.Ref}; } private Object createInstance() throws InvocationTargetException, InstantiationException, IllegalAccessException, ClassNotFoundException, NoSuchMethodException { Class clazz = Class.forName(type.getTypeName()); Constructor constructor = clazz.getConstructor(); return constructor.newInstance(); } private void setFields(final Object instance, final UTF8FaunaParser parser, final FaunaTokenType endToken) throws IllegalAccessException { InternalDocument.Builder builder = new InternalDocument.Builder(); while (parser.read() && parser.getCurrentTokenType() != endToken) { if (parser.getCurrentTokenType() != FaunaTokenType.FIELD_NAME) { throw new CodecException(unexpectedTokenExceptionMessage( parser.getCurrentTokenType())); } String fieldName = parser.getValueAsString(); parser.read(); if (endToken == FaunaTokenType.END_REF) { builder = builder.withRefField(fieldName, parser); } if (fieldName.equals(ID_FIELD)) { trySetId(fieldName, instance, parser); } else if (fieldName.equals(NAME_FIELD)) { trySetName(fieldName, instance, parser); } else { trySetField(fieldName, instance, parser); } } // Throws if it does not exist, otherwise no-op. builder.build(); } private void trySetId(final String fieldName, final Object instance, final UTF8FaunaParser parser) throws IllegalAccessException { if (parser.getCurrentTokenType() != FaunaTokenType.STRING) { return; } FieldInfo field = fieldsByName.get(fieldName); if (field != null) { String id = parser.getValueAsString(); field.getField().setAccessible(true); if (field.getType() == Long.class) { field.getField().set(instance, Long.parseLong(id)); } else if (field.getType() == String.class) { field.getField().set(instance, id); } } } private void trySetName(final String fieldName, final Object instance, final UTF8FaunaParser parser) throws IllegalAccessException { if (parser.getCurrentTokenType() != FaunaTokenType.STRING) { return; } FieldInfo field = fieldsByName.get(fieldName); if (field != null) { String name = parser.getValueAsString(); field.getField().setAccessible(true); if (field.getType() == String.class) { field.getField().set(instance, name); } } } private void trySetField(final String fieldName, final Object instance, final UTF8FaunaParser parser) throws IllegalAccessException { FieldInfo field = fieldsByName.get(fieldName); if (field == null) { parser.skip(); } else { field.getField().setAccessible(true); field.getField().set(instance, field.getCodec().decode(parser)); } } } ``` ## File: src/main/java/com/fauna/codec/codecs/DoubleCodec.java ```java package com.fauna.codec.codecs; import com.fauna.codec.FaunaType; import com.fauna.codec.UTF8FaunaGenerator; import com.fauna.codec.UTF8FaunaParser; import com.fauna.exception.CodecException; /** * Codec for encoding and decoding {@link Double} values in Fauna's tagged data format. */ public final class DoubleCodec extends BaseCodec { public static final DoubleCodec SINGLETON = new DoubleCodec(); /** * Decodes a {@code Double} value from the Fauna tagged data format. * * @param parser The parser instance for reading Fauna tagged format data. * @return The decoded {@code Double} value or {@code null} if the token is {@code NULL}. * @throws CodecException If the token type is unsupported for decoding a {@code Double}. */ @Override public Double decode(final UTF8FaunaParser parser) throws CodecException { switch (parser.getCurrentTokenType()) { case NULL: return null; case INT: case LONG: case DOUBLE: return parser.getValueAsDouble(); default: throw new CodecException(this.unsupportedTypeDecodingMessage( parser.getCurrentTokenType().getFaunaType(), getSupportedTypes())); } } /** * Encodes a {@code Double} value to Fauna's tagged data format. * * @param gen The generator used to write Fauna tagged format data. * @param obj The {@code Double} value to encode, or {@code null} to write a {@code NULL} value. * @throws CodecException If encoding fails. */ @Override public void encode(final UTF8FaunaGenerator gen, final Double obj) throws CodecException { if (obj == null) { gen.writeNullValue(); } else { gen.writeDoubleValue(obj); } } /** * Returns the class of the codec, which is {@code Double}. * * @return {@code Double.class}. */ @Override public Class getCodecClass() { return Double.class; } /** * Returns the Fauna types supported by this codec. * * @return An array of {@link FaunaType} supported by this codec. */ @Override public FaunaType[] getSupportedTypes() { return new FaunaType[] {FaunaType.Double, FaunaType.Int, FaunaType.Long, FaunaType.Null}; } } ``` ## File: src/main/java/com/fauna/codec/codecs/DynamicCodec.java ```java package com.fauna.codec.codecs; import com.fauna.codec.Codec; import com.fauna.codec.CodecProvider; import com.fauna.codec.FaunaType; import com.fauna.codec.UTF8FaunaGenerator; import com.fauna.codec.UTF8FaunaParser; import com.fauna.event.EventSource; import com.fauna.exception.CodecException; import com.fauna.types.Document; import com.fauna.types.DocumentRef; import com.fauna.types.Page; import java.util.List; import java.util.Map; /** * Codec for dynamically encoding and decoding various FQL types. *

* This codec adapts to different FQL types by delegating to other codecs as needed. */ public final class DynamicCodec extends BaseCodec { private final ListCodec> list = new ListCodec<>(this); private final PageCodec> page = new PageCodec<>(this); private final MapCodec> map = new MapCodec<>(this); private final CodecProvider provider; /** * Constructs a {@code DynamicCodec} with the specified {@code CodecProvider}. * * @param provider The codec provider used to retrieve codecs. */ public DynamicCodec(final CodecProvider provider) { this.provider = provider; } @Override public Object decode(final UTF8FaunaParser parser) throws CodecException { switch (parser.getCurrentTokenType()) { case NULL: return null; case BYTES: return provider.get(byte[].class).decode(parser); case START_OBJECT: return map.decode(parser); case START_ARRAY: return list.decode(parser); case START_PAGE: return page.decode(parser); case START_REF: return provider.get(DocumentRef.class).decode(parser); case START_DOCUMENT: return provider.get(Document.class).decode(parser); case STREAM: return provider.get(EventSource.class).decode(parser); case MODULE: return parser.getValueAsModule(); case INT: return parser.getValueAsInt(); case STRING: return parser.getValueAsString(); case DATE: return parser.getValueAsLocalDate(); case TIME: return parser.getValueAsTime(); case DOUBLE: return parser.getValueAsDouble(); case LONG: return parser.getValueAsLong(); case TRUE: case FALSE: return parser.getValueAsBoolean(); default: break; } throw new CodecException(this.unsupportedTypeDecodingMessage( parser.getCurrentTokenType().getFaunaType(), getSupportedTypes())); } @Override @SuppressWarnings("unchecked") public void encode(final UTF8FaunaGenerator gen, final Object obj) throws CodecException { @SuppressWarnings("rawtypes") Codec codec = provider.get(obj.getClass()); codec.encode(gen, obj); } @Override public Class getCodecClass() { return Object.class; } @Override public FaunaType[] getSupportedTypes() { return new FaunaType[] {FaunaType.Array, FaunaType.Boolean, FaunaType.Bytes, FaunaType.Date, FaunaType.Double, FaunaType.Document, FaunaType.Int, FaunaType.Long, FaunaType.Module, FaunaType.Null, FaunaType.Object, FaunaType.Ref, FaunaType.Set, FaunaType.Stream, FaunaType.String, FaunaType.Time}; } } ``` ## File: src/main/java/com/fauna/codec/codecs/EnumCodec.java ```java package com.fauna.codec.codecs; import com.fauna.codec.FaunaType; import com.fauna.codec.UTF8FaunaGenerator; import com.fauna.codec.UTF8FaunaParser; import com.fauna.exception.CodecException; /** * Codec for encoding and decoding Java Enum types in the Fauna tagged data format. * * @param The type of the enum. */ public final class EnumCodec extends BaseCodec { private final Class enumType; /** * Constructs an {@code EnumCodec} for the specified enum type. * * @param enumType The enum class to be encoded and decoded. */ public EnumCodec(final Class enumType) { this.enumType = enumType; } @Override public T decode(final UTF8FaunaParser parser) throws CodecException { switch (parser.getCurrentTokenType()) { case NULL: return null; case STRING: //noinspection unchecked,rawtypes return (T) Enum.valueOf((Class) enumType, parser.getValueAsString()); default: throw new CodecException(this.unsupportedTypeDecodingMessage( parser.getCurrentTokenType().getFaunaType(), getSupportedTypes())); } } @Override public void encode(final UTF8FaunaGenerator gen, final T obj) throws CodecException { if (obj == null) { gen.writeNullValue(); } else { gen.writeStringValue(((Enum) obj).name()); } } @Override public Class getCodecClass() { return enumType; } @Override public FaunaType[] getSupportedTypes() { return new FaunaType[] {FaunaType.Null, FaunaType.String}; } } ``` ## File: src/main/java/com/fauna/codec/codecs/EventSourceCodec.java ```java package com.fauna.codec.codecs; import com.fauna.codec.FaunaTokenType; import com.fauna.codec.FaunaType; import com.fauna.codec.UTF8FaunaGenerator; import com.fauna.codec.UTF8FaunaParser; import com.fauna.event.EventSource; import com.fauna.exception.CodecException; /** * Codec for encoding and decoding {@link EventSource} instances in the Fauna tagged format. */ public final class EventSourceCodec extends BaseCodec { @Override public EventSource decode(final UTF8FaunaParser parser) throws CodecException { if (parser.getCurrentTokenType() == FaunaTokenType.STREAM) { return new EventSource(parser.getTaggedValueAsString()); } else { throw new CodecException(this.unsupportedTypeDecodingMessage( parser.getCurrentTokenType().getFaunaType(), getSupportedTypes())); } } @Override public void encode(final UTF8FaunaGenerator gen, final EventSource obj) throws CodecException { throw new CodecException("Cannot encode EventSource"); } @Override public Class getCodecClass() { return EventSource.class; } @Override public FaunaType[] getSupportedTypes() { return new FaunaType[] {FaunaType.Stream}; } } ``` ## File: src/main/java/com/fauna/codec/codecs/FloatCodec.java ```java package com.fauna.codec.codecs; import com.fauna.codec.FaunaType; import com.fauna.codec.UTF8FaunaGenerator; import com.fauna.codec.UTF8FaunaParser; import com.fauna.exception.CodecException; /** * Codec for encoding and decoding {@link Float} values in the Fauna tagged data format. */ public final class FloatCodec extends BaseCodec { public static final FloatCodec SINGLETON = new FloatCodec(); @Override public Float decode(final UTF8FaunaParser parser) throws CodecException { switch (parser.getCurrentTokenType()) { case NULL: return null; case INT: case LONG: case DOUBLE: return parser.getValueAsFloat(); default: throw new CodecException(this.unsupportedTypeDecodingMessage( parser.getCurrentTokenType().getFaunaType(), getSupportedTypes())); } } @Override public void encode(final UTF8FaunaGenerator gen, final Float obj) throws CodecException { if (obj == null) { gen.writeNullValue(); } else { gen.writeDoubleValue(obj); } } @Override public Class getCodecClass() { return Float.class; } @Override public FaunaType[] getSupportedTypes() { return new FaunaType[] {FaunaType.Double, FaunaType.Int, FaunaType.Long, FaunaType.Null}; } } ``` ## File: src/main/java/com/fauna/codec/codecs/InstantCodec.java ```java package com.fauna.codec.codecs; import com.fauna.codec.FaunaType; import com.fauna.codec.UTF8FaunaGenerator; import com.fauna.codec.UTF8FaunaParser; import com.fauna.exception.CodecException; import java.time.Instant; /** * Codec for encoding and decoding {@link Instant} values in Fauna's tagged data format. */ public final class InstantCodec extends BaseCodec { public static final InstantCodec SINGLETON = new InstantCodec(); @Override public Instant decode(final UTF8FaunaParser parser) throws CodecException { switch (parser.getCurrentTokenType()) { case NULL: return null; case TIME: return parser.getValueAsTime(); default: throw new CodecException(this.unsupportedTypeDecodingMessage( parser.getCurrentTokenType().getFaunaType(), getSupportedTypes())); } } @Override public void encode(final UTF8FaunaGenerator gen, final Instant obj) throws CodecException { if (obj == null) { gen.writeNullValue(); } else { gen.writeTimeValue(obj); } } @Override public Class getCodecClass() { return Instant.class; } @Override public FaunaType[] getSupportedTypes() { return new FaunaType[] {FaunaType.Null, FaunaType.Time}; } } ``` ## File: src/main/java/com/fauna/codec/codecs/IntCodec.java ```java package com.fauna.codec.codecs; import com.fauna.codec.FaunaType; import com.fauna.codec.UTF8FaunaGenerator; import com.fauna.codec.UTF8FaunaParser; import com.fauna.exception.CodecException; /** * Codec for encoding and decoding Integer values. */ public final class IntCodec extends BaseCodec { public static final IntCodec SINGLETON = new IntCodec(); @Override public Integer decode(final UTF8FaunaParser parser) throws CodecException { switch (parser.getCurrentTokenType()) { case NULL: return null; case INT: case LONG: return parser.getValueAsInt(); default: throw new CodecException(this.unsupportedTypeDecodingMessage( parser.getCurrentTokenType().getFaunaType(), getSupportedTypes())); } } @Override public void encode(final UTF8FaunaGenerator gen, final Integer obj) throws CodecException { if (obj == null) { gen.writeNullValue(); } else { gen.writeIntValue(obj); } } @Override public Class getCodecClass() { return Integer.class; } @Override public FaunaType[] getSupportedTypes() { return new FaunaType[] {FaunaType.Int, FaunaType.Long, FaunaType.Null}; } } ``` ## File: src/main/java/com/fauna/codec/codecs/InternalDocument.java ```java package com.fauna.codec.codecs; import com.fauna.codec.FaunaTokenType; import com.fauna.codec.UTF8FaunaParser; import com.fauna.exception.NullDocumentException; import com.fauna.types.Document; import com.fauna.types.DocumentRef; import com.fauna.types.Module; import com.fauna.types.NamedDocument; import com.fauna.types.NamedDocumentRef; import java.time.Instant; import java.util.HashMap; import java.util.Map; class InternalDocument { /** * Builder class for constructing internal document representations. */ static class Builder { private String id = null; private String name = null; private Module coll = null; private Boolean exists = null; private String cause = null; private Instant ts = null; private final Map data = new HashMap<>(); /** * Adds a data field to the document. * * @param key The field name. * @param value The field value. * @return This builder. */ InternalDocument.Builder withDataField(String key, Object value) { data.put(key, value); return this; } /** * Adds document-specific fields such as id, name, collection, and timestamp. * * @param fieldName The field name. * @param parser The parser used to read values. * @return This builder. */ InternalDocument.Builder withDocField(String fieldName, UTF8FaunaParser parser) { switch (fieldName) { case "id": if (parser.getCurrentTokenType() == FaunaTokenType.STRING) { this.id = parser.getValueAsString(); } break; case "name": if (parser.getCurrentTokenType() == FaunaTokenType.STRING) { this.name = parser.getValueAsString(); } break; case "coll": if (parser.getCurrentTokenType() == FaunaTokenType.MODULE) { this.coll = parser.getValueAsModule(); } break; case "ts": if (parser.getCurrentTokenType() == FaunaTokenType.TIME) { this.ts = parser.getValueAsTime(); } } return this; } /** * Adds reference-specific fields like id, name, collection, exists, and cause. * * @param fieldName The field name. * @param parser The parser used to read values. * @return This builder. */ InternalDocument.Builder withRefField(String fieldName, UTF8FaunaParser parser) { switch (fieldName) { case "id": if (parser.getCurrentTokenType() == FaunaTokenType.STRING) { this.id = parser.getValueAsString(); } break; case "name": if (parser.getCurrentTokenType() == FaunaTokenType.STRING) { this.name = parser.getValueAsString(); } break; case "coll": if (parser.getCurrentTokenType() == FaunaTokenType.MODULE) { this.coll = parser.getValueAsModule(); } break; case "exists": if (parser.getCurrentTokenType() == FaunaTokenType.FALSE || parser.getCurrentTokenType() == FaunaTokenType.TRUE) { this.exists = parser.getValueAsBoolean(); } break; case "cause": if (parser.getCurrentTokenType() == FaunaTokenType.STRING) { this.cause = parser.getValueAsString(); } break; } return this; } /** * Builds and returns the constructed document or reference object. * * @return The constructed document or reference object. * @throws NullDocumentException If the document is marked as "exists: false" but lacks an id or name. */ Object build() { if (exists != null && !exists) { throw new NullDocumentException(id != null ? id : name, coll, cause); } if (id != null && coll != null && ts != null) { if (name != null) { data.put("name", name); } return new Document(id, coll, ts, data); } if (id != null && coll != null) { return new DocumentRef(id, coll); } if (name != null && coll != null && ts != null) { return new NamedDocument(name, coll, ts, data); } if (name != null && coll != null) { return new NamedDocumentRef(name, coll); } if (id != null) { data.put("id", id); } if (name != null) { data.put("name", name); } if (coll != null) { data.put("coll", coll); } if (ts != null) { data.put("ts", ts); } if (exists != null) { data.put("exists", exists); } if (cause != null) { data.put("cause", cause); } return data; } } } ``` ## File: src/main/java/com/fauna/codec/codecs/ListCodec.java ```java package com.fauna.codec.codecs; import com.fauna.codec.Codec; import com.fauna.codec.FaunaTokenType; import com.fauna.codec.FaunaType; import com.fauna.codec.UTF8FaunaGenerator; import com.fauna.codec.UTF8FaunaParser; import com.fauna.exception.CodecException; import java.util.ArrayList; import java.util.List; /** * A codec for encoding and decoding lists of elements in Fauna's tagged data format. * * @param The type of elements in the list. * @param The type of the list that will hold the elements. */ public final class ListCodec> extends BaseCodec { private final Codec elementCodec; /** * Creates a codec for encoding and decoding lists of elements. * * @param elementCodec The codec used to encode/decode elements of the list. */ public ListCodec(final Codec elementCodec) { this.elementCodec = elementCodec; } @Override public L decode(final UTF8FaunaParser parser) throws CodecException { switch (parser.getCurrentTokenType()) { case NULL: return null; case START_ARRAY: List list = new ArrayList<>(); while (parser.read() && parser.getCurrentTokenType() != FaunaTokenType.END_ARRAY) { E value = elementCodec.decode(parser); list.add(value); } @SuppressWarnings("unchecked") var typed = (L) list; return typed; default: throw new CodecException(this.unsupportedTypeDecodingMessage( parser.getCurrentTokenType().getFaunaType(), getSupportedTypes())); } } @Override public void encode(final UTF8FaunaGenerator gen, final L obj) throws CodecException { if (obj == null) { gen.writeNullValue(); return; } gen.writeStartArray(); for (E elem : obj) { elementCodec.encode(gen, elem); } gen.writeEndArray(); } @Override public Class getCodecClass() { return elementCodec.getCodecClass(); } @Override public FaunaType[] getSupportedTypes() { return new FaunaType[] {FaunaType.Array, FaunaType.Null}; } } ``` ## File: src/main/java/com/fauna/codec/codecs/LocalDateCodec.java ```java package com.fauna.codec.codecs; import com.fauna.codec.FaunaType; import com.fauna.codec.UTF8FaunaGenerator; import com.fauna.codec.UTF8FaunaParser; import com.fauna.exception.CodecException; import java.time.LocalDate; /** * A codec for encoding and decoding {@link LocalDate} in Fauna's tagged data format. */ public final class LocalDateCodec extends BaseCodec { public static final LocalDateCodec SINGLETON = new LocalDateCodec(); @Override public LocalDate decode(final UTF8FaunaParser parser) throws CodecException { switch (parser.getCurrentTokenType()) { case NULL: return null; case DATE: return parser.getValueAsLocalDate(); default: throw new CodecException(this.unsupportedTypeDecodingMessage( parser.getCurrentTokenType().getFaunaType(), getSupportedTypes())); } } @Override public void encode(final UTF8FaunaGenerator gen, final LocalDate obj) throws CodecException { if (obj == null) { gen.writeNullValue(); } else { gen.writeDateValue(obj); } } @Override public Class getCodecClass() { return LocalDate.class; } @Override public FaunaType[] getSupportedTypes() { return new FaunaType[] {FaunaType.Date, FaunaType.Null}; } } ``` ## File: src/main/java/com/fauna/codec/codecs/LongCodec.java ```java package com.fauna.codec.codecs; import com.fauna.codec.FaunaType; import com.fauna.codec.UTF8FaunaGenerator; import com.fauna.codec.UTF8FaunaParser; import com.fauna.exception.CodecException; /** * A codec for encoding and decoding {@link Long} values in Fauna's tagged data format. */ public final class LongCodec extends BaseCodec { public static final LongCodec SINGLETON = new LongCodec(); @Override public Long decode(final UTF8FaunaParser parser) throws CodecException { switch (parser.getCurrentTokenType()) { case NULL: return null; case INT: case LONG: return parser.getValueAsLong(); default: throw new CodecException(this.unsupportedTypeDecodingMessage( parser.getCurrentTokenType().getFaunaType(), getSupportedTypes())); } } @Override public void encode(final UTF8FaunaGenerator gen, final Long obj) throws CodecException { if (obj == null) { gen.writeNullValue(); } else { gen.writeLongValue(obj); } } @Override public Class getCodecClass() { return Long.class; } @Override public FaunaType[] getSupportedTypes() { return new FaunaType[] {FaunaType.Int, FaunaType.Long, FaunaType.Null}; } } ``` ## File: src/main/java/com/fauna/codec/codecs/MapCodec.java ```java package com.fauna.codec.codecs; import com.fauna.codec.Codec; import com.fauna.codec.FaunaTokenType; import com.fauna.codec.FaunaType; import com.fauna.codec.UTF8FaunaGenerator; import com.fauna.codec.UTF8FaunaParser; import com.fauna.exception.CodecException; import java.util.HashMap; import java.util.Map; /** * A codec for encoding and decoding {@link Map} values in Fauna's tagged data format. *

* This class handles encoding and decoding of maps, where the keys are strings and the values are of a generic * type {@code V}. *

* * @param The type of the values in the map. * @param The type of the map. */ public final class MapCodec> extends BaseCodec { private final Codec valueCodec; /** * Constructs a {@code MapCodec} with the specified {@code Codec}. * * @param valueCodec The codec to use for the value. */ public MapCodec(final Codec valueCodec) { this.valueCodec = valueCodec; } @Override public L decode(final UTF8FaunaParser parser) throws CodecException { switch (parser.getCurrentTokenType()) { case NULL: return null; case START_OBJECT: Map map = new HashMap<>(); while (parser.read() && parser.getCurrentTokenType() != FaunaTokenType.END_OBJECT) { if (parser.getCurrentTokenType() != FaunaTokenType.FIELD_NAME) { throw new CodecException( unexpectedTokenExceptionMessage( parser.getCurrentTokenType())); } String fieldName = parser.getValueAsString(); parser.read(); V value = valueCodec.decode(parser); map.put(fieldName, value); } @SuppressWarnings("unchecked") L typed = (L) map; return typed; default: throw new CodecException(this.unsupportedTypeDecodingMessage( parser.getCurrentTokenType().getFaunaType(), getSupportedTypes())); } } @Override public void encode(final UTF8FaunaGenerator gen, final L obj) throws CodecException { if (obj == null) { gen.writeNullValue(); return; } boolean shouldEscape = obj.keySet().stream().anyMatch(TAGS::contains); if (shouldEscape) { gen.writeStartEscapedObject(); } else { gen.writeStartObject(); } for (Map.Entry entry : obj.entrySet()) { gen.writeFieldName(entry.getKey().toString()); valueCodec.encode(gen, entry.getValue()); } if (shouldEscape) { gen.writeEndEscapedObject(); } else { gen.writeEndObject(); } } @Override public Class getCodecClass() { return valueCodec.getCodecClass(); } @Override public FaunaType[] getSupportedTypes() { return new FaunaType[] {FaunaType.Null, FaunaType.Object}; } } ``` ## File: src/main/java/com/fauna/codec/codecs/ModuleCodec.java ```java package com.fauna.codec.codecs; import com.fauna.codec.FaunaType; import com.fauna.codec.UTF8FaunaGenerator; import com.fauna.codec.UTF8FaunaParser; import com.fauna.exception.CodecException; import com.fauna.types.Module; /** * A codec for encoding and decoding {@link Module} in Fauna's tagged data format. */ public final class ModuleCodec extends BaseCodec { public static final ModuleCodec SINGLETON = new ModuleCodec(); @Override public Module decode(final UTF8FaunaParser parser) throws CodecException { switch (parser.getCurrentTokenType()) { case NULL: return null; case MODULE: return parser.getValueAsModule(); default: throw new CodecException(this.unsupportedTypeDecodingMessage( parser.getCurrentTokenType().getFaunaType(), getSupportedTypes())); } } @Override public void encode(final UTF8FaunaGenerator gen, final Module obj) throws CodecException { if (obj == null) { gen.writeNullValue(); } else { gen.writeModuleValue(obj); } } @Override public Class getCodecClass() { return Module.class; } @Override public FaunaType[] getSupportedTypes() { return new FaunaType[] {FaunaType.Module, FaunaType.Null}; } } ``` ## File: src/main/java/com/fauna/codec/codecs/NullableDocumentCodec.java ```java package com.fauna.codec.codecs; import com.fauna.codec.Codec; import com.fauna.codec.FaunaTokenType; import com.fauna.codec.FaunaType; import com.fauna.codec.UTF8FaunaGenerator; import com.fauna.codec.UTF8FaunaParser; import com.fauna.exception.CodecException; import com.fauna.exception.NullDocumentException; import com.fauna.types.NonNullDocument; import com.fauna.types.NullDocument; import com.fauna.types.NullableDocument; /** * Codec for encoding and decoding NullableDocument types. * * @param The type of the value in the document. * @param The type of the document (NullableDocument). */ public final class NullableDocumentCodec> extends BaseCodec { private final Codec valueCodec; /** * Constructs a {@code NullableDocumentCodec} with the specified {@code Codec}. * * @param valueCodec The codec to use for the value. */ public NullableDocumentCodec(final Codec valueCodec) { this.valueCodec = valueCodec; } @Override @SuppressWarnings("unchecked") public L decode(final UTF8FaunaParser parser) throws CodecException { if (parser.getCurrentTokenType() == FaunaTokenType.NULL) { return null; } try { E decoded = valueCodec.decode(parser); if (decoded instanceof NullDocument) { return (L) decoded; } return (L) new NonNullDocument<>(decoded); } catch (NullDocumentException e) { return (L) new NullDocument<>(e.getId(), e.getCollection(), e.getNullCause()); } } @Override public void encode(final UTF8FaunaGenerator gen, final L obj) throws CodecException { if (obj instanceof NonNullDocument) { @SuppressWarnings("unchecked") NonNullDocument nn = (NonNullDocument) obj; valueCodec.encode(gen, nn.get()); } else { throw new CodecException(unsupportedTypeMessage(obj.getClass())); } } @Override public Class getCodecClass() { return valueCodec.getCodecClass(); } @Override public FaunaType[] getSupportedTypes() { return valueCodec.getSupportedTypes(); } } ``` ## File: src/main/java/com/fauna/codec/codecs/OptionalCodec.java ```java package com.fauna.codec.codecs; import com.fauna.codec.Codec; import com.fauna.codec.FaunaTokenType; import com.fauna.codec.FaunaType; import com.fauna.codec.UTF8FaunaGenerator; import com.fauna.codec.UTF8FaunaParser; import com.fauna.exception.CodecException; import java.util.Optional; /** * Codec for encoding and decoding Optional types. * * @param The type of the value inside the Optional. * @param The type of the Optional (Optional). */ public final class OptionalCodec> extends BaseCodec { private final Codec valueCodec; /** * Constructs a {@code OptionalCodec} with the specified {@code Codec}. * * @param valueCodec The codec to use for the value. */ public OptionalCodec(final Codec valueCodec) { this.valueCodec = valueCodec; } @Override public L decode(final UTF8FaunaParser parser) throws CodecException { if (parser.getCurrentTokenType() == FaunaTokenType.NULL) { @SuppressWarnings("unchecked") L res = (L) Optional.empty(); return res; } @SuppressWarnings("unchecked") L res = (L) Optional.of(valueCodec.decode(parser)); return res; } @Override public void encode(final UTF8FaunaGenerator gen, final L obj) throws CodecException { if (obj == null || obj.isEmpty()) { gen.writeNullValue(); return; } valueCodec.encode(gen, obj.get()); } @Override public Class getCodecClass() { return Optional.class; } @Override public FaunaType[] getSupportedTypes() { return valueCodec.getSupportedTypes(); } } ``` ## File: src/main/java/com/fauna/codec/codecs/package-info.java ```java /** * The {@code com.fauna.codec.codecs} package provides codec implementations. */ package com.fauna.codec.codecs; ``` ## File: src/main/java/com/fauna/codec/codecs/PageCodec.java ```java package com.fauna.codec.codecs; import com.fauna.codec.Codec; import com.fauna.codec.FaunaTokenType; import com.fauna.codec.FaunaType; import com.fauna.codec.UTF8FaunaGenerator; import com.fauna.codec.UTF8FaunaParser; import com.fauna.exception.CodecException; import com.fauna.types.Page; import java.util.ArrayList; import java.util.List; /** * Codec for encoding and decoding Fauna's paginated results. * * @param The type of elements in the page. * @param The type of the Page (Page). */ public final class PageCodec> extends BaseCodec { private final Codec elementCodec; private final Codec> listCodec; /** * Constructs a {@code PageCodec} with the specified {@code Codec}. * * @param elementCodec The codec to use for elements of the page. */ public PageCodec(final Codec elementCodec) { this.elementCodec = elementCodec; this.listCodec = new ListCodec<>(elementCodec); } @Override public L decode(final UTF8FaunaParser parser) throws CodecException { switch (parser.getCurrentTokenType()) { case NULL: return null; case START_PAGE: return decodePage(parser, FaunaTokenType.END_PAGE); case START_OBJECT: // Handles a special case where calls with Set.paginate do not // return an object with a @set tag. return decodePage(parser, FaunaTokenType.END_OBJECT); case START_ARRAY: @SuppressWarnings("unchecked") L res = (L) new Page<>(listCodec.decode(parser), null); return res; case START_DOCUMENT: case START_REF: case STRING: case BYTES: case INT: case LONG: case DOUBLE: case DATE: case TIME: case TRUE: case FALSE: case MODULE: // In the event the user requests a Page but the query just returns T return wrapInPage(parser); default: throw new CodecException(this.unsupportedTypeDecodingMessage( parser.getCurrentTokenType().getFaunaType(), getSupportedTypes())); } } @Override public void encode(final UTF8FaunaGenerator gen, final L obj) throws CodecException { if (obj == null) { gen.writeNullValue(); } else { throw new CodecException( this.unsupportedTypeMessage(obj.getClass())); } } @Override public Class getCodecClass() { return elementCodec.getCodecClass(); } private L decodePage(final UTF8FaunaParser parser, final FaunaTokenType endToken) throws CodecException { parser.read(); if (parser.getCurrentTokenType() == FaunaTokenType.STRING) { return handleUnmaterialized(parser, endToken); } else { return handleMaterialized(parser, endToken); } } private L handleMaterialized(final UTF8FaunaParser parser, final FaunaTokenType endToken) { List data = null; String after = null; do { String fieldName = parser.getValueAsString(); parser.read(); switch (fieldName) { case "data": data = listCodec.decode(parser); break; case "after": after = parser.getValueAsString(); break; default: break; } } while (parser.read() && parser.getCurrentTokenType() != endToken); //noinspection unchecked return (L) new Page<>(data, after); } private L handleUnmaterialized(final UTF8FaunaParser parser, final FaunaTokenType endToken) { var after = parser.getValueAsString(); parser.read(); if (parser.getCurrentTokenType() != endToken) { throw new CodecException(unexpectedTokenExceptionMessage(parser.getCurrentTokenType())); } //noinspection unchecked return (L) new Page<>(new ArrayList<>(), after); } private L wrapInPage(final UTF8FaunaParser parser) throws CodecException { E elem = this.elementCodec.decode(parser); @SuppressWarnings("unchecked") L res = (L) new Page<>(List.of(elem), null); return res; } @Override public FaunaType[] getSupportedTypes() { return new FaunaType[] {FaunaType.Array, FaunaType.Boolean, FaunaType.Bytes, FaunaType.Date, FaunaType.Double, FaunaType.Document, FaunaType.Int, FaunaType.Long, FaunaType.Module, FaunaType.Null, FaunaType.Object, FaunaType.Ref, FaunaType.Set, FaunaType.String, FaunaType.Time}; } } ``` ## File: src/main/java/com/fauna/codec/codecs/QueryArrCodec.java ```java package com.fauna.codec.codecs; import com.fauna.codec.Codec; import com.fauna.codec.CodecProvider; import com.fauna.codec.FaunaType; import com.fauna.codec.UTF8FaunaGenerator; import com.fauna.codec.UTF8FaunaParser; import com.fauna.exception.CodecException; import com.fauna.query.builder.QueryArr; /** * Codec for encoding and decoding {@link QueryArr} objects. */ @SuppressWarnings("rawtypes") public final class QueryArrCodec extends BaseCodec { private final CodecProvider provider; /** * Creates a new instance of the {@link QueryArrCodec}. * * @param provider The codec provider used to retrieve codecs for object types. */ public QueryArrCodec(final CodecProvider provider) { this.provider = provider; } @Override public QueryArr decode(final UTF8FaunaParser parser) throws CodecException { throw new CodecException( "Decoding into a QueryFragment is not supported"); } @Override public void encode(final UTF8FaunaGenerator gen, final QueryArr obj) throws CodecException { if (obj == null) { gen.writeNullValue(); } else { gen.writeStartObject(); gen.writeFieldName("array"); Object unwrapped = obj.get(); Codec codec = provider.get(unwrapped.getClass()); //noinspection unchecked codec.encode(gen, unwrapped); gen.writeEndObject(); } } @Override public Class getCodecClass() { return QueryArr.class; } @Override public FaunaType[] getSupportedTypes() { return new FaunaType[0]; } } ``` ## File: src/main/java/com/fauna/codec/codecs/QueryCodec.java ```java package com.fauna.codec.codecs; import com.fauna.codec.Codec; import com.fauna.codec.CodecProvider; import com.fauna.codec.FaunaType; import com.fauna.codec.UTF8FaunaGenerator; import com.fauna.codec.UTF8FaunaParser; import com.fauna.exception.CodecException; import com.fauna.query.builder.Query; import com.fauna.query.builder.QueryFragment; /** * Codec for encoding and decoding {@link Query} objects. */ public final class QueryCodec extends BaseCodec { private final CodecProvider provider; /** * Creates a new instance of the {@link QueryCodec}. * * @param provider The codec provider used to retrieve codecs for object types. */ public QueryCodec(final CodecProvider provider) { this.provider = provider; } @Override public Query decode(final UTF8FaunaParser parser) throws CodecException { throw new CodecException( "Decoding into a QueryFragment is not supported"); } @SuppressWarnings("rawtypes") @Override public void encode(final UTF8FaunaGenerator gen, final Query obj) throws CodecException { gen.writeStartObject(); gen.writeFieldName("fql"); gen.writeStartArray(); for (QueryFragment f : obj.get()) { Codec codec = provider.get(f.getClass()); //noinspection unchecked codec.encode(gen, f); } gen.writeEndArray(); gen.writeEndObject(); } @Override public Class getCodecClass() { return Query.class; } @Override public FaunaType[] getSupportedTypes() { return new FaunaType[0]; } } ``` ## File: src/main/java/com/fauna/codec/codecs/QueryLiteralCodec.java ```java package com.fauna.codec.codecs; import com.fauna.codec.FaunaType; import com.fauna.codec.UTF8FaunaGenerator; import com.fauna.codec.UTF8FaunaParser; import com.fauna.exception.CodecException; import com.fauna.query.builder.QueryLiteral; /** * Codec for encoding and decoding {@link QueryLiteral} objects. */ public final class QueryLiteralCodec extends BaseCodec { /** * Creates a new instance of the {@link QueryLiteralCodec}. */ public QueryLiteralCodec() { // No additional setup required for the QueryLiteralCodec. } @Override public QueryLiteral decode(final UTF8FaunaParser parser) throws CodecException { throw new CodecException( "Decoding into a QueryFragment is not supported"); } @Override public void encode(final UTF8FaunaGenerator gen, final QueryLiteral obj) throws CodecException { if (obj == null) { gen.writeNullValue(); } else { gen.writeStringValue(obj.get()); } } @Override public Class getCodecClass() { return QueryLiteral.class; } @Override public FaunaType[] getSupportedTypes() { return new FaunaType[0]; } } ``` ## File: src/main/java/com/fauna/codec/codecs/QueryObjCodec.java ```java package com.fauna.codec.codecs; import com.fauna.codec.Codec; import com.fauna.codec.CodecProvider; import com.fauna.codec.FaunaType; import com.fauna.codec.UTF8FaunaGenerator; import com.fauna.codec.UTF8FaunaParser; import com.fauna.exception.CodecException; import com.fauna.query.builder.QueryObj; /** * Codec for encoding and decoding {@link QueryObj} objects. */ @SuppressWarnings("rawtypes") public final class QueryObjCodec extends BaseCodec { private final CodecProvider provider; /** * Creates a new instance of the {@link QueryObjCodec}. * * @param provider The codec provider to retrieve codecs for the underlying object types. */ public QueryObjCodec(final CodecProvider provider) { this.provider = provider; } @Override public QueryObj decode(final UTF8FaunaParser parser) throws CodecException { throw new CodecException( "Decoding into a QueryFragment is not supported"); } @Override public void encode(final UTF8FaunaGenerator gen, final QueryObj obj) throws CodecException { if (obj == null) { gen.writeNullValue(); } else { gen.writeStartObject(); gen.writeFieldName("object"); Object unwrapped = obj.get(); Codec codec = provider.get(unwrapped.getClass()); //noinspection unchecked codec.encode(gen, unwrapped); gen.writeEndObject(); } } @Override public Class getCodecClass() { return QueryObj.class; } @Override public FaunaType[] getSupportedTypes() { return new FaunaType[0]; } } ``` ## File: src/main/java/com/fauna/codec/codecs/QueryValCodec.java ```java package com.fauna.codec.codecs; import com.fauna.codec.Codec; import com.fauna.codec.CodecProvider; import com.fauna.codec.FaunaType; import com.fauna.codec.UTF8FaunaGenerator; import com.fauna.codec.UTF8FaunaParser; import com.fauna.exception.CodecException; import com.fauna.query.builder.QueryVal; /** * Codec for encoding and decoding {@link QueryVal} objects. */ @SuppressWarnings("rawtypes") public final class QueryValCodec extends BaseCodec { private final CodecProvider provider; /** * Creates a new instance of the {@link QueryValCodec}. * * @param provider The codec provider to retrieve codecs for the underlying object types. */ public QueryValCodec(final CodecProvider provider) { this.provider = provider; } @Override public QueryVal decode(final UTF8FaunaParser parser) throws CodecException { throw new CodecException( "Decoding into a QueryFragment is not supported"); } @Override public void encode(final UTF8FaunaGenerator gen, final QueryVal obj) throws CodecException { if (obj == null) { gen.writeNullValue(); } else { gen.writeStartObject(); gen.writeFieldName("value"); Object unwrapped = obj.get(); Codec codec = provider.get(unwrapped.getClass()); //noinspection unchecked codec.encode(gen, unwrapped); gen.writeEndObject(); } } @Override public Class getCodecClass() { return QueryVal.class; } @Override public FaunaType[] getSupportedTypes() { return new FaunaType[0]; } } ``` ## File: src/main/java/com/fauna/codec/codecs/ShortCodec.java ```java package com.fauna.codec.codecs; import com.fauna.codec.FaunaType; import com.fauna.codec.UTF8FaunaGenerator; import com.fauna.codec.UTF8FaunaParser; import com.fauna.exception.CodecException; /** * Codec for encoding and decoding {@link Short} values. */ public final class ShortCodec extends BaseCodec { public static final ShortCodec SINGLETON = new ShortCodec(); @Override public Short decode(final UTF8FaunaParser parser) throws CodecException { switch (parser.getCurrentTokenType()) { case NULL: return null; case INT: return parser.getValueAsShort(); default: throw new CodecException(this.unsupportedTypeDecodingMessage( parser.getCurrentTokenType().getFaunaType(), getSupportedTypes())); } } @Override public void encode(final UTF8FaunaGenerator gen, final Short obj) throws CodecException { if (obj == null) { gen.writeNullValue(); } else { gen.writeIntValue(obj); } } @Override public Class getCodecClass() { return Short.class; } @Override public FaunaType[] getSupportedTypes() { return new FaunaType[] {FaunaType.Int, FaunaType.Null}; } } ``` ## File: src/main/java/com/fauna/codec/codecs/StringCodec.java ```java package com.fauna.codec.codecs; import com.fauna.codec.FaunaType; import com.fauna.codec.UTF8FaunaGenerator; import com.fauna.codec.UTF8FaunaParser; import com.fauna.exception.CodecException; /** * Codec for encoding and decoding {@link String} values. */ public final class StringCodec extends BaseCodec { public static final StringCodec SINGLETON = new StringCodec(); @Override public String decode(final UTF8FaunaParser parser) throws CodecException { switch (parser.getCurrentTokenType()) { case NULL: return null; case STRING: return parser.getValueAsString(); case BYTES: return parser.getTaggedValueAsString(); default: throw new CodecException(this.unsupportedTypeDecodingMessage( parser.getCurrentTokenType().getFaunaType(), getSupportedTypes())); } } @Override public void encode(final UTF8FaunaGenerator gen, final String obj) throws CodecException { if (obj == null) { gen.writeNullValue(); } else { gen.writeStringValue(obj); } } @Override public Class getCodecClass() { return String.class; } @Override public FaunaType[] getSupportedTypes() { return new FaunaType[] {FaunaType.Bytes, FaunaType.Null, FaunaType.String}; } } ``` ## File: src/main/java/com/fauna/codec/Codec.java ```java package com.fauna.codec; import com.fauna.exception.CodecException; /** * Interface for codecs, which handle the serialization and deserialization of specific types. *

* Each codec is associated with a particular class and supports a set of Fauna data types. * * @param The type of object that this codec can encode and decode. */ public interface Codec { /** * Decodes an object from the provided {@link UTF8FaunaParser}. * * @param parser The parser to use for reading and decoding the data. * @return The decoded object of type {@code T}. * @throws CodecException If an error occurs during decoding. */ T decode(UTF8FaunaParser parser) throws CodecException; /** * Encodes the specified object using the provided {@link UTF8FaunaGenerator}. * * @param gen The generator to use for writing and encoding the data. * @param obj The object of type {@code T} to encode. * @throws CodecException If an error occurs during encoding. */ void encode(UTF8FaunaGenerator gen, T obj) throws CodecException; /** * Gets the class associated with this codec. * * @return The {@link Class} type that this codec handles. */ Class getCodecClass(); /** * Gets the set of supported Fauna data types for this codec. * * @return An array of {@link FaunaType} values representing the supported types. */ FaunaType[] getSupportedTypes(); } ``` ## File: src/main/java/com/fauna/codec/CodecProvider.java ```java package com.fauna.codec; import java.lang.reflect.Type; /** * Interface for providing codecs. Responsible for obtaining codecs for specific classes and types. */ public interface CodecProvider { /** * Retrieves a codec for the specified class type. * * @param clazz The class type for which to obtain a codec. * @param The type of the class. * @return A codec capable of serializing and deserializing instances of the specified class. */ Codec get(Class clazz); /** * Retrieves a codec for the specified class type with additional type arguments for generic classes. * * @param clazz The class type for which to obtain a codec. * @param typeArgs The generic type arguments, if applicable. * @param The type of the class. * @return A codec capable of serializing and deserializing instances of the specified class with the provided type arguments. */ Codec get(Class clazz, Type[] typeArgs); } ``` ## File: src/main/java/com/fauna/codec/CodecRegistry.java ```java package com.fauna.codec; /** * Interface defining a registry for codecs, which manage the serialization and deserialization of objects. *

* Provides methods for storing, retrieving, and checking for codecs by their unique keys. */ public interface CodecRegistry { /** * Retrieves the codec associated with the specified key. * * @param key The unique key representing the codec. * @param The type of the object handled by the codec. * @return The codec associated with the specified key, or {@code null} if not found. */ Codec get(CodecRegistryKey key); /** * Registers a codec with the specified key in the registry. * * @param key The unique key representing the codec. * @param codec The codec to register. * @param The type of the object handled by the codec. */ void put(CodecRegistryKey key, Codec codec); /** * Checks if a codec is registered under the specified key. * * @param key The unique key representing the codec. * @return {@code true} if a codec exists for the specified key; {@code false} otherwise. */ boolean contains(CodecRegistryKey key); } ``` ## File: src/main/java/com/fauna/codec/CodecRegistryKey.java ```java package com.fauna.codec; import java.lang.reflect.Type; import java.util.Arrays; import java.util.Objects; /** * Represents a unique key in the codec registry. */ public class CodecRegistryKey { private final Class base; private final Type[] typeArgs; /** * Constructs a new {@code CodecRegistryKey} for the specified class and type arguments. * * @param clazz The base class of the codec. * @param typeArgs The type arguments for generic types, if applicable. * @param The type of the base class. */ public CodecRegistryKey(final Class clazz, final Type[] typeArgs) { this.base = clazz; this.typeArgs = typeArgs; } /** * Creates a {@code CodecRegistryKey} for the specified class without any type arguments. * * @param clazz The base class of the codec. * @param The type of the base class. * @return A new {@code CodecRegistryKey} instance. */ public static CodecRegistryKey from(final Class clazz) { return new CodecRegistryKey(clazz, null); } /** * Creates a {@code CodecRegistryKey} for the specified class and type arguments. * * @param clazz The base class of the codec. * @param typeArgs The type arguments for generic types. * @param The type of the base class. * @return A new {@code CodecRegistryKey} instance. */ public static CodecRegistryKey from(final Class clazz, final Type[] typeArgs) { return new CodecRegistryKey(clazz, typeArgs); } /** * Compares this key with another object for equality based on the base class and type arguments. * * @param other The object to compare with this key. * @return {@code true} if the other object is a {@code CodecRegistryKey} with the same base class and type arguments; * {@code false} otherwise. */ @Override public boolean equals(final Object other) { if (other == this) { return true; } else if (other instanceof CodecRegistryKey) { CodecRegistryKey otherCRK = (CodecRegistryKey) other; return Objects.equals(base, otherCRK.base) && Arrays.equals(typeArgs, otherCRK.typeArgs); } else { return false; } } /** * Returns a hash code for this key, based on the base class and type arguments. * * @return The hash code for this {@code CodecRegistryKey}. */ @Override public final int hashCode() { return Objects.hash(base, Arrays.hashCode(typeArgs)); } } ``` ## File: src/main/java/com/fauna/codec/DefaultCodecProvider.java ```java package com.fauna.codec; import com.fauna.codec.codecs.BaseDocumentCodec; import com.fauna.codec.codecs.ClassCodec; import com.fauna.codec.codecs.DynamicCodec; import com.fauna.codec.codecs.EnumCodec; import com.fauna.codec.codecs.EventSourceCodec; import com.fauna.codec.codecs.ListCodec; import com.fauna.codec.codecs.MapCodec; import com.fauna.codec.codecs.NullableDocumentCodec; import com.fauna.codec.codecs.OptionalCodec; import com.fauna.codec.codecs.PageCodec; import com.fauna.codec.codecs.QueryArrCodec; import com.fauna.codec.codecs.QueryCodec; import com.fauna.codec.codecs.QueryLiteralCodec; import com.fauna.codec.codecs.QueryObjCodec; import com.fauna.codec.codecs.QueryValCodec; import com.fauna.event.EventSource; import com.fauna.query.builder.Query; import com.fauna.query.builder.QueryArr; import com.fauna.query.builder.QueryLiteral; import com.fauna.query.builder.QueryObj; import com.fauna.query.builder.QueryVal; import com.fauna.types.BaseDocument; import com.fauna.types.Document; import com.fauna.types.NamedDocument; import com.fauna.types.NullableDocument; import com.fauna.types.Page; import java.lang.reflect.Type; import java.util.List; import java.util.Map; import java.util.Optional; /** * Provides codecs for serialization and deserialization of various data types in Fauna. *

* This provider supports codecs for primitive types, collections, optional values, documents, enums, and more. *

*/ public final class DefaultCodecProvider implements CodecProvider { private final CodecRegistry registry; /** * Singleton instance of the {@code DefaultCodecProvider} for global access. */ public static final CodecProvider SINGLETON = new DefaultCodecProvider(DefaultCodecRegistry.SINGLETON); /** * Initializes a new instance of {@code DefaultCodecProvider} with a specified registry. * * @param registry The codec registry to store generated codecs. */ public DefaultCodecProvider(final CodecRegistry registry) { registry.put(CodecRegistryKey.from(Object.class), new DynamicCodec(this)); registry.put(CodecRegistryKey.from(Query.class), new QueryCodec(this)); registry.put(CodecRegistryKey.from(QueryObj.class), new QueryObjCodec(this)); registry.put(CodecRegistryKey.from(QueryArr.class), new QueryArrCodec(this)); registry.put(CodecRegistryKey.from(QueryVal.class), new QueryValCodec(this)); registry.put(CodecRegistryKey.from(QueryLiteral.class), new QueryLiteralCodec()); registry.put(CodecRegistryKey.from(EventSource.class), new EventSourceCodec()); var bdc = new BaseDocumentCodec(this); registry.put(CodecRegistryKey.from(BaseDocument.class), bdc); registry.put(CodecRegistryKey.from(Document.class), bdc); registry.put(CodecRegistryKey.from(NamedDocument.class), bdc); this.registry = registry; } /** * Retrieves the codec for the specified class type. * * @param clazz The class for which a codec is requested. * @param The data type to be encoded or decoded. * @return The {@link Codec} associated with the class. */ public Codec get(final Class clazz) { return get(clazz, null); } /** * Retrieves the codec for the specified class type and type arguments. * * @param clazz The class for which a codec is requested. * @param typeArgs The type arguments for generic classes. * @param The data type to be encoded or decoded. * @return The {@link Codec} associated with the class and type arguments. */ @Override public Codec get(final Class clazz, final Type[] typeArgs) { CodecRegistryKey key = CodecRegistryKey.from(clazz, typeArgs); if (!registry.contains(key)) { var codec = generate(clazz, typeArgs); registry.put(key, codec); } return registry.get(key); } /** * Generates a codec for the specified class type and type arguments if not already available. * * @param clazz The class for which a codec needs to be generated. * @param typeArgs The type arguments for generic classes. * @param The data type to be encoded or decoded. * @param The element type for collection codecs. * @return The generated {@link Codec} for the class and type arguments. */ @SuppressWarnings({"unchecked"}) private Codec generate(final Class clazz, final Type[] typeArgs) { if (Map.class.isAssignableFrom(clazz)) { var ta = typeArgs == null || typeArgs.length <= 1 ? Object.class : typeArgs[1]; Codec valueCodec = this.get((Class) ta, null); return (Codec) new MapCodec>((Codec) valueCodec); } var ta = typeArgs == null || typeArgs.length == 0 ? Object.class : typeArgs[0]; if (List.class.isAssignableFrom(clazz)) { Codec elemCodec = this.get((Class) ta, null); return (Codec) new ListCodec>((Codec) elemCodec); } if (clazz == Optional.class) { Codec valueCodec = this.get((Class) ta, null); return (Codec) new OptionalCodec>((Codec) valueCodec); } if (clazz == Page.class) { Codec valueCodec = this.get((Class) ta, null); return (Codec) new PageCodec>((Codec) valueCodec); } if (clazz == NullableDocument.class) { Codec valueCodec = this.get((Class) ta, null); return (Codec) new NullableDocumentCodec>((Codec) valueCodec); } if (clazz.isEnum()) { return new EnumCodec<>(clazz); } return new ClassCodec<>(clazz, this); } } ``` ## File: src/main/java/com/fauna/codec/DefaultCodecRegistry.java ```java package com.fauna.codec; import com.fauna.codec.codecs.BaseRefCodec; import com.fauna.codec.codecs.BoolCodec; import com.fauna.codec.codecs.ByteArrayCodec; import com.fauna.codec.codecs.ByteCodec; import com.fauna.codec.codecs.CharCodec; import com.fauna.codec.codecs.DoubleCodec; import com.fauna.codec.codecs.FloatCodec; import com.fauna.codec.codecs.InstantCodec; import com.fauna.codec.codecs.IntCodec; import com.fauna.codec.codecs.LocalDateCodec; import com.fauna.codec.codecs.LongCodec; import com.fauna.codec.codecs.ModuleCodec; import com.fauna.codec.codecs.ShortCodec; import com.fauna.codec.codecs.StringCodec; import com.fauna.types.BaseRef; import com.fauna.types.DocumentRef; import com.fauna.types.Module; import com.fauna.types.NamedDocumentRef; import java.time.Instant; import java.time.LocalDate; import java.util.concurrent.ConcurrentHashMap; /** * The default codec registry for Fauna serialization and deserialization. *

* This registry provides pre-defined codecs for common data types, enabling * serialization to and deserialization from FQL. *

*/ public final class DefaultCodecRegistry implements CodecRegistry { /** * Singleton instance of the {@code DefaultCodecRegistry} for global access. */ public static final CodecRegistry SINGLETON = new DefaultCodecRegistry(); private final ConcurrentHashMap> codecs; /** * Initializes a new instance of {@code DefaultCodecRegistry} with predefined codecs * for commonly used data types. */ public DefaultCodecRegistry() { codecs = new ConcurrentHashMap<>(); codecs.put(CodecRegistryKey.from(String.class), StringCodec.SINGLETON); codecs.put(CodecRegistryKey.from(byte[].class), ByteArrayCodec.SINGLETON); codecs.put(CodecRegistryKey.from(boolean.class), BoolCodec.SINGLETON); codecs.put(CodecRegistryKey.from(Boolean.class), BoolCodec.SINGLETON); codecs.put(CodecRegistryKey.from(char.class), CharCodec.SINGLETON); codecs.put(CodecRegistryKey.from(Character.class), CharCodec.SINGLETON); codecs.put(CodecRegistryKey.from(byte.class), ByteCodec.SINGLETON); codecs.put(CodecRegistryKey.from(Byte.class), ByteCodec.SINGLETON); codecs.put(CodecRegistryKey.from(short.class), ShortCodec.SINGLETON); codecs.put(CodecRegistryKey.from(Short.class), ShortCodec.SINGLETON); codecs.put(CodecRegistryKey.from(Integer.class), IntCodec.SINGLETON); codecs.put(CodecRegistryKey.from(int.class), IntCodec.SINGLETON); codecs.put(CodecRegistryKey.from(Long.class), LongCodec.SINGLETON); codecs.put(CodecRegistryKey.from(long.class), LongCodec.SINGLETON); codecs.put(CodecRegistryKey.from(Float.class), FloatCodec.SINGLETON); codecs.put(CodecRegistryKey.from(float.class), FloatCodec.SINGLETON); codecs.put(CodecRegistryKey.from(Double.class), DoubleCodec.SINGLETON); codecs.put(CodecRegistryKey.from(double.class), DoubleCodec.SINGLETON); codecs.put(CodecRegistryKey.from(Instant.class), InstantCodec.SINGLETON); codecs.put(CodecRegistryKey.from(LocalDate.class), LocalDateCodec.SINGLETON); codecs.put(CodecRegistryKey.from(Module.class), ModuleCodec.SINGLETON); codecs.put(CodecRegistryKey.from(BaseRef.class), BaseRefCodec.SINGLETON); codecs.put(CodecRegistryKey.from(DocumentRef.class), BaseRefCodec.SINGLETON); codecs.put(CodecRegistryKey.from(NamedDocumentRef.class), BaseRefCodec.SINGLETON); } /** * Retrieves the codec associated with the specified key, if it exists. * * @param key The {@link CodecRegistryKey} representing the data type. * @param The data type to be encoded or decoded. * @return The {@link Codec} associated with the key, or {@code null} if not found. */ @Override public Codec get(final CodecRegistryKey key) { @SuppressWarnings("unchecked") var codec = (Codec) codecs.get(key); return codec; } /** * Registers a new codec for the specified key in the registry. * * @param key The {@link CodecRegistryKey} representing the data type. * @param codec The {@link Codec} to associate with the key. * @param The data type to be encoded or decoded. */ @Override public void put(final CodecRegistryKey key, final Codec codec) { codecs.put(key, codec); } /** * Checks if the registry contains a codec for the specified key. * * @param key The {@link CodecRegistryKey} representing the data type. * @return {@code true} if a codec for the key is registered; otherwise, {@code false}. */ @Override public boolean contains(final CodecRegistryKey key) { return codecs.containsKey(key); } } ``` ## File: src/main/java/com/fauna/codec/FaunaTokenType.java ```java package com.fauna.codec; import com.fauna.exception.ClientResponseException; /** * Enumeration representing token types for Fauna serialization. *

* The {@code FaunaTokenType} enum defines various tokens that are used to * identify different elements and data structures within FQL serialization * and deserialization processes. *

*/ public enum FaunaTokenType { NONE, /** * A structural token that starts an object. * Wire representation: { */ START_OBJECT, /** * A structural token that ends an object. * Wire representation: } */ END_OBJECT, /** * A structural token that starts an array. * Wire representation: {@code [} */ START_ARRAY, /** * A structural token that ends an array. * Wire representation: {@code ]} */ END_ARRAY, /** * A structural token that starts a page. * Wire representation: { "@page": */ START_PAGE, /** * A structural token that ends a page. * Wire representation: } */ END_PAGE, /** * A structural token that starts a ref. * Wire representation: { "@ref": */ START_REF, /** * A structural token that ends a ref. * Wire representation: } */ END_REF, /** * A structural token that starts a document. * Wire representation: { "@doc": */ START_DOCUMENT, /** * A structural token that ends a document. * Wire representation: } */ END_DOCUMENT, /** * A value token that represents a field of an Fauna object, document, or other structure. */ FIELD_NAME, /** * A value token that represents a Fauna string. */ STRING, /** * A value token that represents a Fauna base64-encoded byte sequence. */ BYTES, /** * A value token that represents a Fauna integer. */ INT, /** * A value token that represents a Fauna long. */ LONG, /** * A value token that represents a Fauna double. */ DOUBLE, /** * A value token that represents a Fauna date. */ DATE, /** * A value token that represents a Fauna time. */ TIME, /** * A value token that represents the Fauna boolean {@code true}. */ TRUE, /** * A value token that represents the Fauna boolean {@code false}. */ FALSE, /** * A value token that represents null. */ NULL, /** * A value token that represents a Fauna Event Source. */ STREAM, /** * A value token that represents a Fauna symbolic object, such as a user collection. */ MODULE; /** * Returns the corresponding end token for the current start token. *

* For tokens representing the beginning of a structure (e.g., {@code START_OBJECT}), * this method returns the matching token for the end of that structure * (e.g., {@code END_OBJECT}). *

* * @return The end token associated with the current start token. * @throws ClientResponseException If the current token has no corresponding end token. */ public FaunaTokenType getEndToken() { switch (this) { case START_DOCUMENT: return END_DOCUMENT; case START_OBJECT: return END_OBJECT; case START_ARRAY: return END_ARRAY; case START_PAGE: return END_PAGE; case START_REF: return END_REF; default: throw new ClientResponseException("No end token for " + this.name()); } } /** * Returns the {@link FaunaType} that corresponds to the current {@code FaunaTokenType}. *

* This method maps each token type in {@code FaunaTokenType} to a specific {@code FaunaType}, * which represents the underlying data type in Fauna's type system. *

* * @return The {@link FaunaType} associated with the current token type. * @throws IllegalStateException If the token type does not have an associated {@code FaunaType}. */ public FaunaType getFaunaType() { switch (this) { case START_OBJECT: case END_OBJECT: return FaunaType.Object; case START_ARRAY: case END_ARRAY: return FaunaType.Array; case START_PAGE: case END_PAGE: return FaunaType.Set; case START_REF: case END_REF: return FaunaType.Ref; case START_DOCUMENT: case END_DOCUMENT: return FaunaType.Document; case STRING: return FaunaType.String; case BYTES: return FaunaType.Bytes; case INT: return FaunaType.Int; case LONG: return FaunaType.Long; case DOUBLE: return FaunaType.Double; case DATE: return FaunaType.Date; case TIME: return FaunaType.Time; case TRUE: case FALSE: return FaunaType.Boolean; case NULL: return FaunaType.Null; case STREAM: return FaunaType.Stream; case MODULE: return FaunaType.Module; default: throw new IllegalStateException("No associated FaunaType for FaunaTokenType: " + this); } } } ``` ## File: src/main/java/com/fauna/codec/FaunaType.java ```java package com.fauna.codec; /** * Enum representing various FQL data types used by Fauna for data storage and retrieval. * These types provide structured representations for * encoding and decoding data in FQL queries and responses. */ public enum FaunaType { /** * Represents an integer value in FQL. */ Int, /** * Represents a long integer value in FQL. */ Long, /** * Represents a double-precision floating-point number in FQL. */ Double, /** * Represents a UTF-8 encoded string in FQL. */ String, /** * Represents a date without time in FQL. * Dates are in ISO 8601 format (YYYY-MM-DD). */ Date, /** * Represents an exact timestamp or time value in FQL. * Timestamps are in ISO 8601 format. */ Time, /** * Represents a boolean value in FQL. */ Boolean, /** * Represents an object in FQL. */ Object, /** * Represents a reference to a document. */ Ref, /** * Represents a complete document in FQL. */ Document, /** * Represents an array (or list) of values in FQL. * Arrays are ordered collections of elements and can contain multiple data types. */ Array, /** * Represents binary data encoded in Base64 within FQL. * Used for storing raw bytes of data. */ Bytes, /** * Represents a null value in FQL, denoting the absence of a value. */ Null, /** * Represents an event source in FQL. * Event sources are used to track events as event feeds or event streams. */ Stream, /** * Represents a module in FQL, which serves as a symbolic object * with associated methods */ Module, /** * Represents a pageable Set in FQL. */ Set } ``` ## File: src/main/java/com/fauna/codec/Generic.java ```java package com.fauna.codec; /** * A helper class for providing static access to parameterized generic types, aiding in * deserialization by circumventing type erasure. */ public final class Generic { /** * Private constructor to prevent instantiation. */ private Generic() { } /** * Creates a {@link ListOf} instance for the specified element type. * * @param elementClass The class of the elements contained in the list. * @param The type of elements in the list. * @return A {@link ListOf} instance with the specified element type. */ public static ListOf listOf(final Class elementClass) { return new ListOf<>(elementClass); } /** * Creates a {@link MapOf} instance for a map with {@link String} keys and the specified value type. * * @param valueClass The class of the map's values. * @param The type of keys in the map (constrained to {@link String}). * @param The type of values in the map. * @return A {@link MapOf} instance with {@link String} keys and the specified value type. */ public static MapOf mapOf(final Class valueClass) { return new MapOf<>(valueClass); } /** * Creates a {@link PageOf} instance for the specified element type. * * @param valueClass The class of the elements contained in the page. * @param The type of elements in the page. * @return A {@link PageOf} instance with the specified element type. */ public static PageOf pageOf(final Class valueClass) { return new PageOf<>(valueClass); } /** * Creates an {@link OptionalOf} instance for the specified element type. * * @param valueClass The class of the elements contained in the optional. * @param The type of the element in the optional. * @return An {@link OptionalOf} instance with the specified element type. */ public static OptionalOf optionalOf(final Class valueClass) { return new OptionalOf<>(valueClass); } /** * Creates a {@link NullableDocumentOf} instance for the specified element type. * * @param valueClass The class of the elements contained in the nullable document. * @param The type of the element in the nullable document. * @return A {@link NullableDocumentOf} instance with the specified element type. */ public static NullableDocumentOf nullableDocumentOf(final Class valueClass) { return new NullableDocumentOf<>(valueClass); } } ``` ## File: src/main/java/com/fauna/codec/ListOf.java ```java package com.fauna.codec; import java.lang.reflect.Type; import java.util.List; /** * Represents a {@link List} with a specified element type, allowing for retention of the * generic type {@code E} during deserialization by circumventing type erasure. * * @param The type of elements in the list. */ public final class ListOf extends ParameterizedOf> { /** * Constructs a {@code ListOf} instance for the specified element type. * * @param elementClass The class of the elements contained in the list. */ public ListOf(final Class elementClass) { super(List.class, new Type[]{elementClass}); } } ``` ## File: src/main/java/com/fauna/codec/MapOf.java ```java package com.fauna.codec; import java.lang.reflect.Type; import java.util.Map; /** * Represents a {@link Map} with {@link String} keys and a specified value type, allowing for * retention of the generic type {@code V} during deserialization by circumventing type erasure. * * @param The type of keys maintained by the map (constrained to {@link String}). * @param The type of mapped values. */ public final class MapOf extends ParameterizedOf> { /** * Constructs a {@code MapOf} instance for a map with {@link String} keys and the specified value type. * * @param valueClass The class of the map's values. */ public MapOf(final Class valueClass) { super(Map.class, new Type[]{String.class, valueClass}); } } ``` ## File: src/main/java/com/fauna/codec/NullableDocumentOf.java ```java package com.fauna.codec; import com.fauna.types.NullableDocument; import java.lang.reflect.Type; /** * Represents a {@link NullableDocument} with a specified value type, allowing for retention * of the generic type {@code E} during deserialization by circumventing type erasure. * * @param The type of the value contained in the {@code NullableDocument}. */ public final class NullableDocumentOf extends ParameterizedOf> { /** * Constructs a {@code NullableDocumentOf} instance for the specified value type. * * @param valueClass The class of the value contained in the {@code NullableDocument}. */ public NullableDocumentOf(final Class valueClass) { super(NullableDocument.class, new Type[]{valueClass}); } } ``` ## File: src/main/java/com/fauna/codec/OptionalOf.java ```java package com.fauna.codec; import java.lang.reflect.Type; import java.util.Optional; /** * Represents an {@link Optional} with a specified element type, allowing for retention of the * generic type {@code V} during deserialization by circumventing type erasure. * * @param The element type within the optional. */ public final class OptionalOf extends ParameterizedOf> { /** * Constructs an {@code OptionalOf} instance for the specified element type. * * @param valueClass The class of the elements contained in the optional. */ public OptionalOf(final Class valueClass) { super(Optional.class, new Type[]{valueClass}); } } ``` ## File: src/main/java/com/fauna/codec/package-info.java ```java /** * The {@code com.fauna.codec} package provides classes and interfaces for encoding and decoding data types used in * Fauna. *

* Key classes and interfaces: *

    *
  • {@link com.fauna.codec.Codec} - An interface for implementing codecs that define encoding and decoding * behavior.
  • *
  • {@link com.fauna.codec.CodecProvider} - Provides codecs, allowing retrieval based on class and type * arguments.
  • *
  • {@link com.fauna.codec.CodecRegistry} - A registry for managing codecs.
  • *
  • {@linkcom.fauna.codec.UTF8FaunaParser} - A custom parser for reading Fauna's tagged format.
  • *
  • {@link com.fauna.codec.UTF8FaunaGenerator} - A generator for writing data in Fauna's tagged format.
  • *
  • {@link com.fauna.codec.DefaultCodecProvider} - A standard codec provider implementation.
  • *
  • {@link com.fauna.codec.Generic} - A utility class for generating codecs for generic types.
  • *
*/ package com.fauna.codec; ``` ## File: src/main/java/com/fauna/codec/PageOf.java ```java package com.fauna.codec; import com.fauna.types.Page; import java.lang.reflect.Type; /** * Represents a {@link Page} with a specified element type, allowing for retention of the * generic type {@code V} during deserialization by circumventing type erasure. * * @param The element type within the page. */ public final class PageOf extends ParameterizedOf> { /** * Constructs a {@code PageOf} instance for the specified element type. * * @param valueClass The class of the elements contained in the page. */ public PageOf(final Class valueClass) { super(Page.class, new Type[]{valueClass}); } } ``` ## File: src/main/java/com/fauna/codec/ParameterizedOf.java ```java package com.fauna.codec; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; /** * A utility class that implements {@link ParameterizedType} to represent a type with specified * type arguments at runtime. * * @param The type parameter of the parameterized type. */ public class ParameterizedOf implements ParameterizedType { private final Type rawType; private final Type[] typeArguments; /** * Constructs a new {@code ParameterizedOf} instance. * * @param rawType The raw type (e.g., {@code List.class} for {@code List}). * @param typeArguments The type arguments (e.g., {@code String.class} for {@code List}). */ public ParameterizedOf(final Type rawType, final Type[] typeArguments) { this.rawType = rawType; this.typeArguments = typeArguments; } /** * Returns the type arguments for this parameterized type. * * @return An array of {@link Type} objects representing the actual type arguments. */ @Override public Type[] getActualTypeArguments() { return typeArguments; } /** * Returns the raw type of this parameterized type. * * @return The raw {@link Type} representing the parameterized type. */ @Override public Type getRawType() { return rawType; } /** * Returns the owner type of this parameterized type. * * @return {@code null} as this implementation does not support owner types. */ @Override public Type getOwnerType() { return null; } } ``` ## File: src/main/java/com/fauna/codec/UTF8FaunaGenerator.java ```java package com.fauna.codec; import com.fasterxml.jackson.core.JsonFactory; import com.fasterxml.jackson.core.JsonGenerator; import com.fauna.exception.CodecException; import com.fauna.types.Module; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.time.Instant; import java.time.LocalDate; import java.time.ZoneOffset; import java.time.format.DateTimeFormatter; import java.util.Base64; import static java.nio.charset.StandardCharsets.UTF_8; /** * A generator for encoding JSON with Fauna-specific tagged values and other data types. */ public final class UTF8FaunaGenerator implements AutoCloseable { private final JsonGenerator jsonGenerator; private final ByteArrayOutputStream output; /** * Initializes a new instance of the {@code UTF8FaunaGenerator} class. * * @throws IOException If an error occurs during creation of the JSON generator. */ public UTF8FaunaGenerator() throws IOException { JsonFactory factory = new JsonFactory(); this.output = new ByteArrayOutputStream(); this.jsonGenerator = factory.createGenerator(this.output); } /** * Creates a new {@code UTF8FaunaGenerator} instance. * * @return A new instance of the {@code UTF8FaunaGenerator}. * @throws CodecException If an I/O error occurs. */ public static UTF8FaunaGenerator create() throws CodecException { try { return new UTF8FaunaGenerator(); } catch (IOException exc) { throw CodecException.encodingIOException(exc); } } /** * Flushes the written data to the underlying buffer or stream. * * @throws CodecException If an I/O error occurs. */ public void flush() throws CodecException { try { jsonGenerator.flush(); } catch (IOException exc) { throw CodecException.encodingIOException(exc); } } /** * Serializes the current state of the generator's buffer as a UTF-8 encoded string. * * @return A string representation of the serialized output. * @throws CodecException If an I/O error occurs. */ public String serialize() throws CodecException { this.flush(); return this.output.toString(UTF_8); } /** * Writes the beginning of an object. * * @throws CodecException If an I/O error occurs. */ public void writeStartObject() throws CodecException { try { jsonGenerator.writeStartObject(); } catch (IOException exc) { throw CodecException.encodingIOException(exc); } } /** * Writes the end of an object. * * @throws CodecException If an I/O error occurs. */ public void writeEndObject() throws CodecException { try { jsonGenerator.writeEndObject(); } catch (IOException exc) { throw CodecException.encodingIOException(exc); } } /** * Writes the beginning of a specially tagged object. * * @throws CodecException If an I/O error occurs. */ public void writeStartEscapedObject() throws CodecException { writeStartObject(); writeFieldName("@object"); writeStartObject(); } /** * Writes the end of a specially tagged object. * * @throws CodecException If an I/O error occurs. */ public void writeEndEscapedObject() throws CodecException { writeEndObject(); writeEndObject(); } /** * Writes the beginning of an array. * * @throws CodecException If an I/O error occurs. */ public void writeStartArray() throws CodecException { try { jsonGenerator.writeStartArray(); } catch (IOException e) { throw CodecException.encodingIOException(e); } } /** * Writes the end of an array. * * @throws CodecException If an I/O error occurs. */ public void writeEndArray() throws CodecException { try { jsonGenerator.writeEndArray(); } catch (IOException e) { throw CodecException.encodingIOException(e); } } /** * Writes the beginning of a reference object. * * @throws CodecException If an I/O error occurs. */ public void writeStartRef() throws CodecException { writeStartObject(); writeFieldName("@ref"); writeStartObject(); } /** * Writes the end of a reference object. * * @throws CodecException If an error occurs. */ public void writeEndRef() throws CodecException { writeEndObject(); writeEndObject(); } /** * Writes a double value with a specific field name. * * @param fieldName The name of the field. * @param value The double value to write. * @throws CodecException If an I/O error occurs. */ public void writeDouble(final String fieldName, final double value) throws CodecException { writeFieldName(fieldName); writeDoubleValue(value); } /** * Writes an integer value with a specific field name. * * @param fieldName The name of the field. * @param value The integer value to write. * @throws CodecException If an I/O error occurs. */ public void writeInt(final String fieldName, final int value) throws CodecException { writeFieldName(fieldName); writeIntValue(value); } /** * Writes a long integer value with a specific field name. * * @param fieldName The name of the field. * @param value The long integer value to write. * @throws CodecException If an I/O error occurs. */ public void writeLong(final String fieldName, final long value) throws CodecException { writeFieldName(fieldName); writeLongValue(value); } /** * Writes a string value with a specific field name. * * @param fieldName The name of the field. * @param value The string value to write. * @throws CodecException If an I/O error occurs. */ public void writeString(final String fieldName, final String value) throws CodecException { writeFieldName(fieldName); writeStringValue(value); } /** * Writes a date value with a specific field name. * * @param fieldName The name of the field. * @param value The date value to write. * @throws CodecException If an I/O error occurs. */ public void writeDate(final String fieldName, final LocalDate value) throws CodecException { writeFieldName(fieldName); writeDateValue(value); } /** * Writes a time value with a specific field name. * * @param fieldName The name of the field. * @param value The time value to write. * @throws CodecException If an I/O error occurs. */ public void writeTime(final String fieldName, final Instant value) throws CodecException { writeFieldName(fieldName); writeTimeValue(value); } /** * Writes a boolean value with a specific field name. * * @param fieldName The name of the field. * @param value The boolean value to write. * @throws CodecException If an I/O error occurs. */ public void writeBoolean(final String fieldName, final boolean value) throws CodecException { writeFieldName(fieldName); writeBooleanValue(value); } /** * Writes a null value with a specific field name. * * @param fieldName The name of the field. * @throws CodecException If an I/O error occurs. */ public void writeNull(final String fieldName) throws CodecException { writeFieldName(fieldName); writeNullValue(); } /** * Writes a module value with a specific field name. * * @param fieldName The name of the field. * @param value The Module value to write. * @throws CodecException If an I/O error occurs. */ public void writeModule(final String fieldName, final Module value) throws CodecException { writeFieldName(fieldName); writeModuleValue(value); } /** * Writes a field name for the next value. * * @param value The name of the field. * @throws CodecException If an I/O error occurs. */ public void writeFieldName(final String value) throws CodecException { try { jsonGenerator.writeFieldName(value); } catch (IOException e) { throw CodecException.encodingIOException(e); } } /** * Writes a tagged value in an object. * * @param tag The tag to use for the value. * @param value The value associated with the tag. * @throws CodecException If an I/O error occurs. */ public void writeTaggedValue(final String tag, final String value) throws CodecException { writeStartObject(); writeString(tag, value); writeEndObject(); } /** * Writes a double value as a tagged element. * * @param value The double value to write. * @throws CodecException If an I/O error occurs. */ public void writeDoubleValue(final double value) throws CodecException { writeTaggedValue("@double", Double.toString(value)); } /** * Writes a float value as a tagged element (@double). * * @param value The float value to write as a double. * @throws CodecException If an I/O error occurs. */ public void writeDoubleValue(final float value) throws CodecException { writeTaggedValue("@double", Float.toString(value)); } /** * Writes an integer value as a tagged element. * * @param value The integer value to write. * @throws CodecException If an I/O error occurs. */ public void writeIntValue(final int value) throws CodecException { writeTaggedValue("@int", Integer.toString(value)); } /** * Writes a long integer value as a tagged element. * * @param value The long integer value to write. * @throws CodecException If an I/O error occurs. */ public void writeLongValue(final long value) throws CodecException { writeTaggedValue("@long", Long.toString(value)); } /** * Writes a string value as a tagged element. * * @param value The string value to write. * @throws CodecException If an I/O error occurs. */ public void writeStringValue(final String value) throws CodecException { try { jsonGenerator.writeString(value); } catch (IOException exc) { throw CodecException.encodingIOException(exc); } } /** * Writes a date value as a tagged element. * * @param value The date value to write. * @throws CodecException If an I/O error occurs. */ public void writeDateValue(final LocalDate value) throws CodecException { String str = value.format(DateTimeFormatter.ofPattern("yyyy-MM-dd")); writeTaggedValue("@date", str); } /** * Writes a time value as a tagged element. * * @param value The time value to write. * @throws CodecException If an I/O error occurs. */ public void writeTimeValue(final Instant value) throws CodecException { Instant instant = value.atZone(ZoneOffset.UTC).toInstant(); String formattedTime = instant.toString(); writeTaggedValue("@time", formattedTime); } /** * Writes a boolean value to the stream. * * @param value The boolean value to write. * @throws CodecException If an I/O error occurs. */ public void writeBooleanValue(final boolean value) throws CodecException { try { jsonGenerator.writeBoolean(value); } catch (IOException exc) { throw CodecException.encodingIOException(exc); } } /** * Writes a character value as an integer. * * @param value The character value to write. * @throws CodecException If an I/O error occurs. */ public void writeCharValue(final Character value) throws CodecException { writeIntValue(value); } /** * Writes a null value to the stream. * * @throws CodecException If an I/O error occurs. */ public void writeNullValue() throws CodecException { try { jsonGenerator.writeNull(); } catch (IOException exc) { throw CodecException.encodingIOException(exc); } } /** * Writes a module value as a tagged element. * * @param value The module value to write. * @throws CodecException If an I/O error occurs. */ public void writeModuleValue(final Module value) throws CodecException { writeTaggedValue("@mod", value.getName()); } /** * Writes a byte array encoded as a base64 string as a tagged element. * * @param value The byte array to write. * @throws CodecException If an I/O error occurs. */ public void writeBytesValue(final byte[] value) throws CodecException { writeTaggedValue("@bytes", Base64.getEncoder().encodeToString(value)); } /** * Closes the generator and the underlying resources. * * @throws CodecException If an I/O error occurs during closing. */ @Override public void close() throws CodecException { try { jsonGenerator.close(); } catch (IOException exc) { throw CodecException.encodingIOException(exc); } finally { try { output.close(); } catch (IOException e) { //noinspection ThrowFromFinallyBlock throw CodecException.encodingIOException(e); } } } } ``` ## File: src/main/java/com/fauna/codec/UTF8FaunaParser.java ```java package com.fauna.codec; import com.fasterxml.jackson.core.JsonFactory; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.JsonToken; import com.fauna.exception.CodecException; import com.fauna.types.Module; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.nio.charset.StandardCharsets; import java.time.Instant; import java.time.LocalDate; import java.time.format.DateTimeParseException; import java.util.Arrays; import java.util.Base64; import java.util.HashSet; import java.util.Objects; import java.util.Set; import java.util.Stack; /** * Represents a reader that provides fast, non-cached, forward-only access to serialized data. */ public final class UTF8FaunaParser { private static final String INT_TAG = "@int"; private static final String LONG_TAG = "@long"; private static final String DOUBLE_TAG = "@double"; private static final String TIME_TAG = "@time"; private static final String DATE_TAG = "@date"; private static final String REF_TAG = "@ref"; private static final String DOC_TAG = "@doc"; private static final String MOD_TAG = "@mod"; private static final String SET_TAG = "@set"; private static final String STREAM_TAG = "@stream"; private static final String OBJECT_TAG = "@object"; private static final String BYTES_TAG = "@bytes"; private final JsonParser jsonParser; private final Stack tokenStack = new Stack<>(); private final Set closers = new HashSet<>(Arrays.asList( FaunaTokenType.END_OBJECT, FaunaTokenType.END_PAGE, FaunaTokenType.END_DOCUMENT, FaunaTokenType.END_REF, FaunaTokenType.END_ARRAY )); private FaunaTokenType currentFaunaTokenType = FaunaTokenType.NONE; private FaunaTokenType bufferedFaunaTokenType; private Object bufferedTokenValue; private String taggedTokenValue; private enum InternalTokenType { START_ESCAPED_OBJECT, START_PAGE_UNMATERIALIZED } /** * Constructs a {@code UTF8FaunaParser} instance with the given JSON parser. * * @param jsonParser The {@link JsonParser} used to read the JSON data. */ public UTF8FaunaParser(final JsonParser jsonParser) { this.jsonParser = jsonParser; } /** * Creates a {@code UTF8FaunaParser} from an {@link InputStream}. * * @param body The input stream of JSON data. * @return A {@code UTF8FaunaParser} instance. * @throws CodecException if an {@link IOException} occurs while creating the parser. */ public static UTF8FaunaParser fromInputStream(final InputStream body) throws CodecException { JsonFactory factory = new JsonFactory(); try { JsonParser jsonParser = factory.createParser(body); UTF8FaunaParser faunaParser = new UTF8FaunaParser(jsonParser); if (faunaParser.getCurrentTokenType() == FaunaTokenType.NONE) { faunaParser.read(); } return faunaParser; } catch (IOException exc) { throw CodecException.decodingIOException(exc); } } /** * Creates a {@code UTF8FaunaParser} from a JSON string * * @param str The JSON string. * @return A {@code UTF8FaunaParser} instance. */ public static UTF8FaunaParser fromString(String str) { return UTF8FaunaParser.fromInputStream( new ByteArrayInputStream(str.getBytes(StandardCharsets.UTF_8))); } /** * Retrieves the current Fauna token type. * * @return The {@link FaunaTokenType} currently being processed. */ public FaunaTokenType getCurrentTokenType() { return currentFaunaTokenType; } /** * Skips the current object or array in the JSON data. */ public void skip() { switch (getCurrentTokenType()) { case START_OBJECT: case START_ARRAY: case START_PAGE: case START_REF: case START_DOCUMENT: skipInternal(); break; } } private void skipInternal() { int startCount = tokenStack.size(); while (read()) { if (tokenStack.size() < startCount) { break; } } } /** * Reads the next token from the JSON parser. * * @return {@code true} if there is another token to read, {@code false} if there are no more tokens. * @throws CodecException if there is an error reading the token. */ public boolean read() throws CodecException { taggedTokenValue = null; if (bufferedFaunaTokenType != null) { currentFaunaTokenType = bufferedFaunaTokenType; bufferedFaunaTokenType = null; if (closers.contains(currentFaunaTokenType)) { tokenStack.pop(); } return true; } bufferedTokenValue = null; if (!advance()) { return false; } JsonToken currentToken = jsonParser.currentToken(); if (currentToken != null) { switch (currentToken) { case VALUE_STRING: currentFaunaTokenType = FaunaTokenType.STRING; break; case START_OBJECT: handleStartObject(); break; case START_ARRAY: tokenStack.push(FaunaTokenType.START_ARRAY); currentFaunaTokenType = FaunaTokenType.START_ARRAY; break; case END_OBJECT: handleEndObject(); break; case END_ARRAY: tokenStack.pop(); currentFaunaTokenType = FaunaTokenType.END_ARRAY; break; case VALUE_TRUE: currentFaunaTokenType = FaunaTokenType.TRUE; break; case VALUE_FALSE: currentFaunaTokenType = FaunaTokenType.FALSE; break; case FIELD_NAME: currentFaunaTokenType = FaunaTokenType.FIELD_NAME; break; case VALUE_NULL: currentFaunaTokenType = FaunaTokenType.NULL; break; default: throw new CodecException( "Unhandled JSON token type " + currentToken + "."); } } else { return false; } return true; } private void handleStartObject() throws CodecException { advanceTrue(); switch (jsonParser.currentToken()) { case FIELD_NAME: switch (getText()) { case BYTES_TAG: handleTaggedString(FaunaTokenType.BYTES); break; case INT_TAG: handleTaggedString(FaunaTokenType.INT); break; case DATE_TAG: handleTaggedString(FaunaTokenType.DATE); break; case TIME_TAG: handleTaggedString(FaunaTokenType.TIME); break; case DOUBLE_TAG: handleTaggedString(FaunaTokenType.DOUBLE); break; case LONG_TAG: handleTaggedString(FaunaTokenType.LONG); break; case MOD_TAG: handleTaggedString(FaunaTokenType.MODULE); break; case STREAM_TAG: handleTaggedString(FaunaTokenType.STREAM); break; case OBJECT_TAG: advanceTrue(); currentFaunaTokenType = FaunaTokenType.START_OBJECT; tokenStack.push(InternalTokenType.START_ESCAPED_OBJECT); break; case DOC_TAG: advanceTrue(); currentFaunaTokenType = FaunaTokenType.START_DOCUMENT; tokenStack.push(FaunaTokenType.START_DOCUMENT); break; case SET_TAG: advanceTrue(); currentFaunaTokenType = FaunaTokenType.START_PAGE; if (jsonParser.currentToken() == JsonToken.VALUE_STRING) { bufferedFaunaTokenType = FaunaTokenType.STRING; try { bufferedTokenValue = jsonParser.getValueAsString(); } catch (IOException e) { throw new CodecException(e.getMessage(), e); } tokenStack.push( InternalTokenType.START_PAGE_UNMATERIALIZED); } else { tokenStack.push(FaunaTokenType.START_PAGE); } break; case REF_TAG: advanceTrue(); currentFaunaTokenType = FaunaTokenType.START_REF; tokenStack.push(FaunaTokenType.START_REF); break; default: bufferedFaunaTokenType = FaunaTokenType.FIELD_NAME; tokenStack.push(FaunaTokenType.START_OBJECT); currentFaunaTokenType = FaunaTokenType.START_OBJECT; break; } break; case END_OBJECT: bufferedFaunaTokenType = FaunaTokenType.END_OBJECT; tokenStack.push(FaunaTokenType.START_OBJECT); currentFaunaTokenType = FaunaTokenType.START_OBJECT; break; default: throw new CodecException( "Unexpected token following StartObject: " + jsonParser.currentToken()); } } private void handleEndObject() { Object startToken = tokenStack.pop(); if (startToken.equals(FaunaTokenType.START_DOCUMENT)) { currentFaunaTokenType = FaunaTokenType.END_DOCUMENT; advanceTrue(); } else if (startToken.equals(InternalTokenType.START_PAGE_UNMATERIALIZED)) { currentFaunaTokenType = FaunaTokenType.END_PAGE; } else if (startToken.equals(FaunaTokenType.START_PAGE)) { currentFaunaTokenType = FaunaTokenType.END_PAGE; advanceTrue(); } else if (startToken.equals(FaunaTokenType.START_REF)) { currentFaunaTokenType = FaunaTokenType.END_REF; advanceTrue(); } else if (startToken.equals(InternalTokenType.START_ESCAPED_OBJECT)) { currentFaunaTokenType = FaunaTokenType.END_OBJECT; advanceTrue(); } else if (startToken.equals(FaunaTokenType.START_OBJECT)) { currentFaunaTokenType = FaunaTokenType.END_OBJECT; } else { throw new CodecException("Unexpected token " + startToken + ". This might be a bug."); } } private void handleTaggedString(final FaunaTokenType token) throws CodecException { try { advanceTrue(); currentFaunaTokenType = token; taggedTokenValue = jsonParser.getText(); advance(); } catch (IOException exc) { throw CodecException.decodingIOException(exc); } } private String getText() throws CodecException { try { return jsonParser.getText(); } catch (IOException exc) { throw CodecException.decodingIOException(exc); } } private void advanceTrue() { if (!advance()) { throw new CodecException( "Unexpected end of underlying JSON reader."); } } private boolean advance() { try { return Objects.nonNull(jsonParser.nextToken()); } catch (IOException e) { throw new CodecException( "Failed to advance underlying JSON reader.", e); } } private void validateTaggedType(final FaunaTokenType type) { if (currentFaunaTokenType != type || taggedTokenValue == null) { throw new IllegalStateException( "CurrentTokenType is a " + currentFaunaTokenType.toString() + ", not a " + type.toString() + "."); } } private void validateTaggedTypes(final FaunaTokenType... types) { if (!Arrays.asList(types).contains(currentFaunaTokenType)) { throw new IllegalStateException( "CurrentTokenType is a " + currentFaunaTokenType.toString() + ", not in " + Arrays.toString(types) + "."); } } // Getters for various token types with appropriate validation /** * Retrieves the value as a {@code Character} if the current token type is {@link FaunaTokenType#INT}. * * @return The current value as a {@link Character}. */ public Character getValueAsCharacter() { validateTaggedType(FaunaTokenType.INT); return (char) Integer.parseInt(taggedTokenValue); } /** * Retrieves the current value as a {@link String}. * * @return The current value as a {@link String}. */ public String getValueAsString() { try { if (bufferedTokenValue != null) { return bufferedTokenValue.toString(); } return jsonParser.getValueAsString(); } catch (IOException e) { throw new CodecException( "Error getting the current token as String", e); } } /** * Retrieves the tagged value as a {@link String}. * * @return The tagged value as a {@link String}. */ public String getTaggedValueAsString() { return taggedTokenValue; } /** * Retrieves the value as a byte array if the current token type is {@link FaunaTokenType#BYTES}. * * @return The current value as a byte array. */ public byte[] getValueAsByteArray() { validateTaggedTypes(FaunaTokenType.BYTES); return Base64.getDecoder().decode(taggedTokenValue.getBytes()); } /** * Retrieves the value as a {@code Byte} if the current token type is {@link FaunaTokenType#INT}. * * @return The current value as a {@code Byte}. */ public Byte getValueAsByte() { validateTaggedType(FaunaTokenType.INT); try { return Byte.parseByte(taggedTokenValue); } catch (NumberFormatException e) { throw new CodecException("Error getting the current token as Byte", e); } } /** * Retrieves the value as a {@code Short} if the current token type is {@link FaunaTokenType#INT}. * * @return The current value as a {@code Short}. */ public Short getValueAsShort() { validateTaggedType(FaunaTokenType.INT); try { return Short.parseShort(taggedTokenValue); } catch (NumberFormatException e) { throw new CodecException("Error getting the current token as Short", e); } } /** * Retrieves the value as an {@code Integer} if the current token type is {@link FaunaTokenType#INT} or {@link FaunaTokenType#LONG}. * * @return The current value as an {@code Integer}. */ public Integer getValueAsInt() { validateTaggedTypes(FaunaTokenType.INT, FaunaTokenType.LONG); try { return Integer.parseInt(taggedTokenValue); } catch (NumberFormatException e) { throw new CodecException("Error getting the current token as Integer", e); } } /** * Retrieves the current value as a {@code Boolean}. * * @return The current value as a {@code Boolean}. */ public Boolean getValueAsBoolean() { try { return jsonParser.getValueAsBoolean(); } catch (IOException e) { throw new CodecException("Error getting the current token as Boolean", e); } } /** * Retrieves the current value as a {@link LocalDate} if the current token type is {@link FaunaTokenType#DATE}. * * @return The current value as a {@link LocalDate}. */ public LocalDate getValueAsLocalDate() { validateTaggedType(FaunaTokenType.DATE); try { return LocalDate.parse(taggedTokenValue); } catch (DateTimeParseException e) { throw new CodecException("Error getting the current token as LocalDate", e); } } /** * Retrieves the current value as an {@link Instant} if the current token type is {@link FaunaTokenType#TIME}. * * @return The current value as an {@link Instant}. */ public Instant getValueAsTime() { validateTaggedType(FaunaTokenType.TIME); try { return Instant.parse(taggedTokenValue); } catch (DateTimeParseException e) { throw new CodecException("Error getting the current token as LocalDateTime", e); } } /** * Retrieves the value as a {@code Float} if the current token type is * {@link FaunaTokenType#INT}, {@link FaunaTokenType#LONG}, or {@link FaunaTokenType#DOUBLE}. * * @return The current value as a {@code Float}. */ public Float getValueAsFloat() { validateTaggedTypes(FaunaTokenType.INT, FaunaTokenType.LONG, FaunaTokenType.DOUBLE); try { return Float.parseFloat(taggedTokenValue); } catch (NumberFormatException e) { throw new CodecException("Error getting the current token as Float", e); } } /** * Retrieves the value as a {@code Double} if the current token type is {@link FaunaTokenType#INT}, * {@link FaunaTokenType#LONG}, or {@link FaunaTokenType#DOUBLE}. * * @return The current value as a {@code Double}. */ public Double getValueAsDouble() { validateTaggedTypes(FaunaTokenType.INT, FaunaTokenType.LONG, FaunaTokenType.DOUBLE); try { return Double.parseDouble(taggedTokenValue); } catch (NumberFormatException e) { throw new CodecException("Error getting the current token as Double", e); } } /** * Retrieves the value as a {@code Long} if the current token type is * {@link FaunaTokenType#INT} or {@link FaunaTokenType#LONG}. * * @return The current value as a {@code Long}. */ public Long getValueAsLong() { validateTaggedTypes(FaunaTokenType.INT, FaunaTokenType.LONG); try { return Long.parseLong(taggedTokenValue); } catch (NumberFormatException e) { throw new CodecException("Error getting the current token as Long", e); } } /** * Retrieves the value as a {@link Module} if the current token type is {@link FaunaTokenType#MODULE}. * * @return The current value as a {@link Module}. */ public Module getValueAsModule() { try { return new Module(taggedTokenValue); } catch (Exception e) { throw new CodecException("Error getting the current token as Module", e); } } } ``` ## File: src/main/java/com/fauna/constants/Defaults.java ```java package com.fauna.constants; import java.time.Duration; /** * Defines default constants used throughout the Fauna client. * *

The {@code Defaults} class includes constants for configuration settings, such as timeouts, * retry limits, and default secrets, that provide sensible defaults for common client operations.

*/ public final class Defaults { private Defaults() { } /** * The buffer duration added to the client timeout to ensure safe execution time. */ public static final Duration CLIENT_TIMEOUT_BUFFER = Duration.ofSeconds(5); /** * The default timeout duration for client requests. */ public static final Duration DEFAULT_TIMEOUT = Duration.ofSeconds(5); /** * The default secret for local Fauna deployments created using the Fauna Docker image. */ public static final String LOCAL_FAUNA_SECRET = "secret"; /** * The maximum number of retries allowed for handling transaction contention. */ public static final int MAX_CONTENTION_RETRIES = 3; } ``` ## File: src/main/java/com/fauna/constants/ErrorMessages.java ```java package com.fauna.constants; /** * Defines standard error messages used throughout the Fauna client. * *

The {@code ErrorMessages} class centralizes error message constants for common * operations, allowing for consistent messaging across the client and simplifying maintenance.

*/ public final class ErrorMessages { private ErrorMessages() { } /** * Error message indicating a query execution failure. */ public static final String QUERY_EXECUTION = "Unable to execute query."; /** * Error message indicating a failure to subscribe to an event stream. */ public static final String STREAM_SUBSCRIPTION = "Unable to subscribe to stream."; /** * Error message indicating a failure to subscribe to an event feed. */ public static final String FEED_SUBSCRIPTION = "Unable to subscribe to feed."; /** * Error message indicating a failure to query a page of data. */ public static final String QUERY_PAGE = "Unable to query page."; } ``` ## File: src/main/java/com/fauna/constants/ResponseFields.java ```java package com.fauna.constants; /** * Defines constants for field names in responses returned by the Fauna Core HTTP API. * *

The {@code ResponseFields} class centralizes commonly used JSON field names in Fauna responses, * making it easier to reference them consistently and preventing hard-coded strings throughout the codebase.

*/ public final class ResponseFields { private ResponseFields() { } // Top-level fields /** * Field name for data returned in a response. */ public static final String DATA_FIELD_NAME = "data"; /** * Field name for the last seen transaction timestamp. */ public static final String LAST_SEEN_TXN_FIELD_NAME = "txn_ts"; /** * Field name for the static type of the response data. */ public static final String STATIC_TYPE_FIELD_NAME = "static_type"; /** * Field name for query and event stats in the response. */ public static final String STATS_FIELD_NAME = "stats"; /** * Field name for the database schema version in the response. */ public static final String SCHEMA_VERSION_FIELD_NAME = "schema_version"; /** * Field name for the summary information in the response. */ public static final String SUMMARY_FIELD_NAME = "summary"; /** * Field name for query tags included in the response. */ public static final String QUERY_TAGS_FIELD_NAME = "query_tags"; /** * Field name for error information in the response. */ public static final String ERROR_FIELD_NAME = "error"; // "stats" block /** * Field name for compute operation statistics. */ public static final String STATS_COMPUTE_OPS_FIELD_NAME = "compute_ops"; /** * Field name for read operation statistics. */ public static final String STATS_READ_OPS = "read_ops"; /** * Field name for write operation statistics. */ public static final String STATS_WRITE_OPS = "write_ops"; /** * Field name for the query runtime in milliseconds. */ public static final String STATS_QUERY_TIME_MS = "query_time_ms"; /** * Field name for event processing time in milliseconds. */ public static final String STATS_PROCESSING_TIME_MS = "processing_time_ms"; /** * Field name for transaction contention retries count. */ public static final String STATS_CONTENTION_RETRIES = "contention_retries"; /** * Field name for data read from storage, in bytes. */ public static final String STATS_STORAGE_BYTES_READ = "storage_bytes_read"; /** * Field name for data written to storage, in bytes. */ public static final String STATS_STORAGE_BYTES_WRITE = "storage_bytes_write"; /** * Field name for rate limit hits. */ public static final String STATS_RATE_LIMITS_HIT = "rate_limits_hit"; // "error" block /** * Field name for the error code in the response. */ public static final String ERROR_CODE_FIELD_NAME = "code"; /** * Field name for the error message in the response. */ public static final String ERROR_MESSAGE_FIELD_NAME = "message"; /** * Field name for constraint failures in error information. */ public static final String ERROR_CONSTRAINT_FAILURES_FIELD_NAME = "constraint_failures"; /** * Field name for abort error information in the error response. */ public static final String ERROR_ABORT_FIELD_NAME = "abort"; /** * Field name for the error name in the response. */ public static final String ERROR_NAME_FIELD_NAME = "name"; /** * Field name for paths involved in the error. */ public static final String ERROR_PATHS_FIELD_NAME = "paths"; // Event-related fields /** * Field name for the cursor in stream and feed responses. */ public static final String CURSOR_FIELD_NAME = "cursor"; /** * Field name for the event type in event feed and event stream responses. */ public static final String STREAM_TYPE_FIELD_NAME = "type"; // Feed-related fields /** * Field name for events in event feed responses. */ public static final String EVENTS_FIELD_NAME = "events"; /** * Field name indicating whether there are more pages in event feed responses. */ public static final String FEED_HAS_NEXT_FIELD_NAME = "has_next"; } ``` ## File: src/main/java/com/fauna/env/DriverEnvironment.java ```java package com.fauna.env; import java.io.IOException; import java.io.InputStream; import java.util.Properties; /** * Provides information about the runtime environment of the Fauna driver. * *

The {@code DriverEnvironment} class detects the environment, operating system, runtime version, * and driver version, which can be helpful for diagnostics and compatibility checks.

*/ public class DriverEnvironment { private final String driverVersion; private final String env; private final String os; private String runtime; /** * Enum representing the supported JVM drivers. */ public enum JvmDriver { JAVA, SCALA } /** * Constructs a {@code DriverEnvironment} instance with the specified JVM driver type. * * @param jvmDriver The {@link JvmDriver} type to identify the runtime (e.g., Java or Scala). */ public DriverEnvironment(final JvmDriver jvmDriver) { this.env = getRuntimeEnvironment(); this.os = System.getProperty("os.name") + "-" + System.getProperty("os.version"); this.runtime = String.format("java-%s", System.getProperty("java.version")); this.driverVersion = getVersion(); if (jvmDriver == JvmDriver.SCALA) { this.runtime = String.format("%s,scala", this.runtime); } } /** * Retrieves the software version from the "version.properties" file. * * @return A {@code String} representing the software version. * @throws RuntimeException If the "version.properties" file is not found or if there is an issue reading the file. */ public static String getVersion() { Properties properties = new Properties(); try (InputStream input = DriverEnvironment.class.getClassLoader().getResourceAsStream("version.properties")) { if (input == null) { throw new RuntimeException("Version not found."); } properties.load(input); return properties.getProperty("version"); } catch (IOException ex) { throw new RuntimeException("Failed to retrieve version."); } } /** * Determines the runtime environment by checking for the presence of certain environment variables. * * @return A {@code String} representing the detected environment (e.g., "Heroku", "AWS Lambda"). */ private String getRuntimeEnvironment() { if (System.getenv("PATH") != null && System.getenv("PATH").contains(".heroku")) { return "Heroku"; } else if (System.getenv("AWS_LAMBDA_FUNCTION_VERSION") != null) { return "AWS Lambda"; } else if (System.getenv("_") != null && System.getenv("_").contains("google")) { return "GCP Cloud Functions"; } else if (System.getenv("GOOGLE_CLOUD_PROJECT") != null) { return "GCP Compute Instances"; } else if (System.getenv("WEBSITE_FUNCTIONS_AZUREMONITOR_CATEGORIES") != null) { return "Azure Cloud Functions"; } else if (System.getenv("ORYX_ENV_TYPE") != null && System.getenv("WEBSITE_INSTANCE_ID") != null && System.getenv("ORYX_ENV_TYPE").equals("AppService")) { return "Azure Compute"; } else { return "Unknown"; } } /** * Returns a string representation of the driver environment, including the driver version, * runtime, environment, and operating system. * * @return A {@code String} representation of this {@code DriverEnvironment}. */ @Override public String toString() { return String.format("driver=%s; runtime=java-%s; env=%s; os=%s", driverVersion, runtime, env, os).toLowerCase(); } } ``` ## File: src/main/java/com/fauna/env/package-info.java ```java /** * Provides utilities for managing the environment in which the Fauna driver operates. * *

This package includes classes that detect and encapsulate runtime information about the * environment, operating system, JVM version, and driver version. These details can be used for * diagnostics, compatibility checks, and logging purposes.

* *
    *
  • {@link com.fauna.env.DriverEnvironment} - Detects and captures information about the runtime * environment, such as operating system details, Java version, and cloud environment (e.g., * AWS Lambda, GCP Cloud Functions).
  • *
* *

The {@code com.fauna.env} package supports gathering environment-related details to help * understand the conditions under which the driver is operating, which can be useful for * troubleshooting and optimizing performance across various deployment platforms.

*/ package com.fauna.env; ``` ## File: src/main/java/com/fauna/event/EventSource.java ```java package com.fauna.event; import com.fauna.query.builder.Query; import java.util.Objects; /** * Represents an event source. You can consume event sources as event feeds by * calling {@link com.fauna.client.FaunaClient#feed(EventSource, FeedOptions, Class) FaunaClient.feed()} * or as event streams by calling * {@link com.fauna.client.FaunaClient#stream(EventSource, StreamOptions, Class) FaunaClient.stream()}. *

* The {@code EventSource} class provides methods for constructing instances from event source tokens and responses */ public class EventSource { private final String token; /** * Constructs a new {@code EventSource} with the specified token. * * @param token A {@code String} representing the event source. */ public EventSource(final String token) { this.token = token; } /** * Retrieves the token for the event source. * * @return A {@code String} representing the token. */ public String getToken() { return token; } /** * Creates an {@code EventSource} from the specified token. * * @param token A {@code String} representing the token for the event source. * @return A new {@code EventSource} instance. */ public static EventSource fromToken(final String token) { return new EventSource(token); } /** * Compares this {@code EventSource} with another object for equality. * * @param o The object to compare with. * @return {@code true} if the specified object is equal to this {@code EventSource}; {@code false} otherwise. */ @Override public boolean equals(final Object o) { if (this == o) { return true; } if (o == null) { return false; } if (getClass() != o.getClass()) { return false; } EventSource c = (EventSource) o; return Objects.equals(token, c.token); } /** * Returns the hash code for this {@code EventSource}. * * @return An {@code int} representing the hash code of this object. */ @Override public int hashCode() { return Objects.hash(token); } } ``` ## File: src/main/java/com/fauna/event/FaunaEvent.java ```java package com.fauna.event; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.JsonToken; import com.fauna.codec.Codec; import com.fauna.codec.FaunaTokenType; import com.fauna.codec.UTF8FaunaParser; import com.fauna.exception.ClientResponseException; import com.fauna.response.ErrorInfo; import com.fauna.response.QueryStats; import java.io.IOException; import java.util.Optional; import static com.fauna.constants.ResponseFields.CURSOR_FIELD_NAME; import static com.fauna.constants.ResponseFields.DATA_FIELD_NAME; import static com.fauna.constants.ResponseFields.ERROR_FIELD_NAME; import static com.fauna.constants.ResponseFields.LAST_SEEN_TXN_FIELD_NAME; import static com.fauna.constants.ResponseFields.STATS_FIELD_NAME; import static com.fauna.constants.ResponseFields.STREAM_TYPE_FIELD_NAME; /** * Represents an event emitted in an event feed or event stream. * @param The type of data contained in the event. */ public final class FaunaEvent { /** * Enum representing possible event types from a Fauna event source. */ public enum EventType { STATUS, ADD, UPDATE, REMOVE, ERROR } private final EventType type; private final String cursor; private final Long txnTs; private final E data; private final QueryStats stats; private final ErrorInfo error; /** * Constructs a new {@code FaunaEvent} with the specified properties. * * @param type The type of the event. * @param cursor The cursor for the event. * @param txnTs The transaction timestamp for the document change that triggered the event. * @param data The data for the document that triggered the event. * @param stats The event stats. * @param error The error information for the event, if any. */ public FaunaEvent(final EventType type, final String cursor, final Long txnTs, final E data, final QueryStats stats, final ErrorInfo error) { this.type = type; this.cursor = cursor; this.txnTs = txnTs; this.data = data; this.stats = stats; this.error = error; } /** * Retrieves the type of this event. * * @return The {@link EventType} of this event. */ public EventType getType() { return type; } /** * Retrieves the Fauna document data associated with this event. * * @return An {@link Optional} containing the event data, or empty if no data is available. */ public Optional getData() { return Optional.ofNullable(data); } /** * Retrieves the transaction timestamp for the document change that triggered the event. * * @return An {@link Optional} containing the transaction timestamp, or empty if not present. */ public Optional getTimestamp() { return Optional.ofNullable(txnTs); } /** * Retrieves the cursor for this event. * * @return A {@code String} representing the cursor. */ public String getCursor() { return cursor; } /** * Retrieves stats associated with this event. * * @return A {@link QueryStats} object representing the statistics. */ public QueryStats getStats() { return stats; } /** * Retrieves the error information for this event, if any. * * @return An {@link ErrorInfo} object containing error details, or {@code null} if no error is present. */ public ErrorInfo getError() { return this.error; } /** * Builder class for constructing a {@code FaunaEvent} instance. * * @param The type of data contained in the event. */ public static final class Builder { private final Codec dataCodec; private String cursor = null; private FaunaEvent.EventType eventType = null; private QueryStats stats = null; private E data = null; private Long txnTs = null; private ErrorInfo errorInfo = null; /** * Constructs a {@code Builder} for building a {@code FaunaEvent}. * * @param dataCodec The {@link Codec} used to decode event data. */ public Builder(final Codec dataCodec) { this.dataCodec = dataCodec; } /** * Sets the cursor for the event. * * @param cursor The cursor to set. * @return This {@code Builder} instance. */ public Builder cursor(final String cursor) { this.cursor = cursor; return this; } /** * Sets the event type. * * @param eventType The {@link EventType} of the event. * @return This {@code Builder} instance. */ public Builder eventType(final FaunaEvent.EventType eventType) { this.eventType = eventType; return this; } /** * Sets the query statistics for the event. * * @param stats The {@link QueryStats} to set. * @return This {@code Builder} instance. */ public Builder stats(final QueryStats stats) { this.stats = stats; return this; } /** * Parses and sets the event data from the given JSON parser. * * @param parser The {@link JsonParser} to decode the data from. * @return This {@code Builder} instance. */ public Builder parseData(final JsonParser parser) { UTF8FaunaParser faunaParser = new UTF8FaunaParser(parser); if (faunaParser.getCurrentTokenType() == FaunaTokenType.NONE) { faunaParser.read(); } this.data = dataCodec.decode(faunaParser); return this; } /** * Sets the transaction timestamp for the event. * * @param txnTs The transaction timestamp to set. * @return This {@code Builder} instance. */ public Builder txnTs(final Long txnTs) { this.txnTs = txnTs; return this; } /** * Sets the error information for the event. * * @param error The {@link ErrorInfo} containing error details. * @return This {@code Builder} instance. */ public Builder error(final ErrorInfo error) { this.errorInfo = error; this.eventType = EventType.ERROR; return this; } /** * Builds and returns a {@code FaunaEvent} instance. * * @return A new {@code FaunaEvent} instance. */ public FaunaEvent build() { return new FaunaEvent<>(eventType, cursor, txnTs, data, stats, errorInfo); } } /** * Creates a new {@code Builder} for constructing a {@code FaunaEvent}. * * @param dataCodec The {@link Codec} used to decode event data. * @param The type of data contained in the event. * @return A new {@code Builder} instance. */ public static Builder builder(final Codec dataCodec) { return new Builder<>(dataCodec); } /** * Parses and sets the appropriate field in the builder based on the JSON parser's current field. * * @param builder The {@code Builder} being populated. * @param parser The {@link JsonParser} for reading the field value. * @param The type of data contained in the event. * @return The updated {@code Builder} instance. * @throws IOException If an error occurs while parsing. */ private static Builder parseField(final Builder builder, final JsonParser parser) throws IOException { String fieldName = parser.getValueAsString(); switch (fieldName) { case CURSOR_FIELD_NAME: return builder.cursor(parser.nextTextValue()); case DATA_FIELD_NAME: return builder.parseData(parser); case STREAM_TYPE_FIELD_NAME: return builder.eventType(parseEventType(parser)); case STATS_FIELD_NAME: return builder.stats(QueryStats.parseStats(parser)); case LAST_SEEN_TXN_FIELD_NAME: return builder.txnTs(parser.nextLongValue(0L)); case ERROR_FIELD_NAME: return builder.error(ErrorInfo.parse(parser)); default: throw new ClientResponseException("Unknown FaunaEvent field: " + fieldName); } } /** * Parses the event type from the JSON parser. * * @param parser The {@link JsonParser} positioned at the event type field. * @return The parsed {@link EventType}. * @throws IOException If an error occurs while parsing. */ private static FaunaEvent.EventType parseEventType(final JsonParser parser) throws IOException { if (parser.nextToken() == JsonToken.VALUE_STRING) { String typeString = parser.getText().toUpperCase(); try { return FaunaEvent.EventType.valueOf(typeString); } catch (IllegalArgumentException e) { throw new ClientResponseException("Invalid event type: " + typeString, e); } } else { throw new ClientResponseException("Event type should be a string, but got a " + parser.currentToken().asString()); } } /** * Parses a {@code FaunaEvent} from the JSON parser using the specified codec. * * @param parser The {@link JsonParser} positioned at the start of the event. * @param dataCodec The {@link Codec} used to decode event data. * @param The type of data contained in the event. * @return The parsed {@code FaunaEvent}. * @throws IOException If an error occurs while parsing. */ public static FaunaEvent parse(final JsonParser parser, final Codec dataCodec) throws IOException { if (parser.currentToken() == JsonToken.START_OBJECT || parser.nextToken() == JsonToken.START_OBJECT) { Builder builder = FaunaEvent.builder(dataCodec); while (parser.nextToken() == JsonToken.FIELD_NAME) { builder = parseField(builder, parser); } return builder.build(); } else { throw new ClientResponseException("Invalid event starting with: " + parser.currentToken()); } } } ``` ## File: src/main/java/com/fauna/event/FaunaStream.java ```java package com.fauna.event; import com.fasterxml.jackson.core.JsonFactory; import com.fasterxml.jackson.core.JsonParser; import com.fauna.client.StatsCollector; import com.fauna.codec.Codec; import com.fauna.codec.DefaultCodecProvider; import com.fauna.exception.ClientException; import com.fauna.response.ErrorInfo; import com.fauna.response.MultiByteBufferInputStream; import java.io.IOException; import java.nio.ByteBuffer; import java.text.MessageFormat; import java.util.List; import java.util.concurrent.Flow.Processor; import java.util.concurrent.Flow.Subscriber; import java.util.concurrent.Flow.Subscription; import java.util.concurrent.SubmissionPublisher; /** * A processor for handling and decoding Fauna event streams. *

* The {@code FaunaStream} class extends {@link SubmissionPublisher} to process * incoming ByteBuffers, decode them into {@link FaunaEvent} objects, and forward * them to subscribers. * * @param The type of document data contained in the Fauna events. */ public class FaunaStream extends SubmissionPublisher> implements Processor, FaunaEvent> { private static final JsonFactory JSON_FACTORY = new JsonFactory(); private final Codec dataCodec; private Subscription subscription; private Subscriber> eventSubscriber; private MultiByteBufferInputStream buffer = null; private final StatsCollector statsCollector; /** * Constructs a {@code FaunaStream} instance with the specified event data type and stats collector. * * @param elementClass The class of the event data type. * @param statsCollector The {@link StatsCollector} to track statistics for events. */ public FaunaStream(final Class elementClass, final StatsCollector statsCollector) { this.statsCollector = statsCollector; this.dataCodec = DefaultCodecProvider.SINGLETON.get(elementClass); } /** * Subscribes a single subscriber to this stream. * * @param subscriber The {@link Subscriber} to subscribe to this stream. * @throws ClientException if more than one subscriber attempts to subscribe. */ @Override public void subscribe(final Subscriber> subscriber) { if (this.eventSubscriber == null) { this.eventSubscriber = subscriber; super.subscribe(subscriber); this.subscription.request(1); } else { throw new ClientException("Only one subscriber is supported."); } } /** * Handles subscription by setting the subscription and requesting data. * * @param subscription The subscription to this stream. */ @Override public void onSubscribe(final Subscription subscription) { this.subscription = subscription; } /** * Processes incoming ByteBuffers, decodes them into Fauna events, and submits the events to subscribers. * * @param buffers The list of {@link ByteBuffer}s containing encoded event data. * @throws ClientException if there is an error decoding the stream or processing events. */ @Override public void onNext(final List buffers) { try { synchronized (this) { if (this.buffer == null) { this.buffer = new MultiByteBufferInputStream(buffers); } else { this.buffer.add(buffers); } try { JsonParser parser = JSON_FACTORY.createParser(buffer); FaunaEvent event = FaunaEvent.parse(parser, dataCodec); statsCollector.add(event.getStats()); if (event.getType() == FaunaEvent.EventType.ERROR) { ErrorInfo error = event.getError(); this.onComplete(); this.close(); throw new ClientException(MessageFormat.format( "Stream stopped due to error {0} {1}", error.getCode(), error.getMessage())); } this.submit(event); this.buffer = null; } catch (ClientException e) { this.buffer.reset(); // Handles partial event decoding } catch (IOException e) { throw new ClientException("Unable to decode stream", e); } } } catch (Exception e) { throw new ClientException("Unable to decode stream", e); } finally { this.subscription.request(1); } } /** * Handles errors by canceling the subscription and closing the stream. * * @param throwable The {@link Throwable} encountered during stream processing. */ @Override public void onError(final Throwable throwable) { this.subscription.cancel(); this.close(); } /** * Completes the stream by canceling the subscription. */ @Override public void onComplete() { this.subscription.cancel(); } } ``` ## File: src/main/java/com/fauna/event/FeedIterator.java ```java package com.fauna.event; import com.fauna.client.FaunaClient; import com.fauna.exception.FaunaException; import java.util.Iterator; import java.util.NoSuchElementException; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionException; /** * FeedIterator iterates over event feed pages from Fauna. * * @param */ public final class FeedIterator implements Iterator> { private final FaunaClient client; private final Class resultClass; private final EventSource eventSource; private FeedOptions latestOptions; private CompletableFuture> feedFuture; /** * Construct a new PageIterator. * * @param client A client that makes requests to Fauna. * @param eventSource The Fauna Event Source. * @param feedOptions The FeedOptions object. * @param resultClass The class of the elements returned from Fauna (i.e. the rows). */ public FeedIterator(final FaunaClient client, final EventSource eventSource, final FeedOptions feedOptions, final Class resultClass) { this.client = client; this.resultClass = resultClass; this.eventSource = eventSource; this.latestOptions = feedOptions; this.feedFuture = client.poll(eventSource, feedOptions, resultClass); } @Override public boolean hasNext() { return this.feedFuture != null; } /** * Returns a CompletableFuture that will complete with the next page (or throw a FaunaException). * When the future completes, the next page will be fetched in the background. * * @return A CompletableFuture that completes with a new FeedPage instance. */ public CompletableFuture> nextAsync() { if (this.feedFuture != null) { return this.feedFuture.thenApply(fs -> { if (fs.hasNext()) { FeedOptions options = this.latestOptions.nextPage(fs); this.latestOptions = options; this.feedFuture = client.poll(this.eventSource, options, resultClass); } else { this.feedFuture = null; } return fs; }); } else { throw new NoSuchElementException(); } } /** * Get the next Page (synchronously). * * @return FeedPage The next Page of elements E. * @throws FaunaException If there is an error getting the next page. */ @Override public FeedPage next() { try { return nextAsync().join(); } catch (CompletionException ce) { if (ce.getCause() != null && ce.getCause() instanceof FaunaException) { throw (FaunaException) ce.getCause(); } else { throw ce; } } } /** * Return an iterator that iterates directly over the items that make up the page contents. * * @return An iterator of E. */ public Iterator> flatten() { return new Iterator<>() { private final FeedIterator feedIterator = FeedIterator.this; private Iterator> thisPage = feedIterator.hasNext() ? feedIterator.next().getEvents().iterator() : null; @Override public boolean hasNext() { return thisPage != null && (thisPage.hasNext() || feedIterator.hasNext()); } @Override public FaunaEvent next() { if (thisPage == null) { throw new NoSuchElementException(); } try { return thisPage.next(); } catch (NoSuchElementException e) { if (feedIterator.hasNext()) { this.thisPage = feedIterator.next().getEvents().iterator(); return thisPage.next(); } else { throw e; } } } }; } } ``` ## File: src/main/java/com/fauna/event/FeedOptions.java ```java package com.fauna.event; import java.time.Duration; import java.util.Optional; import static com.fauna.constants.Defaults.DEFAULT_TIMEOUT; /** * Represents the options for configuring an event feed request in Fauna. *

* The {@code FeedOptions} class provides configuration parameters such as cursor, * start timestamp, page size, and timeout for retrieving feeds from Fauna. */ public class FeedOptions { private final String cursor; private final Long startTs; private final Integer pageSize; private final Duration timeout; /** * The default {@code FeedOptions} instance with default settings. */ public static final FeedOptions DEFAULT = FeedOptions.builder().build(); /** * Constructs a new {@code FeedOptions} with the specified parameters. * * @param cursor A {@code String} representing the cursor in the feed. Cannot be provided with a * {@code startTs}. * @param startTs A {@code Long} representing the start timestamp for the feed. Represents a time in microseconds since the Unix epoch. Cannot be provided with a * {@code cursor}. * @param pageSize An {@code Integer} specifying the maximum number of * events returned per page. Must be in the range 1 to 16000 * (inclusive). Defaults to 16. * @param timeout A {@code Duration} specifying the timeout for the feed request. * @throws IllegalArgumentException if both {@code cursor} and {@code startTs} are set. */ public FeedOptions(final String cursor, final Long startTs, final Integer pageSize, final Duration timeout) { this.cursor = cursor; this.startTs = startTs; this.pageSize = pageSize; this.timeout = timeout; if (cursor != null && startTs != null) { throw new IllegalArgumentException( "Only one of cursor and startTs can be set."); } } /** * Retrieves the cursor. * * @return An {@link Optional} containing the cursor, or empty if not set. */ public Optional getCursor() { return Optional.ofNullable(cursor); } /** * Retrieves the start timestamp. * * @return An {@link Optional} containing the start timestamp, or empty if not set. */ public Optional getStartTs() { return Optional.ofNullable(startTs); } /** * Retrieves the page size. * * @return An {@link Optional} containing the page size, or empty if not set. */ public Optional getPageSize() { return Optional.ofNullable(pageSize); } /** * Retrieves the timeout duration. * * @return An {@link Optional} containing the timeout duration, or empty if not set. */ public Optional getTimeout() { return Optional.ofNullable(timeout); } /** * Builder class for constructing {@code FeedOptions} instances. */ public static class Builder { private String cursor = null; private Long startTs = null; private Integer pageSize = null; private Duration timeout = DEFAULT_TIMEOUT; /** * Sets the cursor. * * @param cursor A {@code String} representing the cursor. * @return This {@code Builder} instance. * @throws IllegalArgumentException if {@code startTs} is already set. */ public Builder cursor(final String cursor) { if (startTs != null) { throw new IllegalArgumentException( "Only one of cursor and startTs can be set."); } this.cursor = cursor; return this; } /** * Sets the start timestamp. * * @param startTs A {@code Long} representing the start timestamp. * @return This {@code Builder} instance. * @throws IllegalArgumentException if {@code cursor} is already set. */ public Builder startTs(final Long startTs) { if (cursor != null) { throw new IllegalArgumentException( "Only one of cursor and startTs can be set."); } this.startTs = startTs; return this; } /** * Sets the page size. * * @param pageSize An {@code Integer} specifying the number of items per page. * @return This {@code Builder} instance. */ public Builder pageSize(final Integer pageSize) { this.pageSize = pageSize; return this; } /** * Sets the timeout duration. * * @param timeout A {@code Duration} specifying the timeout for the feed request. * @return This {@code Builder} instance. */ public Builder timeout(final Duration timeout) { this.timeout = timeout; return this; } /** * Builds a new {@code FeedOptions} instance with the configured parameters. * * @return A new {@code FeedOptions} instance. * @throws IllegalArgumentException if both {@code cursor} and {@code startTs} are set. */ public FeedOptions build() { return new FeedOptions(cursor, startTs, pageSize, timeout); } } /** * Creates a new {@code Builder} for constructing {@code FeedOptions}. * * @return A new {@code Builder} instance. */ public static Builder builder() { return new Builder(); } /** * Returns the {@code FeedOptions} for the next page, based on the cursor of the given page. *

* This method copies options like page size and timeout, but does not set or copy {@code startTs}, * because it uses the cursor. * * @param page The current or latest {@code FeedPage}. * @return A new {@code FeedOptions} instance configured for the next page. */ public FeedOptions nextPage(final FeedPage page) { FeedOptions.Builder builder = FeedOptions.builder().cursor(page.getCursor()); // Do not set or copy startTs, because we are using cursor. getPageSize().ifPresent(builder::pageSize); getTimeout().ifPresent(builder::timeout); return builder.build(); } } ``` ## File: src/main/java/com/fauna/event/FeedPage.java ```java package com.fauna.event; import com.fasterxml.jackson.core.JsonFactory; import com.fasterxml.jackson.core.JsonParser; import com.fauna.client.StatsCollector; import com.fauna.codec.Codec; import com.fauna.exception.ClientResponseException; import com.fauna.response.QueryResponse; import com.fauna.response.QueryStats; import java.io.IOException; import java.io.InputStream; import java.net.http.HttpResponse; import java.util.ArrayList; import java.util.List; import static com.fasterxml.jackson.core.JsonToken.END_ARRAY; import static com.fasterxml.jackson.core.JsonToken.FIELD_NAME; import static com.fasterxml.jackson.core.JsonToken.START_ARRAY; import static com.fasterxml.jackson.core.JsonToken.START_OBJECT; import static com.fauna.constants.ResponseFields.CURSOR_FIELD_NAME; import static com.fauna.constants.ResponseFields.EVENTS_FIELD_NAME; import static com.fauna.constants.ResponseFields.FEED_HAS_NEXT_FIELD_NAME; import static com.fauna.constants.ResponseFields.STATS_FIELD_NAME; /** * Represents a page of events from an event feed. * * @param The type of data contained in each event. */ public class FeedPage { private final List> events; private final String cursor; private final boolean hasNext; private final QueryStats stats; private static final JsonFactory JSON_FACTORY = new JsonFactory(); /** * Constructs a {@code FeedPage} with the specified events, cursor, pagination flag, and statistics. * * @param events A list of {@link FaunaEvent} objects representing the events in this page. * @param cursor A {@code String} representing the cursor for pagination. * @param hasNext A {@code boolean} indicating if there are more pages available. * @param stats A {@link QueryStats} object containing statistics for the page. * @throws IllegalArgumentException if {@code events} is null or {@code cursor} is blank. */ public FeedPage(final List> events, final String cursor, final boolean hasNext, final QueryStats stats) { if (events == null) { throw new IllegalArgumentException("events cannot be null"); } if (cursor == null || cursor.isBlank()) { throw new IllegalArgumentException("cursor cannot be blank"); } this.events = events; this.cursor = cursor; this.hasNext = hasNext; this.stats = stats; } /** * Retrieves the list of events in this feed page. * * @return A {@code List} of {@link FaunaEvent} objects. */ public List> getEvents() { return events; } /** * Retrieves the cursor for pagination. * * @return A {@code String} representing the cursor. */ public String getCursor() { return cursor; } /** * Checks if there are more pages available. * * @return {@code true} if there are more pages, {@code false} otherwise. */ public boolean hasNext() { return hasNext; } /** * Retrieves the statistics for this feed page. * * @return A {@link QueryStats} object. */ public QueryStats getStats() { return stats; } /** * Builder class for constructing {@code FeedPage} instances. * * @param The type of data contained in each event. */ public static class Builder { private final Codec elementCodec; private final StatsCollector statsCollector; private List> events; private String cursor = ""; private Boolean hasNext = false; private QueryStats stats = null; /** * Constructs a {@code Builder} with the specified codec and stats collector. * * @param elementCodec The {@link Codec} used to decode events. * @param statsCollector The {@link StatsCollector} to gather statistics for the feed. */ public Builder(final Codec elementCodec, final StatsCollector statsCollector) { this.elementCodec = elementCodec; this.statsCollector = statsCollector; } /** * Sets the list of events for the feed page. * * @param events A list of {@link FaunaEvent} objects representing the events in this page. * @return This {@code Builder} instance. */ public Builder events(final List> events) { this.events = events; return this; } /** * Sets the cursor for pagination. * * @param cursor A {@code String} representing the cursor. * @return This {@code Builder} instance. */ public Builder cursor(final String cursor) { this.cursor = cursor; return this; } /** * Sets the flag indicating if there are more pages available. * * @param hasNext A {@code Boolean} indicating if there are more pages. * @return This {@code Builder} instance. */ public Builder hasNext(final Boolean hasNext) { this.hasNext = hasNext; return this; } /** * Sets the statistics for the feed page. * * @param stats A {@link QueryStats} object containing statistics for the page. * @return This {@code Builder} instance. */ public Builder stats(final QueryStats stats) { this.stats = stats; return this; } /** * Parses and sets the list of events from the provided JSON parser. * * @param parser The {@link JsonParser} to decode the events from. * @return This {@code Builder} instance. * @throws IOException if an error occurs during parsing. */ public Builder parseEvents(final JsonParser parser) throws IOException { if (parser.nextToken() == START_ARRAY) { List> events = new ArrayList<>(); while (parser.nextToken() != END_ARRAY) { events.add(FaunaEvent.parse(parser, elementCodec)); } this.events = events; } else { throw new IOException("Invalid event starting with: " + parser.currentToken()); } return this; } /** * Builds a new {@code FeedPage} instance with the configured parameters. * * @return A new {@code FeedPage} instance. * @throws IllegalArgumentException if {@code events} is null or {@code cursor} is blank. */ public FeedPage build() { return new FeedPage<>(events, cursor, hasNext, stats); } /** * Parses and sets the appropriate field in the builder based on the JSON parser's current field. * * @param parser The {@link JsonParser} for reading the field value. * @return The updated {@code Builder} instance. * @throws IOException if an error occurs during parsing. */ public Builder parseField(final JsonParser parser) throws IOException { String fieldName = parser.getValueAsString(); switch (fieldName) { case CURSOR_FIELD_NAME: return cursor(parser.nextTextValue()); case EVENTS_FIELD_NAME: return parseEvents(parser); case STATS_FIELD_NAME: QueryStats stats = QueryStats.parseStats(parser); statsCollector.add(stats); return stats(stats); case FEED_HAS_NEXT_FIELD_NAME: return hasNext(parser.nextBooleanValue()); default: throw new ClientResponseException("Unknown FeedPage field: " + fieldName); } } } /** * Creates a new {@code Builder} for constructing a {@code FeedPage}. * * @param elementCodec The {@link Codec} used to decode events. * @param statsCollector The {@link StatsCollector} to gather statistics. * @param The type of data contained in each event. * @return A new {@code Builder} instance. */ public static Builder builder(final Codec elementCodec, final StatsCollector statsCollector) { return new Builder<>(elementCodec, statsCollector); } /** * Parses an HTTP response and constructs a {@code FeedPage} instance. * * @param response The {@link HttpResponse} containing the feed data. * @param elementCodec The {@link Codec} used to decode events. * @param statsCollector The {@link StatsCollector} to gather statistics. * @param The type of data contained in each event. * @return The parsed {@code FeedPage}. * @throws ClientResponseException if an error occurs while parsing the feed response. */ public static FeedPage parseResponse(final HttpResponse response, final Codec elementCodec, final StatsCollector statsCollector) { try { if (response.statusCode() >= 400) { QueryResponse.parseResponse(response, elementCodec, statsCollector); } JsonParser parser = JSON_FACTORY.createParser(response.body()); if (parser.nextToken() != START_OBJECT) { throw new ClientResponseException("Invalid event starting with: " + parser.currentToken()); } Builder builder = FeedPage.builder(elementCodec, statsCollector); while (parser.nextToken() == FIELD_NAME) { builder = builder.parseField(parser); } return builder.build(); } catch (IOException e) { throw new ClientResponseException("Error parsing Feed response.", e); } } } ``` ## File: src/main/java/com/fauna/event/FeedRequest.java ```java package com.fauna.event; import com.fasterxml.jackson.core.JsonFactory; import com.fasterxml.jackson.core.JsonGenerator; import com.fauna.client.RequestBuilder; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.nio.charset.StandardCharsets; /** * Represents an event feed request from Fauna. *

* The {@code FeedRequest} class contains an {@link EventSource} and {@link FeedOptions} to * specify the details of the feed request, such as the cursor, start timestamp, and page size. */ public class FeedRequest { private final EventSource source; private final FeedOptions options; /** * Constructs a {@code FeedRequest} with the specified event source and options. * * @param source The {@link EventSource} containing the event source token. * @param options The {@link FeedOptions} specifying additional feed request options. * @throws IllegalArgumentException if {@code source} or {@code options} is null. */ public FeedRequest(final EventSource source, final FeedOptions options) { if (source == null) { throw new IllegalArgumentException("EventSource cannot be null."); } if (options == null) { throw new IllegalArgumentException("FeedOptions cannot be null."); } this.source = source; this.options = options; } /** * Serializes this {@code FeedRequest} to a JSON string. * * @return A {@code String} representation of this feed request in JSON format. * @throws IOException if an error occurs during serialization. */ public String serialize() throws IOException { ByteArrayOutputStream requestBytes = new ByteArrayOutputStream(); JsonGenerator gen = new JsonFactory().createGenerator(requestBytes); gen.writeStartObject(); gen.writeStringField(RequestBuilder.FieldNames.TOKEN, source.getToken()); if (options.getCursor().isPresent()) { gen.writeStringField(RequestBuilder.FieldNames.CURSOR, options.getCursor().get()); } if (options.getStartTs().isPresent()) { gen.writeNumberField(RequestBuilder.FieldNames.START_TS, options.getStartTs().get()); } if (options.getPageSize().isPresent()) { gen.writeNumberField(RequestBuilder.FieldNames.PAGE_SIZE, options.getPageSize().get()); } gen.writeEndObject(); gen.flush(); return requestBytes.toString(StandardCharsets.UTF_8); } /** * Creates a new {@code FeedRequest} from an {@link EventSource}. * * @param resp The {@link EventSource} containing the event source token. * @param options The {@link FeedOptions} specifying additional feed request options. * @return A new {@code FeedRequest} instance based on the response and options. */ public static FeedRequest fromResponse(final EventSource resp, final FeedOptions options) { return new FeedRequest(resp, options); } } ``` ## File: src/main/java/com/fauna/event/package-info.java ```java /** * Provides classes for managing and interacting with event feeds and event streams. * *

This package includes core components and utilities for handling event streaming, * such as options, requests, and response handling mechanisms. * *

    *
  • {@link com.fauna.event.EventSource} - Represents an event source.
  • *
  • {@link com.fauna.event.FaunaEvent} - Defines an event.
  • *
  • {@link com.fauna.event.FaunaStream} - Processes events from an event stream, decoding them into {@code FaunaEvent} instances.
  • *
  • {@link com.fauna.event.FeedIterator} - Enables iteration through pages of events in an event feed.
  • *
  • {@link com.fauna.event.FeedOptions} - Specifies configuration options for managing event feed pagination and timeout.
  • *
  • {@link com.fauna.event.FeedPage} - Represents a paginated events in an event feed, including metadata like cursor and statistics.
  • *
  • {@link com.fauna.event.FeedRequest} - Constructs a request for Fauna's Event feed HTTP API endpoint.
  • *
  • {@link com.fauna.event.StreamOptions} - Specified configuration options for an event stream, such as cursor, retry strategy, and timeout settings.
  • *
  • {@link com.fauna.event.StreamRequest} - Constructs a request for Fauna's Event stream HTTP API endpoint.
  • *
* *

The classes in this package are designed to support Fauna event feeds and event streams. */ package com.fauna.event; ``` ## File: src/main/java/com/fauna/event/StreamOptions.java ```java package com.fauna.event; import com.fauna.client.RetryStrategy; import java.time.Duration; import java.util.Optional; /** * Represents configuration options for a Fauna event stream. *

* The {@code StreamOptions} class allows customization of the stream request, including cursor, * retry strategy, start timestamp, status events, and timeout. */ public class StreamOptions { private final String cursor; private final RetryStrategy retryStrategy; private final Long startTimestamp; private final Boolean statusEvents; private final Duration timeout; /** * Default {@code StreamOptions} instance with defaults. */ public static final StreamOptions DEFAULT = StreamOptions.builder().build(); /** * Constructs a {@code StreamOptions} instance with the specified builder. * * @param builder The {@link Builder} instance containing the configuration options. */ public StreamOptions(final Builder builder) { this.cursor = builder.cursor; this.retryStrategy = builder.retryStrategy; this.startTimestamp = builder.startTimestamp; this.statusEvents = builder.statusEvents; this.timeout = builder.timeout; } /** * Retrieves the event cursor. Used to restart the stream. * * @return An {@link Optional} containing the cursor, or empty if not set. */ public Optional getCursor() { return Optional.ofNullable(cursor); } /** * Retrieves the retry strategy for the stream. * * @return An {@link Optional} containing the retry strategy, or empty if not set. */ public Optional getRetryStrategy() { return Optional.ofNullable(retryStrategy); } /** * Retrieves the start timestamp for the stream. * * @return An {@link Optional} containing the start timestamp, or empty if not set. */ public Optional getStartTimestamp() { return Optional.ofNullable(startTimestamp); } /** * Checks if status events are enabled for the stream. * * @return An {@link Optional} containing a boolean for status events, or empty if not set. */ public Optional getStatusEvents() { return Optional.ofNullable(statusEvents); } /** * Retrieves the timeout duration for the stream. * * @return An {@link Optional} containing the timeout duration, or empty if not set. */ public Optional getTimeout() { return Optional.ofNullable(timeout); } /** * Builder class for constructing {@code StreamOptions} instances. */ public static class Builder { private String cursor = null; private RetryStrategy retryStrategy = null; private Long startTimestamp = null; private Boolean statusEvents = null; private Duration timeout = null; /** * Sets the cursor for the stream. * * @param cursor A {@code String} representing the cursor position. * @return This {@code Builder} instance. */ public Builder cursor(final String cursor) { this.cursor = cursor; return this; } /** * Sets the retry strategy for the stream. * * @param retryStrategy The {@link RetryStrategy} for managing retries. * @return This {@code Builder} instance. */ public Builder retryStrategy(final RetryStrategy retryStrategy) { this.retryStrategy = retryStrategy; return this; } /** * Sets the start timestamp for the stream. * * @param startTimestamp A {@code long} representing the start timestamp. * @return This {@code Builder} instance. */ public Builder startTimestamp(final long startTimestamp) { this.startTimestamp = startTimestamp; return this; } /** * Enables or disables status events for the stream. * * @param statusEvents A {@code Boolean} indicating if status events are enabled. * @return This {@code Builder} instance. */ public Builder statusEvents(final Boolean statusEvents) { this.statusEvents = statusEvents; return this; } /** * Sets the timeout duration for the stream. * * @param timeout A {@link Duration} representing the timeout. * @return This {@code Builder} instance. */ public Builder timeout(final Duration timeout) { this.timeout = timeout; return this; } /** * Builds a new {@code StreamOptions} instance with the configured parameters. * * @return A new {@code StreamOptions} instance. */ public StreamOptions build() { return new StreamOptions(this); } } /** * Creates a new {@code Builder} for constructing {@code StreamOptions}. * * @return A new {@code Builder} instance. */ public static Builder builder() { return new Builder(); } } ``` ## File: src/main/java/com/fauna/event/StreamRequest.java ```java package com.fauna.event; import com.fasterxml.jackson.core.JsonFactory; import com.fasterxml.jackson.core.JsonGenerator; import com.fauna.client.RequestBuilder; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.nio.charset.StandardCharsets; /** * Defines the request body for interacting with the Fauna /stream endpoint. *

* The {@code StreamRequest} class constructs a JSON request body that includes * an {@link EventSource} and {@link StreamOptions} to configure the request parameters. */ public class StreamRequest { private final EventSource source; private final StreamOptions options; /** * Constructs a {@code StreamRequest} with the specified event source and options. * * @param eventSource The {@link EventSource} providing the event source token. * @param streamOptions The {@link StreamOptions} specifying additional request options. * @throws IllegalArgumentException if {@code eventSource} or {@code streamOptions} is null. */ public StreamRequest(final EventSource eventSource, final StreamOptions streamOptions) { if (eventSource == null) { throw new IllegalArgumentException("Event source cannot be null."); } if (streamOptions == null) { throw new IllegalArgumentException("Stream options cannot be null."); } this.source = eventSource; this.options = streamOptions; } /** * Serializes this {@code StreamRequest} to a JSON string for the Fauna /stream endpoint. * *

The JSON includes fields based on the {@link EventSource} and {@link StreamOptions} * configurations. Either the cursor or start timestamp is included, with cursor taking precedence. * * @return A JSON-formatted {@code String} representing this stream request. * @throws IOException if an error occurs during serialization. */ public String serialize() throws IOException { ByteArrayOutputStream requestBytes = new ByteArrayOutputStream(); JsonGenerator gen = new JsonFactory().createGenerator(requestBytes); gen.writeStartObject(); gen.writeStringField(RequestBuilder.FieldNames.TOKEN, source.getToken()); // Prefer cursor if present, otherwise use start timestamp. if (options.getCursor().isPresent()) { gen.writeStringField(RequestBuilder.FieldNames.CURSOR, options.getCursor().get()); } else if (options.getStartTimestamp().isPresent()) { gen.writeNumberField(RequestBuilder.FieldNames.START_TS, options.getStartTimestamp().get()); } gen.writeEndObject(); gen.flush(); return requestBytes.toString(StandardCharsets.UTF_8); } } ``` ## File: src/main/java/com/fauna/exception/AbortException.java ```java package com.fauna.exception; import com.fauna.response.QueryFailure; import java.util.HashMap; import java.util.Map; /** * An exception that represents an abort error in Fauna. * This exception extends {@link ServiceException} and includes methods to retrieve * the abort data. */ public class AbortException extends ServiceException { @SuppressWarnings("rawtypes") private final Map decoded = new HashMap<>(); /** * Constructs a new {@code AbortException} with the specified {@link QueryFailure} response. * * @param response The {@code QueryFailure} object containing details about the aborted query. */ public AbortException(final QueryFailure response) { super(response); } /** * Returns the abort data as a top-level {@code Object}. This is primarily useful for debugging * or situations where the type of abort data may be unknown. * * @return An {@code Object} containing the abort data, or {@code null} if no data is present. */ public Object getAbort() { return getAbort(Object.class); } /** * Returns the abort data decoded into the specified class, or {@code null} if there is no abort data. * The abort data is cached upon retrieval to avoid redundant decoding. * * @param clazz The {@code Class} to decode the abort data into. * @param The type of the abort data. * @return The decoded abort data of type {@code T}, or {@code null} if no data is available. */ public T getAbort(final Class clazz) { if (!decoded.containsKey(clazz)) { Object abortData = getResponse().getAbort(clazz).orElse(null); decoded.put(clazz, abortData); } //noinspection unchecked return (T) decoded.get(clazz); } } ``` ## File: src/main/java/com/fauna/exception/AuthenticationException.java ```java package com.fauna.exception; import com.fauna.response.QueryFailure; /** * Exception thrown when an authentication error occurs in Fauna. * This typically indicates an issue with the authentication secret used for Fauna requests. *

* Extends {@link ServiceException} and provides access to detailed failure information through the * {@link QueryFailure} response. */ public class AuthenticationException extends ServiceException { /** * Constructs a new {@code AuthenticationException} with the specified {@code QueryFailure} response. * * @param response The {@code QueryFailure} object containing details about the authentication failure. */ public AuthenticationException(final QueryFailure response) { super(response); } } ``` ## File: src/main/java/com/fauna/exception/AuthorizationException.java ```java package com.fauna.exception; import com.fauna.response.QueryFailure; /** * Exception thrown when an authorization error occurs in Fauna. * This typically indicates that the Fauna authentication secret does not have permissions * required to perform the requested operation. *

* Extends {@link ServiceException} and provides access to detailed failure information through * the {@link QueryFailure} response. */ public class AuthorizationException extends ServiceException { /** * Constructs a new {@code AuthorizationException} with the specified {@code QueryFailure} response. * * @param response The {@code QueryFailure} object containing details about the authorization failure. */ public AuthorizationException(final QueryFailure response) { super(response); } } ``` ## File: src/main/java/com/fauna/exception/ClientException.java ```java package com.fauna.exception; /** * Exception representing client-side errors in Fauna. *

* This exception is typically thrown when there is an issue with client configuration, * request formation, or any other client-specific error that does not originate from Fauna. * Extends {@link FaunaException} to provide detailed information about the error. */ public class ClientException extends FaunaException { /** * Constructs a new {@code ClientException} with the specified detail message. * * @param message A {@code String} describing the reason for the client error. */ public ClientException(final String message) { super(message); } /** * Constructs a new {@code ClientException} with the specified detail message and cause. * * @param message A {@code String} describing the reason for the client error. * @param cause The underlying {@code Throwable} cause of the error. */ public ClientException(final String message, final Throwable cause) { super(message, cause); } } ``` ## File: src/main/java/com/fauna/exception/ClientRequestException.java ```java package com.fauna.exception; /** * Exception representing errors related to client requests in Fauna. *

* This exception is thrown when there is an issue with the structure or content of a request * sent from the client, such as invalid parameters or improperly formatted data. * Extends {@link ClientException} to provide information specific to request-related errors. */ public class ClientRequestException extends ClientException { /** * Constructs a new {@code ClientRequestException} with the specified detail message. * * @param message A {@code String} describing the reason for the client request error. */ public ClientRequestException(final String message) { super(message); } /** * Constructs a new {@code ClientRequestException} with the specified detail message and cause. * * @param message A {@code String} describing the reason for the client request error. * @param cause The underlying {@code Throwable} cause of the error. */ public ClientRequestException(final String message, final Throwable cause) { super(message, cause); } } ``` ## File: src/main/java/com/fauna/exception/ClientResponseException.java ```java package com.fauna.exception; import java.text.MessageFormat; /** * Exception representing errors in the client's response handling. *

* This exception is typically thrown when there is an issue with the response received from * Fauna, including unexpected status codes or other response-related errors. * Extends {@link ClientException} to provide information specific to response handling errors. */ public class ClientResponseException extends ClientException { /** * Constructs a new {@code ClientResponseException} with the specified detail message. * * @param message A {@code String} describing the reason for the client response error. */ public ClientResponseException(final String message) { super(message); } /** * Constructs a new {@code ClientResponseException} with a formatted message * based on the provided status code and message. * * @param message A {@code String} describing the reason for the client response error. * @param statusCode An {@code int} representing the HTTP status code received. * @return A formatted message string. */ private static String buildMessage(final String message, final int statusCode) { return MessageFormat.format("ClientResponseException HTTP {0}: {1}", statusCode, message); } /** * Constructs a new {@code ClientResponseException} with the specified detail message, cause, and status code. * * @param message A {@code String} describing the reason for the client response error. * @param exc The underlying {@code Throwable} cause of the error. * @param statusCode An {@code int} representing the HTTP status code received. */ public ClientResponseException(final String message, final Throwable exc, final int statusCode) { super(buildMessage(message, statusCode), exc); } /** * Constructs a new {@code ClientResponseException} with the specified detail message and cause. * * @param message A {@code String} describing the reason for the client response error. * @param cause The underlying {@code Throwable} cause of the error. */ public ClientResponseException(final String message, final Throwable cause) { super(message, cause); } } ``` ## File: src/main/java/com/fauna/exception/CodecException.java ```java package com.fauna.exception; import java.io.IOException; /** * Exception representing errors encountered during encoding or decoding operations. *

* This exception is typically thrown when an encoding or decoding error occurs within the Fauna * client, such as an {@link IOException} while reading or writing data. * Extends {@link FaunaException} to provide detailed information about codec-related errors. */ public class CodecException extends FaunaException { /** * Constructs a new {@code CodecException} with the specified detail message. * * @param message A {@code String} describing the reason for the codec error. */ public CodecException(final String message) { super(message); } /** * Constructs a new {@code CodecException} with the specified detail message and cause. * * @param message A {@code String} describing the reason for the codec error. * @param cause The underlying {@code Throwable} cause of the error. */ public CodecException(final String message, final Throwable cause) { super(message, cause); } /** * Creates a new {@code CodecException} specifically for decoding {@link IOException}s. * * @param exc The {@code IOException} encountered during decoding. * @return A {@code CodecException} describing the decoding error. */ public static CodecException decodingIOException(final IOException exc) { return new CodecException("IOException while decoding.", exc); } /** * Creates a new {@code CodecException} specifically for encoding {@link IOException}s. * * @param exc The {@code IOException} encountered during encoding. * @return A {@code CodecException} describing the encoding error. */ public static CodecException encodingIOException(final IOException exc) { return new CodecException("IOException while encoding.", exc); } } ``` ## File: src/main/java/com/fauna/exception/ConstraintFailureException.java ```java package com.fauna.exception; import com.fauna.response.ConstraintFailure; import com.fauna.response.QueryFailure; /** * Exception representing a constraint failure in a Fauna query. *

* This exception is typically thrown when a query violates a collection's check * or unique constraints. * Extends {@link ServiceException} and provides access to details about the constraint failures. */ public class ConstraintFailureException extends ServiceException { /** * Constructs a new {@code ConstraintFailureException} with the specified {@code QueryFailure}. * * @param failure The {@code QueryFailure} object containing details about the constraint failure. */ public ConstraintFailureException(final QueryFailure failure) { super(failure); } /** * Retrieves an array of {@link ConstraintFailure} objects representing the individual constraint failures. * * @return An array of {@code ConstraintFailure} objects, or {@code null} if no constraint failures are present. */ public ConstraintFailure[] getConstraintFailures() { return getResponse().getConstraintFailures().orElse(null); } } ``` ## File: src/main/java/com/fauna/exception/ContendedTransactionException.java ```java package com.fauna.exception; import com.fauna.response.QueryFailure; /** * An exception indicating that too much transaction contention occurred while executing a query. *

* This exception is thrown when a transaction cannot proceed due to conflicts or contention * with other concurrent transactions. *

* Extends {@link ServiceException} to provide detailed information about the failed query. * * @see ServiceException * @see QueryFailure */ public class ContendedTransactionException extends ServiceException { /** * Constructs a new {@code ContendedTransactionException} with the specified {@code QueryFailure} response. * * @param response The {@code QueryFailure} object containing details about the failed query. */ public ContendedTransactionException(final QueryFailure response) { super(response); } } ``` ## File: src/main/java/com/fauna/exception/ErrorHandler.java ```java package com.fauna.exception; import com.fauna.response.QueryFailure; import static java.net.HttpURLConnection.HTTP_BAD_REQUEST; import static java.net.HttpURLConnection.HTTP_CONFLICT; import static java.net.HttpURLConnection.HTTP_FORBIDDEN; import static java.net.HttpURLConnection.HTTP_INTERNAL_ERROR; import static java.net.HttpURLConnection.HTTP_UNAUTHORIZED; import static java.net.HttpURLConnection.HTTP_UNAVAILABLE; /** * Provides error handling based on error codes and HTTP status codes returned by Fauna. *

* The {@code ErrorHandler} class contains a static method to manage various error scenarios * by analyzing the HTTP status code and specific error codes, mapping them to relevant exceptions. */ public final class ErrorHandler { private static final String INVALID_QUERY = "invalid_query"; private static final String LIMIT_EXCEEDED = "limit_exceeded"; private static final String INVALID_REQUEST = "invalid_request"; private static final String ABORT = "abort"; private static final String CONSTRAINT_FAILURE = "constraint_failure"; private static final String UNAUTHORIZED = "unauthorized"; private static final String FORBIDDEN = "forbidden"; private static final String CONTENDED_TRANSACTION = "contended_transaction"; private static final String TIME_OUT = "time_out"; private static final String INTERNAL_ERROR = "internal_error"; private static final int HTTP_LIMIT_EXCEEDED = 429; private static final int HTTP_TIME_OUT = 440; private ErrorHandler() { } /** * Handles errors based on the HTTP status code and error code returned by Fauna. * * @param statusCode The HTTP status code received from Fauna. * @param failure The {@link QueryFailure} object containing details about the failure. * @throws AbortException Thrown if the transaction was aborted. * @throws AuthenticationException Thrown if authentication credentials are invalid or missing. * @throws AuthorizationException Thrown if authorization credentials are invalid or insufficient. * @throws ConstraintFailureException Thrown if the transaction failed a database constraint check. * @throws ContendedTransactionException Thrown if too much contention occurred during a transaction. * @throws InvalidRequestException Thrown if the request body does not conform to API specifications. * @throws QueryCheckException Thrown if the query failed validation checks. * @throws QueryRuntimeException Thrown if the query encountered a runtime error. * @throws QueryTimeoutException Thrown if the query exceeded the specified timeout. * @throws ServiceInternalException Thrown if an unexpected server error occurred. * @throws ThrottlingException Thrown if the query exceeded capacity limits. */ public static void handleQueryFailure( final int statusCode, final QueryFailure failure) { switch (statusCode) { case HTTP_BAD_REQUEST: switch (failure.getErrorCode()) { case INVALID_QUERY: throw new QueryCheckException(failure); case LIMIT_EXCEEDED: throw new ThrottlingException(failure); case INVALID_REQUEST: throw new InvalidRequestException(failure); case ABORT: throw new AbortException(failure); case CONSTRAINT_FAILURE: throw new ConstraintFailureException(failure); default: throw new QueryRuntimeException(failure); } case HTTP_UNAUTHORIZED: if (UNAUTHORIZED.equals(failure.getErrorCode())) { throw new AuthenticationException(failure); } case HTTP_FORBIDDEN: if (FORBIDDEN.equals(failure.getErrorCode())) { throw new AuthorizationException(failure); } case HTTP_CONFLICT: if (CONTENDED_TRANSACTION.equals(failure.getErrorCode())) { throw new ContendedTransactionException(failure); } case HTTP_LIMIT_EXCEEDED: if (LIMIT_EXCEEDED.equals(failure.getErrorCode())) { throw new ThrottlingException(failure); } case HTTP_TIME_OUT: case HTTP_UNAVAILABLE: if (TIME_OUT.equals(failure.getErrorCode())) { throw new QueryTimeoutException(failure); } case HTTP_INTERNAL_ERROR: if (INTERNAL_ERROR.equals(failure.getErrorCode())) { throw new ServiceInternalException(failure); } } } } ``` ## File: src/main/java/com/fauna/exception/FaunaException.java ```java package com.fauna.exception; /** * Represents a general exception for errors encountered within the Fauna client. * This exception serves as the base class for other specific exceptions in the drive. */ public class FaunaException extends RuntimeException { /** * Constructs a new {@code FaunaException} with the specified message. * * @param message A {@code String} describing the reason for the exception. */ public FaunaException(final String message) { super(message); } /** * Constructs a new {@code FaunaException} with the specified detail message and cause. * * @param message A {@code String} describing the reason for the exception. * @param err The underlying {@code Throwable} cause of the exception. */ public FaunaException(final String message, final Throwable err) { super(message, err); } } ``` ## File: src/main/java/com/fauna/exception/InvalidRequestException.java ```java package com.fauna.exception; import com.fauna.response.QueryFailure; /** * Exception representing an invalid query request. *

* This exception is thrown when a request sent to Fauna does not conform to the API specifications, * typically due to malformed data, incorrect parameters, or other request-related issues. * Extends {@link ServiceException} to provide specific details about the invalid request. */ public class InvalidRequestException extends ServiceException { /** * Constructs a new {@code InvalidRequestException} with the specified {@code QueryFailure} response. * * @param response The {@code QueryFailure} object containing details about the invalid request. */ public InvalidRequestException(final QueryFailure response) { super(response); } } ``` ## File: src/main/java/com/fauna/exception/NullDocumentException.java ```java package com.fauna.exception; import com.fauna.types.Module; /** * Exception representing a null document error in Fauna. *

* This exception is thrown when a document is null in Fauna, providing details about * the document ID, its collection, and the reason it is null. * Extends {@link FaunaException} to provide information specific to null document scenarios. */ public class NullDocumentException extends FaunaException { private final String id; private final Module coll; private final String nullCause; /** * Constructs a new {@code NullDocumentException} with the specified document ID, collection, and cause. * * @param id The ID of the null document. * @param coll The {@link Module} representing the collection of the document. * @param nullCause A {@code String} describing the reason the document is null. */ public NullDocumentException(final String id, final Module coll, final String nullCause) { super(String.format("Document %s in collection %s is null: %s", id, coll != null ? coll.getName() : "unknown", nullCause)); this.id = id; this.coll = coll; this.nullCause = nullCause; } /** * Retrieves the ID of the null document. * * @return A {@code String} representing the document ID. */ public String getId() { return id; } /** * Retrieves the collection associated with the null document. * * @return A {@link Module} representing the document's collection, or {@code null} if unknown. */ public Module getCollection() { return coll; } /** * Retrieves the cause for the document being null. * * @return A {@code String} describing why the document is null. */ public String getNullCause() { return nullCause; } } ``` ## File: src/main/java/com/fauna/exception/ProtocolException.java ```java package com.fauna.exception; import com.fauna.response.QueryFailure; import java.text.MessageFormat; import java.util.Optional; /** * Exception representing protocol-level errors in communication with Fauna. *

* This exception is typically thrown when there is an unexpected shape is received on response.. * Extends {@link FaunaException} to provide details specific to protocol errors. */ public class ProtocolException extends FaunaException { private final int statusCode; private final QueryFailure queryFailure; private final String body; /** * Constructs a {@code ProtocolException} with the specified HTTP status code and {@code QueryFailure} details. * * @param statusCode The HTTP status code received. * @param failure The {@link QueryFailure} object containing details about the protocol failure. */ public ProtocolException(final int statusCode, final QueryFailure failure) { super(MessageFormat.format("ProtocolException HTTP {0}", statusCode)); this.statusCode = statusCode; this.queryFailure = failure; this.body = null; } /** * Constructs a {@code ProtocolException} with the specified HTTP status code and response body. * * @param statusCode The HTTP status code received. * @param body A {@code String} containing the response body associated with the failure. */ public ProtocolException(final int statusCode, final String body) { super(buildMessage(statusCode)); this.statusCode = statusCode; this.body = body; this.queryFailure = null; } /** * Builds a formatted error message based on the HTTP status code. * * @param statusCode The HTTP status code received. * @return A formatted {@code String} message for the protocol error. */ private static String buildMessage(final int statusCode) { return MessageFormat.format("ProtocolException HTTP {0}", statusCode); } /** * Retrieves the HTTP status code associated with this protocol error. * * @return An {@code int} representing the HTTP status code. */ public int getStatusCode() { return this.statusCode; } /** * Retrieves the response body associated with this protocol error, if available. * * @return A {@code String} containing the response body, or {@code null} if the body is unavailable. */ public String getBody() { return this.body; } /** * Retrieves the {@link QueryFailure} details associated with this protocol error, if available. * * @return An {@code Optional} containing the failure details, or {@code Optional.empty()} if not * present. */ public Optional getQueryFailure() { return Optional.ofNullable(this.queryFailure); } } ``` ## File: src/main/java/com/fauna/exception/QueryCheckException.java ```java package com.fauna.exception; import com.fauna.response.QueryFailure; /** * Exception representing a query validation error in Fauna. *

* This exception is thrown when a query fails one or more validation checks in Fauna, * indicating issues with the query's syntax, or other query validation prior to execution. * Extends {@link ServiceException} to provide information specific to query validation errors. */ public class QueryCheckException extends ServiceException { /** * Constructs a new {@code QueryCheckException} with the specified {@code QueryFailure} details. * * @param failure The {@link QueryFailure} object containing details about the validation failure. */ public QueryCheckException(final QueryFailure failure) { super(failure); } } ``` ## File: src/main/java/com/fauna/exception/QueryRuntimeException.java ```java package com.fauna.exception; import com.fauna.response.QueryFailure; /** * Exception representing a runtime error encountered during query execution in Fauna. *

* This exception is thrown when a query fails due to a runtime error. * Extends {@link ServiceException} to provide details specific to runtime query errors. */ public class QueryRuntimeException extends ServiceException { /** * Constructs a new {@code QueryRuntimeException} with the specified {@code QueryFailure} details. * * @param response The {@link QueryFailure} object containing details about the runtime error. */ public QueryRuntimeException(final QueryFailure response) { super(response); } } ``` ## File: src/main/java/com/fauna/exception/QueryTimeoutException.java ```java package com.fauna.exception; import com.fauna.response.QueryFailure; /** * Exception representing a timeout error encountered during query execution in Fauna. *

* This exception is thrown when a query fails to complete within the specified time limit, * indicating that the query timeout was exceeded. * Extends {@link ServiceException} to provide details specific to query timeout errors. */ public class QueryTimeoutException extends ServiceException { /** * Constructs a new {@code QueryTimeoutException} with the specified {@code QueryFailure} details. * * @param response The {@link QueryFailure} object containing details about the timeout error. */ public QueryTimeoutException(final QueryFailure response) { super(response); } } ``` ## File: src/main/java/com/fauna/exception/RetryableException.java ```java package com.fauna.exception; /** * Marker interface for exceptions that indicate a retryable operation. * Exceptions implementing this interface suggest that the operation may be retried, * as the error might be transient or recoverable. * *

This interface allows for easy identification of retryable exceptions in Fauna.

*/ public interface RetryableException { } ``` ## File: src/main/java/com/fauna/exception/ServiceException.java ```java package com.fauna.exception; import com.fauna.response.QueryFailure; import com.fauna.response.QueryStats; import java.util.Map; import java.util.Optional; /** * An exception representing a query failure returned by Fauna. * *

This exception extends {@link FaunaException} and provides detailed information * about the failed query, including HTTP status codes, error codes, statistics, * and other metadata.

*/ public class ServiceException extends FaunaException { private final QueryFailure response; /** * Constructs a new {@code ServiceException} with the specified {@code QueryFailure} response. * * @param response The {@code QueryFailure} object containing details about the failed query. */ public ServiceException(final QueryFailure response) { super(response.getFullMessage()); this.response = response; } /** * Returns the {@link QueryFailure} response associated with this exception. * * @return The {@code QueryFailure} object containing details of the query failure. */ public QueryFailure getResponse() { return this.response; } /** * Returns the HTTP status code of the response returned by the query request. * * @return The HTTP status code as an integer. */ public int getStatusCode() { return this.response.getStatusCode(); } /** * Returns the * Fauna error code associated with the failure. * *

Fauna error codes indicate the specific cause of the error and are part of the API contract, * allowing for programmatic logic based on the error type.

* * @return The error code as a {@code String}. */ public String getErrorCode() { return this.response.getErrorCode(); } /** * Returns a brief summary of the error. * * @return A {@code String} containing the error summary. */ public String getSummary() { return this.response.getSummary(); } /** * Returns the statistics associated with the failed query. * * @return A {@link QueryStats} object containing statistical information for the failed query. */ public QueryStats getStats() { return this.response.getStats(); } /** * Returns the last transaction timestamp seen for the failed query, if available. * * @return An {@code Optional} representing the last transaction timestamp, or {@code Optional.empty()} if not available. */ public Optional getTxnTs() { return Optional.ofNullable(this.response.getLastSeenTxn()); } /** * Returns the schema version used during query execution. * * @return The schema version as a {@code Long} value. */ public Long getSchemaVersion() { return this.response.getSchemaVersion(); } /** * Returns a map of query tags for the failed query, containing key-value pairs of tags. * * @return A {@code Map} with query tags. */ public Map getQueryTags() { return this.response.getQueryTags(); } } ``` ## File: src/main/java/com/fauna/exception/ServiceInternalException.java ```java package com.fauna.exception; import com.fauna.response.QueryFailure; /** * Exception representing an unexpected internal server error in Fauna. *

* This exception is thrown when Fauna encounters an unexpected internal error that prevents * it from completing a request, typically indicating a server-side issue. * Extends {@link ServiceException} to provide details specific to internal server errors. */ public class ServiceInternalException extends ServiceException { /** * Constructs a new {@code ServiceInternalException} with the specified {@code QueryFailure} details. * * @param response The {@link QueryFailure} object containing details about the internal server error. */ public ServiceInternalException(final QueryFailure response) { super(response); } } ``` ## File: src/main/java/com/fauna/exception/ThrottlingException.java ```java package com.fauna.exception; import com.fauna.response.QueryFailure; /** * Exception representing a throttling error in Fauna, indicating that a query exceeded * plan throughput limits. *

* It implements {@link RetryableException} * Extends {@link ServiceException} to provide details specific to throttling errors. */ public class ThrottlingException extends ServiceException implements RetryableException { /** * Constructs a new {@code ThrottlingException} with the specified {@code QueryFailure} details. * * @param response The {@link QueryFailure} object containing details about the throttling error. */ public ThrottlingException(final QueryFailure response) { super(response); } } ``` ## File: src/main/java/com/fauna/mapping/FieldInfo.java ```java package com.fauna.mapping; import com.fauna.codec.Codec; import com.fauna.codec.CodecProvider; import java.lang.reflect.Field; import java.lang.reflect.Type; /** * Represents metadata for a Fauna document field in a class, including its name, type, associated codec, * and other properties used for serialization and deserialization. */ public final class FieldInfo { private final String name; private final CodecProvider provider; private final FieldType fieldType; private final Class clazz; private final Type[] genericTypeArgs; private final Field field; private Codec codec; /** * Constructs a {@code FieldInfo} object with the specified field metadata. * * @param field The field represented by this {@code FieldInfo} instance. * @param name The name of the field. * @param clazz The class type of the field. * @param genericTypeArgs An array of generic type arguments for the field, if any. * @param provider The {@link CodecProvider} used to obtain a codec for this field. * @param fieldType The {@link FieldType} of the field. */ public FieldInfo( final Field field, final String name, final Class clazz, final Type[] genericTypeArgs, final CodecProvider provider, final FieldType fieldType) { this.field = field; this.name = name; this.clazz = clazz; this.genericTypeArgs = genericTypeArgs; this.provider = provider; this.fieldType = fieldType; } /** * Retrieves the name of the field. * * @return A {@code String} representing the field name. */ public String getName() { return name; } /** * Retrieves the class type of the field. * * @return A {@code Class} representing the field's class type. */ public Class getType() { return clazz; } /** * Retrieves the {@link FieldType} of this field. * * @return The {@code FieldType} associated with this field. */ public FieldType getFieldType() { return fieldType; } /** * Retrieves the codec used to serialize and deserialize the field. If the codec is not already set, * it will be retrieved from the {@link CodecProvider} and cached. * * @return A {@code Codec} instance associated with the field type. */ @SuppressWarnings("rawtypes") public Codec getCodec() { if (codec != null) { return codec; } synchronized (this) { // Double-checked locking to ensure thread-safe lazy initialization if (codec != null) { return codec; } codec = provider.get(clazz, genericTypeArgs); } return codec; } /** * Retrieves the {@code Field} object representing this field in the class. * * @return The {@code Field} associated with this {@code FieldInfo}. */ public Field getField() { return field; } } ``` ## File: src/main/java/com/fauna/mapping/FieldName.java ```java package com.fauna.mapping; /** * Utility class for handling field names. */ public final class FieldName { private FieldName() { } /** * Converts the given field name to a canonical format where the first character is lowercase. * If the name is null, empty, or already starts with a lowercase letter, it is unchanged. * * @param name The field name to be converted. * @return The canonicalized field name, or the original name if it is null, empty, or already starts with a * lowercase letter. */ public static String canonical(final String name) { if (name == null || name.isEmpty() || Character.isLowerCase(name.charAt(0))) { return name; } else { return Character.toLowerCase(name.charAt(0)) + name.substring(1); } } } ``` ## File: src/main/java/com/fauna/mapping/FieldType.java ```java package com.fauna.mapping; /** * Enum representing the different types of fields that can be used in Fauna document field mappings. * Each field type specifies a distinct role or characteristic of a field in the mapping process. */ public enum FieldType { /** * Represents a client-generated document ID. * Typically used when the ID is provided by the client and not generated by Fauna. */ ClientGeneratedId, /** * Represents a Fauna-generated document ID. * Typically used for document IDs auto-generated by Fauna. */ ServerGeneratedId, /** * Represents the document's `coll` (collection) metadata field. * Used to denote the collection to which the document belongs. */ Coll, /** * Represents the document's `ts` (timestamp) metadata field. * Used to track the last write to the document. */ Ts, /** * Represents a user-defined document field. */ Field, } ``` ## File: src/main/java/com/fauna/mapping/package-info.java ```java /** * The {@code com.fauna.mapping} package provides classes and utilities used to * map Fauna document fields for serialization and deserialization in the client. * *

The classes in this package include: *

    *
  • {@link com.fauna.mapping.FieldInfo}: Holds metadata for individual fields, such as name, * type, and codec, used to map and handle fields within a Fauna data model.
  • * *
  • {@link com.fauna.mapping.FieldName}: Provides utility methods for handling field names, * including a method to convert names to a canonical format.
  • * *
  • {@link com.fauna.mapping.FieldType}: Defines various field types that can exist within * Fauna mappings, such as identifiers, timestamps, and general-purpose fields.
  • *
*/ package com.fauna.mapping; ``` ## File: src/main/java/com/fauna/query/builder/package-info.java ```java /** * Classes for building queries with Fauna. */ package com.fauna.query.builder; ``` ## File: src/main/java/com/fauna/query/builder/Query.java ```java package com.fauna.query.builder; import com.fauna.query.template.FaunaTemplate; import java.util.Map; import java.util.Objects; import java.util.Spliterator; import java.util.stream.StreamSupport; /** * Represents a Fauna query that is constructed from multiple query fragments. This class enables the creation of * queries from both literal strings and variable placeholders. */ @SuppressWarnings("rawtypes") public class Query extends QueryFragment { private final QueryFragment[] fql; /** * Constructs a Query instance based on the given template string and variable arguments. * * @param query A Fauna Query Language (FQL) v10 template string containing both literals and variable placeholders. * Placeholders should follow the syntax defined by {@link FaunaTemplate}. * @param args A map of variable names to their corresponding values. This map provides the values that are * substituted for placeholders in the template string. * @throws IllegalArgumentException if any placeholder in the template string lacks a matching entry in * {@code args}. */ public Query(final String query, final Map args) throws IllegalArgumentException { Spliterator iter = new FaunaTemplate(query).spliterator(); this.fql = StreamSupport.stream(iter, true).map( part -> { Map foo = Objects.requireNonNullElse(args, Map.of()); return part.toFragment(foo); }).toArray(QueryFragment[]::new); } /** * Creates a Query instance based on a template string and a set of arguments. The template string can contain both * literals and variable placeholders, allowing for dynamic query construction. * * @param query A Fauna Query Language (FQL) v10 template string. It may contain variables designated by * placeholders. * @param args A map associating variable names with their values for substitution within the query. If * {@code null}, no variables will be substituted. * @return a Query instance representing the constructed query. * @throws IllegalArgumentException if any placeholder in the template string lacks a corresponding entry in * {@code args}. */ public static Query fql( final String query, final Map args) throws IllegalArgumentException { return new Query(query, args); } /** * Creates a Query instance based solely on a template string without any arguments. The template string should * contain only literals since no variable values are provided. * * @param query A Fauna Query Language (FQL) v10 template string. This version of the template should contain no * placeholders, as there are no arguments for substitution. * @return a Query instance representing the constructed query. * @throws IllegalArgumentException if the template contains placeholders without a matching entry in the provided * arguments. */ public static Query fql(final String query) throws IllegalArgumentException { return fql(query, null); } /** * Retrieves the list of fragments that compose this query, where each fragment is either a literal or a variable. * * @return an array of QueryFragment instances representing the parts of the query. */ @Override public QueryFragment[] get() { return this.fql; } } ``` ## File: src/main/java/com/fauna/query/builder/QueryArr.java ```java package com.fauna.query.builder; import java.util.List; import java.util.Objects; /** * Represents a special type that allows Fauna to evaluate an array of individual * queries, each of which will be processed, and its result will be an element * in the returned array. *

* Example usage: *

 *   var listOfQueries = List.of(fql("1 + 1"), fql("2 + 2"), fql("3 + 3"));
 * 
* Directly providing this list to a query will fail because it would be treated * as a {@code QueryVal}. By wrapping it in a {@code QueryArr}, each query within the * list will be evaluated separately: *
 *   client.query(fql("${queries}", Map.of("queries", listOfQueries)));
 *   // Error: the list is treated as a single value.
 *
 *   client.query(fql("${queries}", Map.of("queries", QueryArr.of(listOfQueries)));
 *   // Returns: [2, 4, 6]
 * 
* * @param The type of elements in the QueryArr, which must be a subtype of * {@link QueryFragment}. */ @SuppressWarnings("rawtypes") public final class QueryArr extends QueryFragment> { private final List value; /** * Static factory method to create a new {@code QueryArr} instance. * * @param val the list of {@link QueryFragment} elements to wrap. * @param the type of elements in the list, which must extend {@link QueryFragment}. * @return a new instance of {@code QueryArr} encapsulating the provided list. */ public static QueryArr of(final List val) { return new QueryArr<>(val); } /** * Constructs a {@code QueryArr} with the specified list of query fragments. * * @param value the list of query fragments to encapsulate. */ public QueryArr(final List value) { this.value = value; } /** * Retrieves the encapsulated list of query fragments in this {@code QueryArr}. * * @return the list of query fragments. */ @Override public List get() { return value; } /** * Checks if this {@code QueryArr} is equal to another object. * Two {@code QueryArr} objects are equal if they contain the same list of query fragments. * * @param o the object to compare with. * @return {@code true} if the specified object is equal to this {@code QueryArr}; * {@code false} otherwise. */ @Override public boolean equals(final Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } QueryArr that = (QueryArr) o; return Objects.equals(value, that.value); } /** * Returns the hash code of this {@code QueryArr}, based on its encapsulated list of query fragments. * * @return the hash code of this object. */ @Override public int hashCode() { return value != null ? value.hashCode() : 0; } /** * Retrieves the encapsulated list directly. * * @return the encapsulated list of query fragments. */ public List getValue() { return this.value; } } ``` ## File: src/main/java/com/fauna/query/builder/QueryFragment.java ```java package com.fauna.query.builder; /** * An abstract class serving as a base for different types of query fragments. * @param The type of QueryFragment. */ public abstract class QueryFragment { /** * Retrieves the value represented by this fragment. * * @return the value of this fragment. */ public abstract T get(); } ``` ## File: src/main/java/com/fauna/query/builder/QueryLiteral.java ```java package com.fauna.query.builder; import com.fasterxml.jackson.annotation.JsonValue; import java.util.Objects; /** * Represents a literal fragment of a Fauna query. * This class encapsulates a fixed string that does not contain any variables. * A {@code QueryLiteral} is used to represent literal values in a query. */ @SuppressWarnings("rawtypes") public final class QueryLiteral extends QueryFragment { private final String value; /** * Constructs a new {@code QueryLiteral} with the given literal value. * * @param value the string value of this fragment; must not be null. * @throws IllegalArgumentException if {@code value} is null. */ public QueryLiteral(final String value) { if (value == null) { throw new IllegalArgumentException( "A literal value must not be null"); } this.value = value; } /** * Retrieves the string value of this literal fragment. * * @return the string value of this fragment. */ @Override public String get() { return value; } @Override public boolean equals(final Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } QueryLiteral that = (QueryLiteral) o; return Objects.equals(value, that.value); } @Override public int hashCode() { return value.hashCode(); } /** * Gets the wrapped literal value. * * @return The literal value as a string. */ @JsonValue public String getValue() { return this.value; } } ``` ## File: src/main/java/com/fauna/query/builder/QueryObj.java ```java package com.fauna.query.builder; import java.util.Map; import java.util.Objects; /** * This class represents a special type of query fragment that allows users * to provide Fauna with an object whose values are individual queries. * Each of these queries will be evaluated, and the result of each query * will be a value in the returned object. * *

* Example usage: * Given a map of queries: *

 *   var o = Map.of("key1", fql("1 + 1"));
 * 
* If this map is passed directly to a query, it will fail because the entire * object will be treated as a value. However, if you wrap the map in a * {@code QueryObj}, Fauna will evaluate each query. *
 *   client.query(fql("${obj}", Map.of("obj", o));
 *   // Error: the map is treated as a value.
 *
 *   client.query(fql("${obj}", Map.of("obj", QueryObj.of(o)))
 *   // Result: { "key1": 2 }
 * 
* * @param The type of {@code QueryObj}. Must be a subtype of {@code QueryFragment}. */ @SuppressWarnings("rawtypes") public final class QueryObj extends QueryFragment> { /** * Creates a new {@code QueryObj} instance with the specified map of query fragments. * * @param val the map of query fragments to wrap. * @param The map value type, which must extend {@code QueryFragment}. * @return a new {@code QueryObj} instance wrapping the provided map. */ public static QueryObj of(final Map val) { //noinspection unchecked return new QueryObj(val); } private final Map value; /** * Constructs a new {@code QueryObj} with the given map of query fragments. * * @param value the map to encapsulate. */ public QueryObj(final Map value) { this.value = value; } /** * Retrieves the encapsulated map of query fragments that make up this query object. * * @return the map of query fragments. */ @Override public Map get() { return value; } @Override public boolean equals(final Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } QueryObj that = (QueryObj) o; return Objects.equals(value, that.value); } @Override public int hashCode() { return value != null ? value.hashCode() : 0; } /** * Retrieves the wrapped map value. * * @return the encapsulated map. */ public Map getValue() { return this.value; } } ``` ## File: src/main/java/com/fauna/query/builder/QueryVal.java ```java package com.fauna.query.builder; import java.util.Objects; /** * Represents a value fragment of a Fauna query. * This class encapsulates a value that can be a variable in the query, such as a literal value or a reference. * The value can be any object type and will be substituted into the query at runtime. * * @param The type of the value in the fragment, which can be any object. */ public final class QueryVal extends QueryFragment { private final T value; /** * Constructs a QueryVal with the specified value. * * @param value the value to encapsulate, which can be any object. * It can represent a literal value or a reference to be used in the query. */ public QueryVal(final T value) { this.value = value; } /** * Retrieves the encapsulated value of this fragment. * * @return the value contained within this fragment. */ @Override public T get() { return value; } /** * Compares this QueryVal to another object for equality. * Two QueryVal objects are considered equal if their encapsulated values are equal. * * @param o the object to compare to. * @return {@code true} if this QueryVal is equal to the other object, otherwise {@code false}. */ @Override public boolean equals(final Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } @SuppressWarnings("unchecked") QueryVal that = (QueryVal) o; return Objects.equals(value, that.value); } /** * Returns the hash code for this QueryVal. * The hash code is computed based on the encapsulated value. * * @return the hash code of this QueryVal. */ @Override public int hashCode() { return value != null ? value.hashCode() : 0; } /** * Retrieves the value wrapped inside this fragment. * * @return the value contained in the QueryVal fragment. */ public Object getValue() { return this.value; } } ``` ## File: src/main/java/com/fauna/query/template/FaunaTemplate.java ```java package com.fauna.query.template; import com.fauna.query.builder.QueryFragment; import com.fauna.query.builder.QueryLiteral; import com.fauna.query.builder.QueryVal; import java.util.Iterator; import java.util.Map; import java.util.Objects; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * Represents a template for constructing Fauna queries with placeholders * for variable interpolation. This template uses a dollar-sign ($) syntax for * identifying variable placeholders. */ public final class FaunaTemplate implements Iterable { private static final char DELIMITER = '$'; private static final String ID_PATTERN = "[\\p{L}_][\\p{L}\\p{N}_]*"; private static final Pattern PATTERN; static { String delim = Pattern.quote(String.valueOf(DELIMITER)); String pattern = String.format( "%s(?:(?%s)|\\{(?%s)\\}|\\{(?[^}]*))?", delim, delim, ID_PATTERN ); PATTERN = Pattern.compile(pattern, Pattern.COMMENTS); } private final String template; /** * Constructs a new {@code FaunaTemplate} with the specified template * string. The template may contain literals and variable placeholders * identified by a dollar sign and an optional set of braces * (e.g., ${variable}). * * @param template the string template containing literals and placeholders. */ public FaunaTemplate(final String template) { this.template = template; } /** * Creates an iterator over the parts of the template, distinguishing * between literal text and variable placeholders. * * @return an Iterator that iterates over the template parts. */ @Override public Iterator iterator() { return new Iterator<>() { private final Matcher matcher = PATTERN.matcher(template); private int curPos = 0; private boolean foundMatch = false; @Override public boolean hasNext() { if (curPos < template.length()) { return true; } if (foundMatch) { return true; } return matcher.find(curPos); } @Override public TemplatePart next() { if (curPos >= template.length()) { throw new IllegalStateException("No more elements"); } if (foundMatch || (foundMatch = matcher.find(curPos))) { int spanStartPos = matcher.start(); int spanEndPos = matcher.end(); String invalid = matcher.group("invalid"); if (invalid != null) { handleInvalid(matcher.start("invalid")); } String escapedPart = matcher.group("escaped"); String variablePart = matcher.group("braced"); TemplatePart part; if (escapedPart != null) { String literalPart = template.substring(curPos, spanStartPos) + DELIMITER; part = new TemplatePart(literalPart, TemplatePartType.LITERAL); curPos = spanEndPos; } else if (variablePart != null) { if (curPos < spanStartPos) { part = new TemplatePart( template.substring(curPos, spanStartPos), TemplatePartType.LITERAL); curPos = spanStartPos; } else { part = new TemplatePart(variablePart, TemplatePartType.VARIABLE); curPos = spanEndPos; } } else { part = new TemplatePart( template.substring(curPos, spanStartPos), TemplatePartType.LITERAL); curPos = spanEndPos; } foundMatch = false; // Reset after processing a match return part; } else { TemplatePart part = new TemplatePart(template.substring(curPos), TemplatePartType.LITERAL); curPos = template.length(); return part; } } }; } /** * Handles invalid placeholder syntax within the template. * * @param position the starting position of the invalid placeholder. * @throws IllegalArgumentException if the placeholder syntax is invalid. */ private void handleInvalid(final int position) { String substringUpToPosition = template.substring(0, position); String[] lines = substringUpToPosition.split("\r?\n"); int colno; int lineno; if (lines.length == 0) { colno = 1; lineno = 1; } else { String lastLine = lines[lines.length - 1]; // Adjust the column number for invalid placeholder colno = position - (substringUpToPosition.length() - lastLine.length()) - 1; // -1 to exclude the dollar sign lineno = lines.length; } throw new IllegalArgumentException(String.format( "Invalid placeholder in template: line %d, col %d", lineno, colno)); } /** * Represents a part of the template, which can either be a literal string * or a variable placeholder. */ public static final class TemplatePart { private final String part; private final TemplatePartType type; /** * Constructs a new {@code TemplatePart} with the specified text and * type. * * @param part the text for this part of the template, either literal * text or a variable. * @param type the type of this part of the template, * either {@link TemplatePartType#LITERAL} * or {@link TemplatePartType#VARIABLE}. */ public TemplatePart(final String part, final TemplatePartType type) { this.part = part; this.type = type; } /** * Retrieves the text of this part of the template. * * @return the text for this template part. */ public String getPart() { return part; } /** * Retrieves the type of this part of the template. * * @return the type of this template part, either literal or variable. */ public TemplatePartType getType() { return type; } /** * Converts this template part to a {@code QueryFragment} using the * given arguments. If this part is a variable, the argument map is * checked for a corresponding key, returning an appropriate * {@code QueryFragment}. If no matching argument is found, an exception * is thrown. * * @param args the map of arguments for template substitution. * @return a {@code QueryFragment} representing this template part. * @throws IllegalArgumentException if required arguments are missing. */ @SuppressWarnings("rawtypes") public QueryFragment toFragment(final Map args) { if (this.getType().equals(TemplatePartType.VARIABLE)) { if (Objects.isNull(args)) { throw new IllegalArgumentException( String.format( "No args provided for Template variable %s.", this.getPart())); } if (args.containsKey(this.getPart())) { var arg = args.get(this.getPart()); if (arg instanceof QueryFragment) { return (QueryFragment) arg; } else { return new QueryVal<>(arg); } } else { throw new IllegalArgumentException( String.format( "Template variable %s not found in provided args.", this.getPart())); } } else { return new QueryLiteral(this.getPart()); } } } } ``` ## File: src/main/java/com/fauna/query/template/package-info.java ```java /** * Classes for parsing query templates. */ package com.fauna.query.template; ``` ## File: src/main/java/com/fauna/query/template/TemplatePartType.java ```java package com.fauna.query.template; /** * Represents the type of template part within a FaunaTemplate. */ public enum TemplatePartType { /** * Indicates a literal text part of the template. */ LITERAL, /** * Indicates a variable placeholder part of the template. */ VARIABLE } ``` ## File: src/main/java/com/fauna/query/AfterToken.java ```java package com.fauna.query; import java.util.Optional; /** * Represents an `after` token used for Set * pagination. */ public class AfterToken { private final String token; /** * Constructs an {@code AfterToken} with the specified token. * * @param token the token to be stored in this {@code AfterToken} instance. */ public AfterToken(final String token) { this.token = token; } /** * Returns the token stored in this {@code AfterToken} instance. * * @return the token as a {@code String}. */ public String getToken() { return token; } /** * Creates an {@code AfterToken} instance from the specified token string. * If the provided token is {@code null}, an empty {@code Optional} is * returned. * * @param token the token string to convert into an {@code AfterToken}. * @return an {@code Optional} containing an {@code AfterToken} if the * token is non-null, or an empty {@code Optional} if it is null. */ public static Optional fromString(final String token) { return Optional.ofNullable( token != null ? new AfterToken(token) : null); } } ``` ## File: src/main/java/com/fauna/query/package-info.java ```java /** * Classes for defining queries with Fauna. */ package com.fauna.query; ``` ## File: src/main/java/com/fauna/query/QueryOptions.java ```java package com.fauna.query; import java.time.Duration; import java.util.Optional; import static com.fauna.constants.Defaults.DEFAULT_TIMEOUT; /** * Encapsulates options for configuring Fauna queries, such as timeout, * linearized reads, typechecking, query tags, and trace parent for * distributed tracing. */ public class QueryOptions { private final Boolean linearized; private final Boolean typeCheck; private final Duration timeout; private final QueryTags queryTags; private final String traceParent; /** * Creates an instance of QueryOptions using the specified builder. * * @param builder the builder with values for query options. */ public QueryOptions(final Builder builder) { this.linearized = builder.linearized; this.typeCheck = builder.typeCheck; this.timeout = builder.timeout; this.queryTags = builder.queryTags; this.traceParent = builder.traceParent; } /** * Default QueryOptions instance with default configurations. * * @return a new QueryOptions instance with defaults. */ public static QueryOptions getDefault() { return QueryOptions.builder().build(); } /** * Returns an Optional indicating if linearized reads are enabled. * * @return an Optional containing the linearized setting, or empty if not * specified. */ public Optional getLinearized() { return Optional.ofNullable(this.linearized); } /** * Returns an Optional indicating if type checking is enabled. * * @return an Optional containing the typeCheck setting, or empty if not * specified. */ public Optional getTypeCheck() { return Optional.ofNullable(this.typeCheck); } /** * Returns an Optional of the query timeout duration in milliseconds. * * @return an Optional containing the query timeout duration in milliseconds, or * empty if not specified. */ public Optional getTimeoutMillis() { return Optional.ofNullable(this.timeout).map(Duration::toMillis); } /** * Returns an Optional of the query tags. * * @return an Optional containing the QueryTags, or empty if not specified. */ public Optional getQueryTags() { return Optional.ofNullable(this.queryTags); } /** * Returns an Optional of the trace parent for distributed tracing. * * @return an Optional containing the traceParent, or empty if not * specified. */ public Optional getTraceParent() { return Optional.ofNullable(this.traceParent); } /** * Builder class for constructing instances of QueryOptions. */ public static class Builder { private Boolean linearized = null; private Boolean typeCheck = null; private Duration timeout = DEFAULT_TIMEOUT; private QueryTags queryTags = null; private String traceParent = null; /** * If true, read-only transactions that don't read indexes are strictly * serialized. * * @param linearized true to enable linearized reads, false otherwise. * @return this Builder instance for chaining. */ public Builder linearized(final boolean linearized) { this.linearized = linearized; return this; } /** * If true, typechecking * is enabled for queries. You can only enable typechecking for databases that * have typechecking enabled. * * @param typeCheck true to enable type checking, false otherwise. * @return this Builder instance for chaining. */ public Builder typeCheck(final boolean typeCheck) { this.typeCheck = typeCheck; return this; } /** * Sets the timeout duration for the query. * * @param timeout the timeout Duration for the query. * @return this Builder instance for chaining. */ public Builder timeout(final Duration timeout) { this.timeout = timeout; return this; } /** * Sets query tags used to * instrument the query. You typically use query tags to monitor and debug query requests in * Fauna Logs. * * @param queryTags the QueryTags to associate with the query. * @return this Builder instance for chaining. */ public Builder queryTags(final QueryTags queryTags) { if (this.queryTags != null) { this.queryTags.putAll(queryTags); } else { this.queryTags = queryTags; } return this; } /** * Adds a single query tag to the existing tags. * * @param key the key of the query tag. * @param value the value of the query tag. * @return this Builder instance for chaining. */ public Builder queryTag(final String key, final String value) { if (this.queryTags == null) { this.queryTags = new QueryTags(); } this.queryTags.put(key, value); return this; } /** * Traceparent identifier used for distributed tracing. Passed by the drive in the `traceparent` header of Query * HTTP endpoint requests. If you don’t include a traceparent identifier or use an invalid identifier, * Fauna generates a valid identifier. * * @param traceParent the trace parent ID. * @return this Builder instance for chaining. */ public Builder traceParent(final String traceParent) { this.traceParent = traceParent; return this; } /** * Builds and returns a new instance of QueryOptions. * * @return a new QueryOptions instance with the configured settings. */ public QueryOptions build() { return new QueryOptions(this); } } /** * Creates and returns a new Builder instance for constructing QueryOptions. * * @return a new Builder instance. */ public static Builder builder() { return new Builder(); } } ``` ## File: src/main/java/com/fauna/query/QueryTags.java ```java package com.fauna.query; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.JsonToken; import com.fauna.exception.ClientResponseException; import java.io.IOException; import java.util.HashMap; import java.util.Map; import java.util.stream.Collectors; /** * A utility class representing a collection of query tags as a map of key-value pairs. * This class extends {@link HashMap} and provides methods to build, encode, and parse query tags. */ public class QueryTags extends HashMap { private static final String EQUALS = "="; private static final String COMMA = ","; /** * Creates a new {@code QueryTags} instance from an array of tag strings. * Each tag string must be in the form "key=value". * * @param tags an array of strings representing tags in the format "k=v", * where {@code k} is the tag key and {@code v} is the tag value. * @return a new {@code QueryTags} instance containing the parsed tags. * @throws ClientResponseException if a tag string is not in the expected "k=v" format. */ public static QueryTags of(final String... tags) { QueryTags queryTags = new QueryTags(); for (String tagString : tags) { String[] tag = tagString.split(EQUALS); if (tag.length == 2) { queryTags.put(tag[0].strip(), tag[1].strip()); } else { throw new ClientResponseException( "Invalid tag encoding: " + tagString); } } return queryTags; } /** * Encodes the {@code QueryTags} instance as a single string. * The tags are sorted by their keys and concatenated in the format "key=value,key=value,...". * * @return a {@code String} representing the encoded query tags. */ public String encode() { return this.entrySet().stream().sorted(Map.Entry.comparingByKey()) .map(entry -> String.join(EQUALS, entry.getKey(), entry.getValue())) .collect(Collectors.joining(COMMA)); } /** * Parses a JSON parser to construct a {@code QueryTags} instance. * This method expects the JSON to contain either a null value or a string representation of tags in * "key=value,key=value,..." format. * * @param parser a {@code JsonParser} positioned at the JSON data to parse. * @return a {@code QueryTags} instance representing the parsed tags, or {@code null} if the JSON value is null. * @throws IOException if an error occurs during parsing. * @throws ClientResponseException if the JSON token is not a string or null. */ public static QueryTags parse(final JsonParser parser) throws IOException { if (parser.nextToken() == JsonToken.VALUE_NULL) { return null; } else if (parser.getCurrentToken() == JsonToken.VALUE_STRING) { String tagString = parser.getText(); if (!tagString.isEmpty()) { return QueryTags.of(tagString.split(COMMA)); } else { return new QueryTags(); } } else { throw new ClientResponseException( "Unexpected token for QueryTags: " + parser.getCurrentToken()); } } } ``` ## File: src/main/java/com/fauna/response/ConstraintFailure.java ```java package com.fauna.response; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.JsonToken; import com.fauna.constants.ResponseFields; import com.fauna.exception.ClientResponseException; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Objects; import java.util.Optional; import java.util.stream.Collectors; public final class ConstraintFailure { private final String message; private final String name; private final PathElement[][] paths; /** * Initialize a new ConstraintFailure instance. Queries that fail a check * or unique * constraint return a constraint failure. * * @param message Human-readable description of the constraint failure. * @param name Name of the failed constraint. * @param paths A list of paths where the constraint failure occurred. */ public ConstraintFailure( final String message, final String name, final PathElement[][] paths) { this.message = message; this.name = name; this.paths = paths; } /** * Constructs a PathElement[] from the provided objects. Supported types * are String and Integer. * * @param elements The String objects or Integer objects to use. * @return A array of PathElement instances. */ public static PathElement[] createPath(final Object... elements) { List path = new ArrayList<>(); for (Object element : elements) { if (element instanceof String) { path.add(new PathElement((String) element)); } else if (element instanceof Integer) { path.add(new PathElement((Integer) element)); } else { throw new IllegalArgumentException( "Only strings and integers supported"); } } return path.toArray(new PathElement[0]); } /** * Initializes a new empty Builder. * * @return A new Builder */ public static Builder builder() { return new Builder(); } /** * Builds a ConstraintFailure instance from the provided JsonParser. * * @param parser The JsonParser to consume. * @return A new ConstraintFailure instance. * @throws IOException Thrown if an error is encountered while reading the * parser. */ public static ConstraintFailure parse(final JsonParser parser) throws IOException { if (parser.currentToken() != JsonToken.START_OBJECT && parser.nextToken() != JsonToken.START_OBJECT) { throw new ClientResponseException( "Constraint failure should be a JSON object."); } Builder builder = ConstraintFailure.builder(); while (parser.nextToken() == JsonToken.FIELD_NAME) { String fieldName = parser.getValueAsString(); switch (fieldName) { case ResponseFields.ERROR_MESSAGE_FIELD_NAME: builder.message(parser.nextTextValue()); break; case ResponseFields.ERROR_NAME_FIELD_NAME: builder.name(parser.nextTextValue()); break; case ResponseFields.ERROR_PATHS_FIELD_NAME: List paths = new ArrayList<>(); JsonToken firstPathToken = parser.nextToken(); if (firstPathToken == JsonToken.START_ARRAY) { while (parser.nextToken() == JsonToken.START_ARRAY) { List path = new ArrayList<>(); while (parser.nextToken() != JsonToken.END_ARRAY) { path.add(PathElement.parse(parser)); } paths.add(path.toArray(new PathElement[0])); } } else if (firstPathToken != JsonToken.VALUE_NULL) { throw new ClientResponseException( "Constraint failure path should be array or null, got: " + firstPathToken.toString()); } paths.forEach(builder::path); break; default: } } return builder.build(); } /** * Gets the constraint failure message. * * @return A string representation of the message. */ public String getMessage() { return this.message; } /** * Gets the constraint failure name. * * @return A string representation of the name. */ public Optional getName() { return Optional.ofNullable(this.name); } /** * Gets an optional path elements related to the constraint failure. * * @return An array of arrays of PathElements. */ public Optional getPaths() { return Optional.ofNullable(paths); } /** * Gets a list of string representations of the constraint failure paths. * * @return A list of string representations of constraint failure paths. */ public Optional> getPathStrings() { if (paths == null) { return Optional.empty(); } else { return Optional.of(Arrays.stream(paths).map( pathElements -> Arrays.stream(pathElements) .map(PathElement::toString).collect( Collectors.joining("."))) .collect(Collectors.toList())); } } /** * Tests path equality with another ConstraintFailure. * * @param otherFailure The other ConstraintFailure. * @return True if the paths are equal. */ public boolean pathsAreEqual(final ConstraintFailure otherFailure) { PathElement[][] thisArray = this.getPaths().orElse(new PathElement[0][]); PathElement[][] otherArray = otherFailure.getPaths().orElse(new PathElement[0][]); return Arrays.deepEquals(thisArray, otherArray); } @Override public boolean equals(final Object other) { if (other instanceof ConstraintFailure) { ConstraintFailure otherFailure = (ConstraintFailure) other; return this.getMessage().equals(otherFailure.getMessage()) && this.getName().equals(otherFailure.getName()) && pathsAreEqual(otherFailure); } else { return false; } } @Override public int hashCode() { return Objects.hash( this.name, this.message, Arrays.deepHashCode(this.paths)); } public static final class PathElement { private String sVal = null; private Integer iVal = null; /** * Initializes a PathElement with a string value. * * @param sVal The string value. */ public PathElement(final String sVal) { this.sVal = sVal; } /** * Initializes a PathElement with an integer value. * * @param iVal The integer value. */ public PathElement(final Integer iVal) { this.iVal = iVal; } /** * Note that this parse method does not advance the parser. * * @param parser A JsonParser instance. * @return A new PathElement. * @throws IOException Can be thrown if e.g. stream ends. */ public static PathElement parse(final JsonParser parser) throws IOException { if (parser.currentToken().isNumeric()) { return new PathElement(parser.getValueAsInt()); } else { return new PathElement(parser.getText()); } } /** * Tests whether the PathElement stores a string or an integer. * * @return If it's a string, true. Otherwise, false. */ public boolean isString() { return sVal != null; } @Override public boolean equals(final Object o) { if (o instanceof PathElement) { PathElement other = (PathElement) o; return other.isString() == this.isString() && other.toString().equals(this.toString()); } else { return false; } } @Override public int hashCode() { return toString().hashCode(); } /** * Converts the PathElement to a string. * * @return A string representation of the PathElement. */ public String toString() { return sVal == null ? String.valueOf(iVal) : sVal; } } public static class Builder { private final List paths = new ArrayList<>(); private String message = null; private String name = null; /** * Sets a message on the builder. * * @param message The message to set. * @return this. */ public Builder message(final String message) { this.message = message; return this; } /** * Sets a name on the builder. * * @param name The name to set. * @return this. */ public Builder name(final String name) { this.name = name; return this; } /** * Sets a path on the builder. * * @param path The path to set. * @return this. */ public Builder path(final PathElement[] path) { this.paths.add(path); return this; } /** * Builds a ConstraintFailure instance from the current builder. * * @return A ConstraintFailure instance. */ public ConstraintFailure build() { PathElement[][] paths = this.paths.toArray(new PathElement[this.paths.size()][]); return new ConstraintFailure(this.message, this.name, this.paths.isEmpty() ? null : paths); } } } ``` ## File: src/main/java/com/fauna/response/ErrorInfo.java ```java package com.fauna.response; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.JsonToken; import com.fasterxml.jackson.core.TreeNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fauna.codec.Codec; import com.fauna.codec.DefaultCodecProvider; import com.fauna.codec.UTF8FaunaParser; import com.fauna.exception.ClientResponseException; import java.io.IOException; import java.util.ArrayList; import java.util.List; import java.util.Optional; import static com.fauna.constants.ResponseFields.ERROR_ABORT_FIELD_NAME; import static com.fauna.constants.ResponseFields.ERROR_CODE_FIELD_NAME; import static com.fauna.constants.ResponseFields.ERROR_CONSTRAINT_FAILURES_FIELD_NAME; import static com.fauna.constants.ResponseFields.ERROR_MESSAGE_FIELD_NAME; /** * This class will encapsulate all the information Fauna returns about errors including constraint failures, and * abort data, for now it just has the code and message. */ public class ErrorInfo { private final String code; private final String message; private final ConstraintFailure[] constraintFailures; private final TreeNode abort; /** * Initializes a new ErrorInfo. * * @param code The * Fauna error code. * @param message A short, human-readable description of the error. * @param constraintFailures The constraint failures for the error, if any. * Only present if the error code is * `constraint_failure`. * @param abort A user-defined error message passed using an * FQL `abort()` method call. Only present if the error * code is `abort`. */ public ErrorInfo( final String code, final String message, final ConstraintFailure[] constraintFailures, final TreeNode abort) { this.code = code; this.message = message; this.constraintFailures = constraintFailures; this.abort = abort; } /** * A utility method to instantiate an empty builder. * * @return A new builder */ public static Builder builder() { return new Builder(); } private static Builder handleField(final Builder builder, final JsonParser parser) throws IOException { String fieldName = parser.getCurrentName(); switch (fieldName) { case ERROR_CODE_FIELD_NAME: return builder.code(parser.nextTextValue()); case ERROR_MESSAGE_FIELD_NAME: return builder.message(parser.nextTextValue()); case ERROR_ABORT_FIELD_NAME: parser.nextToken(); return builder.abort(new ObjectMapper().readTree(parser)); case ERROR_CONSTRAINT_FAILURES_FIELD_NAME: List failures = new ArrayList<>(); JsonToken token = parser.nextToken(); if (token == JsonToken.VALUE_NULL) { return builder; } else if (token == JsonToken.START_ARRAY) { JsonToken nextToken = parser.nextToken(); while (nextToken == JsonToken.START_OBJECT) { failures.add(ConstraintFailure.parse(parser)); nextToken = parser.nextToken(); } return builder.constraintFailures(failures); } else { throw new ClientResponseException( "Unexpected token in constraint failures: " + token); } default: throw new ClientResponseException( "Unexpected token in error info: " + parser.currentToken()); } } /** * Builds a new ErrorInfo from a JsonParser. * * @param parser The JsonParser to read. * @return A new ErrorInfo instance. * @throws IOException Thrown on errors reading from the parser. */ public static ErrorInfo parse(final JsonParser parser) throws IOException { if (parser.nextToken() != JsonToken.START_OBJECT) { throw new ClientResponseException( "Error parsing error info, got token" + parser.currentToken()); } Builder builder = ErrorInfo.builder(); while (parser.nextToken() == JsonToken.FIELD_NAME) { builder = handleField(builder, parser); } return builder.build(); } /** * Gets the Fauna error code. * * @return A string representing the Fauna error code. */ public String getCode() { return code; } /** * Gets the error message. * * @return A string representing the error message. */ public String getMessage() { return message; } /** * Gets the constraint failures. * * @return An optional containing the constraint failures. */ public Optional getConstraintFailures() { return Optional.ofNullable(this.constraintFailures); } /** * Gets the user-defined abort error message as a JSON node. * * @return An optional TreeNode with the abort data. */ public Optional getAbortJson() { return Optional.ofNullable(this.abort); } /** * Parses the abort data into the provided class. * * @param abortDataClass The class to decode into. * @param The type to decode into. * @return An instance of the provided type. */ public Optional getAbort(final Class abortDataClass) { return this.getAbortJson().map(tree -> { UTF8FaunaParser parser = new UTF8FaunaParser(tree.traverse()); Codec codec = DefaultCodecProvider.SINGLETON.get(abortDataClass); parser.read(); return codec.decode(parser); }); } public static class Builder { private String code = null; private String message = null; private ConstraintFailure[] constraintFailures = null; private TreeNode abort = null; /** * Sets the error code on the builder. * * @param code The error code. * @return this */ public Builder code(final String code) { this.code = code; return this; } /** * Sets the message on the builder. * * @param message The message. * @return this */ public Builder message(final String message) { this.message = message; return this; } /** * Sets the abort data on the builder. * * @param abort The abort JSON node. * @return this */ public Builder abort(final TreeNode abort) { this.abort = abort; return this; } /** * Sets the constraint failures on the builder. * * @param constraintFailures The constraint failures. * @return this */ public Builder constraintFailures( final List constraintFailures) { this.constraintFailures = constraintFailures.toArray(new ConstraintFailure[0]); return this; } /** * Returns a new ErrorInfo instance based on the current builder. * * @return An ErrorInfo instance */ public ErrorInfo build() { return new ErrorInfo(this.code, this.message, this.constraintFailures, this.abort); } } } ``` ## File: src/main/java/com/fauna/response/MultiByteBufferInputStream.java ```java package com.fauna.response; import java.io.EOFException; import java.io.IOException; import java.io.InputStream; import java.nio.ByteBuffer; import java.util.List; /** * Joins a list of byte buffers to make them appear as a single input stream. *

* The reset method is supported, and always resets to restart the stream at the beginning of the first buffer, * although markSupported() returns false for this class. */ public class MultiByteBufferInputStream extends InputStream { private final int ff = 0xFF; private final List buffers; private int index = 0; private ByteBuffer currentBuffer; /** * Initializes a MultiByteBufferInputStream using the provided byte buffers. * * @param initialBuffers A list of ByteBuffers to use. */ public MultiByteBufferInputStream(final List initialBuffers) { this.buffers = initialBuffers; this.currentBuffer = buffers.get(index); } /** * Adds additional byte buffers to this instance in a thread-safe manner. * * @param additionalBuffers The additional ByteBuffers. */ public synchronized void add(final List additionalBuffers) { buffers.addAll(additionalBuffers); } /** * Reads the next byte from the buffer. * * @return The next byte. * @throws IOException Thrown when the byte buffers are exhausted. */ @SuppressWarnings("checkstyle:MagicNumber") @Override public synchronized int read() throws IOException { if (currentBuffer.hasRemaining()) { return currentBuffer.get() & 0xFF; } else if (buffers.size() > index + 1) { index++; currentBuffer = buffers.get(index); return currentBuffer.get() & 0xFF; } else { throw new EOFException("End of Stream"); } } /** * Resets the byte buffer. */ @Override public synchronized void reset() { for (ByteBuffer buffer : buffers.subList(0, index)) { buffer.position(0); } index = 0; currentBuffer = buffers.get(index); } } ``` ## File: src/main/java/com/fauna/response/package-info.java ```java /** * Classes for modeling and handling query responses from Fauna. */ package com.fauna.response; ``` ## File: src/main/java/com/fauna/response/QueryFailure.java ```java package com.fauna.response; import java.util.Optional; public final class QueryFailure extends QueryResponse { private final int statusCode; private final ErrorInfo errorInfo; public QueryFailure(int httpStatus, Builder builder) { super(builder); this.statusCode = httpStatus; this.errorInfo = builder.getError(); } public int getStatusCode() { return statusCode; } public String getErrorCode() { return errorInfo.getCode(); } public String getMessage() { return errorInfo.getMessage(); } public Optional getAbort(Class clazz) { return errorInfo.getAbort(clazz); } public String getFullMessage() { String summarySuffix = this.getSummary() != null ? "\n---\n" + this.getSummary() : ""; return String.format("%d (%s): %s%s", this.getStatusCode(), this.getErrorCode(), this.getMessage(), summarySuffix); } public Optional getConstraintFailures() { return this.errorInfo.getConstraintFailures(); } } ``` ## File: src/main/java/com/fauna/response/QueryResponse.java ```java package com.fauna.response; import com.fasterxml.jackson.core.JsonFactory; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.JsonToken; import com.fauna.client.StatsCollector; import com.fauna.codec.Codec; import com.fauna.codec.UTF8FaunaParser; import com.fauna.exception.ClientResponseException; import com.fauna.exception.ErrorHandler; import com.fauna.exception.FaunaException; import com.fauna.exception.ProtocolException; import com.fauna.query.QueryTags; import java.io.IOException; import java.io.InputStream; import java.net.HttpURLConnection; import java.net.http.HttpResponse; import java.util.Map; import static com.fauna.constants.ResponseFields.DATA_FIELD_NAME; import static com.fauna.constants.ResponseFields.ERROR_FIELD_NAME; import static com.fauna.constants.ResponseFields.LAST_SEEN_TXN_FIELD_NAME; import static com.fauna.constants.ResponseFields.QUERY_TAGS_FIELD_NAME; import static com.fauna.constants.ResponseFields.SCHEMA_VERSION_FIELD_NAME; import static com.fauna.constants.ResponseFields.STATIC_TYPE_FIELD_NAME; import static com.fauna.constants.ResponseFields.STATS_FIELD_NAME; import static com.fauna.constants.ResponseFields.SUMMARY_FIELD_NAME; public abstract class QueryResponse { private static final JsonFactory JSON_FACTORY = new JsonFactory(); private final Long lastSeenTxn; private final Long schemaVersion; private final String summary; private final Map queryTags; private final QueryStats stats; @SuppressWarnings("rawtypes") QueryResponse(final Builder builder) { this.lastSeenTxn = builder.lastSeenTxn; this.summary = builder.summary; this.schemaVersion = builder.schemaVersion; this.stats = builder.stats; this.queryTags = builder.queryTags; } /** * A helper method to instantiate a new builder. * * @param codec The codec to use when parsing data. * @param The return type of the data. * @return A new Builder instance. */ public static Builder builder(final Codec codec) { return new Builder<>(codec); } private static Builder handleField(final Builder builder, final JsonParser parser) throws IOException { String fieldName = parser.getCurrentName(); switch (fieldName) { case ERROR_FIELD_NAME: return builder.error(ErrorInfo.parse(parser)); case DATA_FIELD_NAME: return builder.data(parser); case STATS_FIELD_NAME: return builder.stats(QueryStats.parseStats(parser)); case QUERY_TAGS_FIELD_NAME: return builder.queryTags(QueryTags.parse(parser)); case LAST_SEEN_TXN_FIELD_NAME: return builder.lastSeenTxn(parser.nextLongValue(0)); case SCHEMA_VERSION_FIELD_NAME: return builder.schemaVersion(parser.nextLongValue(0)); case STATIC_TYPE_FIELD_NAME: return builder.staticType(parser.nextTextValue()); case SUMMARY_FIELD_NAME: return builder.summary(parser.nextTextValue()); default: throw new ClientResponseException( "Unexpected field '" + fieldName + "'."); } } /** * A helper method to adapt an HTTP response into a QuerySuccess or throw * the appropriate FaunaException. * * @param response The HTTP response to adapt. * @param codec The codec to use when reading the HTTP response body. * @param statsCollector The stats collector to accumulate stats against. * @param The response type on success. * @return A QuerySuccess instance. * @throws FaunaException Thrown on non-200 responses. */ public static QuerySuccess parseResponse( final HttpResponse response, final Codec codec, final StatsCollector statsCollector) throws FaunaException { try { JsonParser parser = JSON_FACTORY.createParser(response.body()); JsonToken firstToken = parser.nextToken(); Builder builder = QueryResponse.builder(codec); if (firstToken != JsonToken.START_OBJECT) { throw new ClientResponseException( "Response must be JSON object."); } while (parser.nextToken() == JsonToken.FIELD_NAME) { builder = handleField(builder, parser); } if (builder.stats != null) { statsCollector.add(builder.stats); } int httpStatus = response.statusCode(); if (httpStatus >= HttpURLConnection.HTTP_BAD_REQUEST) { QueryFailure failure = new QueryFailure(httpStatus, builder); ErrorHandler.handleQueryFailure(response.statusCode(), failure); // Fall back on ProtocolException. throw new ProtocolException(response.statusCode(), failure); } return builder.buildSuccess(); } catch (IOException exc) { throw new ClientResponseException( "Failed to handle error response.", exc, response.statusCode()); } } /** * Gets the last seen transaction timestamp. * * @return A long representing the last seen transaction timestamp. */ public Long getLastSeenTxn() { return lastSeenTxn; } /** * Gets the schema version. * * @return A long representing the schema version. */ public Long getSchemaVersion() { return schemaVersion; } /** * Gets the summary associated with the response. * * @return A string representing the summary. */ public String getSummary() { return summary; } /** * Gets the query tags associated with the response. * * @return A Map containing the query tags. */ public Map getQueryTags() { return queryTags; } /** * Gets the query stats associated with the response. * * @return A QueryStats instance. */ public QueryStats getStats() { return stats; } public static final class Builder { private final Codec codec; private Long lastSeenTxn; private String summary; private Long schemaVersion; private QueryStats stats; private QueryTags queryTags; private String staticType; private ErrorInfo error; private T data; /** * Initializes a QueryResponse.Builder. * * @param codec The codec to use when building data. */ public Builder(final Codec codec) { this.codec = codec; } /** * Set the last seen transaction timestamp on the builder. * * @param lastSeenTxn The last seen transaction timestamp. * @return This */ public Builder lastSeenTxn(final Long lastSeenTxn) { this.lastSeenTxn = lastSeenTxn; return this; } /** * Set the schema version on the builder. * * @param schemaVersion The schema version. * @return This */ public Builder schemaVersion(final Long schemaVersion) { this.schemaVersion = schemaVersion; return this; } /** * Set the data on the builder by consuming the provided JsonParser with * the configured codec. * * @param parser The JsonParser to consume. * @return This */ public Builder data(final JsonParser parser) { UTF8FaunaParser faunaParser = new UTF8FaunaParser(parser); faunaParser.read(); this.data = this.codec.decode(faunaParser); return this; } /** * Set the query tags on the builder. * * @param tags The query tags to set. * @return This */ public Builder queryTags(final QueryTags tags) { this.queryTags = tags; return this; } /** * Sets the error info on the builder. * * @param info The error info to set. * @return This */ public Builder error(final ErrorInfo info) { this.error = info; return this; } /** * Sets the static type on the builder. * * @param staticType The static type to set. * @return This */ public Builder staticType(final String staticType) { this.staticType = staticType; return this; } /** * Sets the summary on the builder. * * @param summary The summary to set. * @return This */ public Builder summary(final String summary) { this.summary = summary; return this; } /** * Sets the query stats on the builder. * * @param stats The query stats to set. * @return This */ public Builder stats(final QueryStats stats) { this.stats = stats; return this; } /** * Builds a QuerySuccess. * * @return A QuerySuccess from the current builder. */ public QuerySuccess buildSuccess() { return new QuerySuccess<>(this); } /** * Gets a string representing the static type. * * @return A string representing the static type. */ public String getStaticType() { return staticType; } /** * Gets an ErrorInfo instance representing an error on the response. * * @return An ErrorInfo instance. */ public ErrorInfo getError() { return error; } /** * Gets the parsed data from the response. * * @return The parsed data. */ public T getData() { return data; } } } ``` ## File: src/main/java/com/fauna/response/QueryStats.java ```java package com.fauna.response; import com.fasterxml.jackson.core.JsonParser; import com.fauna.constants.ResponseFields; import com.fauna.exception.ClientResponseException; import java.io.IOException; import java.util.ArrayList; import java.util.List; import static com.fasterxml.jackson.core.JsonToken.FIELD_NAME; import static com.fasterxml.jackson.core.JsonToken.START_ARRAY; import static com.fasterxml.jackson.core.JsonToken.START_OBJECT; import static com.fasterxml.jackson.core.JsonToken.VALUE_NULL; import static com.fasterxml.jackson.core.JsonToken.VALUE_STRING; public final class QueryStats { private final int computeOps; private final int readOps; private final int writeOps; private final int queryTimeMs; private final int processingTimeMs; private final int contentionRetries; private final int storageBytesRead; private final int storageBytesWrite; private final List rateLimitsHit; private String stringValue = null; /** * @param computeOps Transactional * Compute Operations (TCOs) consumed by the request. * @param readOps Transactional * Read Operations (TROs) consumed by the request. * @param writeOps Transactional * Write Operations (TROs) consumed by the request. * @param queryTimeMs Query run time for the request in milliseconds. * @param contentionRetries Number of retries * for contended transactions * @param storageBytesRead Amount of data read from storage, in bytes. * @param storageBytesWrite Amount of data written to storage, in bytes. * @param processingTimeMs Aggregate event processing time in milliseconds. * Only applies to event feed and event stream * requests. * @param rateLimitsHit Operation types that exceeded * plan * throughput limits. */ public QueryStats(final int computeOps, final int readOps, final int writeOps, final int queryTimeMs, final int contentionRetries, final int storageBytesRead, final int storageBytesWrite, final int processingTimeMs, final List rateLimitsHit) { this.computeOps = computeOps; this.readOps = readOps; this.writeOps = writeOps; this.queryTimeMs = queryTimeMs; this.contentionRetries = contentionRetries; this.storageBytesRead = storageBytesRead; this.storageBytesWrite = storageBytesWrite; this.processingTimeMs = processingTimeMs; this.rateLimitsHit = rateLimitsHit != null ? rateLimitsHit : List.of(); } static Builder builder() { return new Builder(); } static Builder parseField(final Builder builder, final JsonParser parser) throws IOException { String fieldName = parser.getValueAsString(); switch (fieldName) { case ResponseFields.STATS_COMPUTE_OPS_FIELD_NAME: return builder.computeOps(parser.nextIntValue(0)); case ResponseFields.STATS_READ_OPS: return builder.readOps(parser.nextIntValue(0)); case ResponseFields.STATS_WRITE_OPS: return builder.writeOps(parser.nextIntValue(0)); case ResponseFields.STATS_QUERY_TIME_MS: return builder.queryTimeMs(parser.nextIntValue(0)); case ResponseFields.STATS_PROCESSING_TIME_MS: return builder.processingTimeMs(parser.nextIntValue(0)); case ResponseFields.STATS_CONTENTION_RETRIES: return builder.contentionRetries(parser.nextIntValue(0)); case ResponseFields.STATS_STORAGE_BYTES_READ: return builder.storageBytesRead(parser.nextIntValue(0)); case ResponseFields.STATS_STORAGE_BYTES_WRITE: return builder.storageBytesWrite(parser.nextIntValue(0)); case ResponseFields.STATS_RATE_LIMITS_HIT: List limits = new ArrayList<>(); if (parser.nextToken() == START_ARRAY) { while (parser.nextToken() == VALUE_STRING) { limits.add(parser.getValueAsString()); } } return builder.rateLimitsHit(limits); default: throw new ClientResponseException("Unknown field " + fieldName); } } /** * Parse QueryStats from a JsonParser. * * @param parser the JsonParser to consume * @return a QueryStats object containing the parsed stats * @throws IOException thrown from the JsonParser */ public static QueryStats parseStats(final JsonParser parser) throws IOException { if (parser.nextToken() == START_OBJECT) { Builder builder = builder(); while (parser.nextToken() == FIELD_NAME) { builder = parseField(builder, parser); } return builder.build(); } else if (parser.nextToken() == VALUE_NULL) { return null; } else { throw new ClientResponseException( "Query stats should be an object or null, not " + parser.getCurrentToken()); } } private static String statString(final String name, final Object value) { return String.join(": ", name, String.valueOf(value)); } /** * Gets the Transactional Compute Operations (TCOs) recorded. * * @return An int representing the compute ops. */ public int getComputeOps() { return computeOps; } /** * Gets the Transactional Read Operations (TROs) recorded. * * @return An int representing the read ops. */ public int getReadOps() { return readOps; } /** * Gets the Transactional Write Operations (TWOs) recorded. * * @return An int representing the write ops. */ public int getWriteOps() { return writeOps; } /** * Gets the query time in milliseconds. * * @return An int representing the query time in milliseconds. */ public int getQueryTimeMs() { return queryTimeMs; } /** * Gets the event processing time in milliseconds. * Applies to event feeds and event stream requests only. * * @return An int representing the processing time in milliseconds. */ public int getProcessingTimeMs() { return processingTimeMs; } /** * Gets the number of retries * for transaction contention. * * @return An int representing the number of transaction contention retries. */ public int getContentionRetries() { return contentionRetries; } /** * Gets the amount of data read from storage in bytes. * * @return An int representing the number of bytes read. */ public int getStorageBytesRead() { return storageBytesRead; } /** * Gets the amount of data written to storage in bytes. * * @return An int representing the number of bytes written. */ public int getStorageBytesWrite() { return storageBytesWrite; } /** * Gets a list of operation types that exceeded their plan * throughput limits. * * @return A list of operation types that exceeded their throughput limit. */ public List getRateLimitsHit() { return rateLimitsHit; } @Override public String toString() { if (this.stringValue == null) { this.stringValue = String.join(", ", statString("compute", computeOps), statString("read", readOps), statString("write", writeOps), statString("queryTime", queryTimeMs), statString("retries", contentionRetries), statString("storageRead", storageBytesRead), statString("storageWrite", storageBytesWrite), statString("limits", rateLimitsHit) ); } return this.stringValue; } static class Builder { private int computeOps; private int readOps; private int writeOps; private int queryTimeMs; private int contentionRetries; private int storageBytesRead; private int storageBytesWrite; private int processingTimeMs; private List rateLimitsHit; Builder computeOps(final int value) { this.computeOps = value; return this; } Builder readOps(final int value) { this.readOps = value; return this; } Builder writeOps(final int value) { this.writeOps = value; return this; } Builder queryTimeMs(final int value) { this.queryTimeMs = value; return this; } Builder contentionRetries(final int value) { this.contentionRetries = value; return this; } Builder storageBytesRead(final int value) { this.storageBytesRead = value; return this; } Builder storageBytesWrite(final int value) { this.storageBytesWrite = value; return this; } Builder processingTimeMs(final int value) { this.processingTimeMs = value; return this; } Builder rateLimitsHit(final List value) { this.rateLimitsHit = value; return this; } QueryStats build() { return new QueryStats(computeOps, readOps, writeOps, queryTimeMs, contentionRetries, storageBytesRead, storageBytesWrite, processingTimeMs, rateLimitsHit); } } } ``` ## File: src/main/java/com/fauna/response/QuerySuccess.java ```java package com.fauna.response; import java.util.Optional; public final class QuerySuccess extends QueryResponse { private final T data; private final String staticType; public QuerySuccess(final Builder builder) { super(builder); this.data = builder.getData(); this.staticType = builder.getStaticType(); } public T getData() { return data; } public Optional getStaticType() { return Optional.ofNullable(staticType); } } ``` ## File: src/main/java/com/fauna/types/BaseDocument.java ```java package com.fauna.types; import java.time.Instant; import java.util.Enumeration; import java.util.Hashtable; import java.util.Iterator; import java.util.Map; /** * Represents the base structure of a document with key-value pairs, a timestamp, and an associated collection. * This class provides functionality to store, retrieve, and iterate over document data. */ public abstract class BaseDocument implements Iterable { private final Hashtable data = new Hashtable<>(); private final Instant ts; private final Module collection; /** * Initializes a new instance of the {@code BaseDocument} class with the specified collection * and timestamp. * * @param coll The collection to which the document belongs. * @param ts The timestamp indicating when the document was created or last modified. */ public BaseDocument(final Module coll, final Instant ts) { this.collection = coll; this.ts = ts; } /** * Initializes a new instance of the {@code BaseDocument} class with the specified collection, * timestamp, and initial data. * * @param coll The collection to which the document belongs. * @param ts The timestamp of the document. * @param data Initial data for the document represented as key-value pairs. */ public BaseDocument( final Module coll, final Instant ts, final Map data) { this(coll, ts); this.data.putAll(data); } /** * Returns an iterator over the entries in this document. * Each entry represents a key-value pair in the document's data. * * @return an {@code Iterator} over the elements in this document. */ @Override public Iterator iterator() { return new Iterator<>() { private final Enumeration keys = data.keys(); @Override public boolean hasNext() { return keys.hasMoreElements(); } @Override public Entry next() { String key = keys.nextElement(); return new Entry(key, data.get(key)); } }; } /** * Represents a key-value pair in the document. */ public static class Entry { private final String key; private final Object value; /** * Initializes an entry with a specified key and value. * * @param key The key for the entry. * @param value The value associated with the key. */ public Entry(final String key, final Object value) { this.key = key; this.value = value; } /** * Gets the key of this entry. * * @return The key as a {@code String}. */ public String getKey() { return key; } /** * Gets the value associated with this entry's key. * * @return The value as an {@code Object}. */ public Object getValue() { return value; } } /** * Gets the timestamp of the document, indicating its creation or last modification time. * * @return An {@code Instant} representing the document's timestamp. */ public Instant getTs() { return ts; } /** * Gets the collection to which this document belongs. * * @return The {@code Module} representing the document's collection. */ public Module getCollection() { return collection; } /** * Retrieves a copy of the document's data as a {@code Map}. * * @return A {@code Map} containing the document's key-value pairs. */ public Map getData() { return data; } /** * Returns the number of key-value pairs contained in the document. * * @return The total number of key-value pairs in the document. */ public int size() { return data.size(); } /** * Determines whether the document contains the specified key. * * @param key The key to search for in the document. * @return {@code true} if the document contains an element with the specified key; * otherwise, {@code false}. */ public boolean containsKey(final String key) { return data.containsKey(key); } /** * Retrieves the value associated with the specified key. * * @param key The key of the value to retrieve. * @return The value associated with the specified key, or {@code null} if the key is not present. */ public Object get(final String key) { return data.get(key); } } ``` ## File: src/main/java/com/fauna/types/BaseRef.java ```java package com.fauna.types; /** * Represents a reference to a document within a collection. * This abstract class serves as a base for specific types of document references, * encapsulating the collection to which the reference belongs. */ public abstract class BaseRef { private final Module collection; /** * Constructs a new {@code BaseRef} object with the specified collection. * * @param coll The module to which the document reference belongs. */ public BaseRef(final Module coll) { this.collection = coll; } /** * Gets the collection to which this reference belongs. * * @return The {@code Module} representing the collection associated with this reference. */ public Module getCollection() { return collection; } /** * Indicates whether some other object is "equal to" this reference. * This method should be overridden in subclasses to provide specific equality logic. * * @param o the reference object with which to compare. * @return {@code true} if this reference is the same as the object argument; * {@code false} otherwise. */ @Override public abstract boolean equals(Object o); /** * Returns a hash code value for the object. * This method should be overridden in subclasses to provide specific hash code logic. * * @return a hash code value for this reference. */ @Override public abstract int hashCode(); } ``` ## File: src/main/java/com/fauna/types/Document.java ```java package com.fauna.types; import java.time.Instant; import java.util.Map; import java.util.Objects; /** * Represents an immutable document with an ID, associated collection, timestamp, and optional key-value data. * This class extends {@link BaseDocument} to provide additional document-specific functionality, * such as unique identification and data equality checks. */ public final class Document extends BaseDocument { private final String id; /** * Initializes a new instance of the {@code Document} class with the specified ID, collection, and timestamp. * * @param id The unique string identifier of the document. * @param coll The module (collection) to which the document belongs. * @param ts The timestamp indicating the document's creation or last modification. */ public Document(final String id, final Module coll, final Instant ts) { super(coll, ts); this.id = id; } /** * Initializes a new instance of the {@code Document} class with the specified ID, collection, * timestamp, and additional key-value data. * * @param id The unique string identifier of the document. * @param coll The module (collection) to which the document belongs. * @param ts The timestamp indicating the document's creation or last modification. * @param data Additional data for the document, represented as a map of key-value pairs. */ public Document( final String id, final Module coll, final Instant ts, final Map data) { super(coll, ts, data); this.id = id; } /** * Gets the unique identifier for this document. * * @return A {@code String} representing the document's unique ID. */ public String getId() { return id; } /** * Checks if this document is equal to another object. Two documents are considered equal * if they have the same ID, timestamp, collection, and data content. * * @param o The object to compare with this document for equality. * @return {@code true} if the specified object is equal to this document; otherwise, {@code false}. */ @Override public boolean equals(final Object o) { if (this == o) { return true; } if (o == null) { return false; } if (getClass() != o.getClass()) { return false; } Document c = (Document) o; return id.equals(c.id) && getTs().equals(c.getTs()) && getCollection().equals(c.getCollection()) && getData().equals(c.getData()); } /** * Returns a hash code value for this document based on its ID, timestamp, collection, and data. * * @return An integer hash code for this document. */ @Override public int hashCode() { return Objects.hash(id, getTs(), getCollection(), getData()); } } ``` ## File: src/main/java/com/fauna/types/DocumentRef.java ```java package com.fauna.types; import java.util.Objects; /** * Represents a reference to a specific document within a collection. * This class provides a unique identifier for the document and references * the collection to which it belongs. */ public final class DocumentRef extends BaseRef { private final String id; /** * Constructs a new {@code DocumentRef} object with the specified ID and collection. * * @param id The unique string identifier of the document reference. * @param coll The module (collection) to which the document reference belongs. */ public DocumentRef(final String id, final Module coll) { super(coll); this.id = id; } /** * Gets the unique identifier of the document reference. * * @return A {@code String} representing the document reference ID. */ public String getId() { return id; } /** * Checks if this document reference is equal to another object. Two document references * are considered equal if they have the same ID and belong to the same collection. * * @param o The object to compare with this document reference for equality. * @return {@code true} if the specified object is equal to this document reference; otherwise, {@code false}. */ @Override public boolean equals(final Object o) { if (this == o) { return true; } if (o == null) { return false; } if (getClass() != o.getClass()) { return false; } DocumentRef c = (DocumentRef) o; return id.equals(c.id) && getCollection().equals(c.getCollection()); } /** * Returns a hash code value for this document reference based on its ID and collection. * * @return An integer hash code for this document reference. */ @Override public int hashCode() { return Objects.hash(id, getCollection()); } } ``` ## File: src/main/java/com/fauna/types/Module.java ```java package com.fauna.types; import java.util.Objects; /** * Represents a module in Fauna Query Language (FQL), which serves as a symbolic object * with associated methods. Modules can represent built-in FQL objects, such as "Collection" or "Math", * as well as custom entities like user-defined collections. *

* For example, a specific collection named "MyCollection" can be represented as a {@code Module} in Java * to enable document creation via the Fauna Java driver: *

 *     var q = fql(
 *         "${coll}.create({foo:'bar'})",
 *         Map.of("coll", new Module("MyCollection"))
 *     );
 *
 *     client.query(q);
 * 
*/ public final class Module { private final String name; /** * Constructs a new {@code Module} object with the specified name. * * @param name The name of the module, representing either a built-in FQL object or a user-defined collection. */ public Module(final String name) { this.name = name; } /** * Gets the name of this module as a string representation. * * @return A {@code String} representing the module's name. */ public String getName() { return name; } /** * Determines if this module is equal to another object. Two modules are considered equal * if they have the same name. * * @param obj The object to compare with this module for equality. * @return {@code true} if the specified object is equal to this module; otherwise, {@code false}. */ @Override public boolean equals(final Object obj) { if (this == obj) { return true; } if (obj == null || getClass() != obj.getClass()) { return false; } Module module = (Module) obj; return Objects.equals(name, module.name); } /** * Returns a hash code value for this module based on its name. * * @return An integer hash code for this module. */ @Override public int hashCode() { return Objects.hash(name); } } ``` ## File: src/main/java/com/fauna/types/NamedDocument.java ```java package com.fauna.types; import java.time.Instant; import java.util.Map; import java.util.Objects; /** * Represents a document identified by a "name" rather than an "id". * This class is commonly used for documents in system collections where a unique name * (e.g., for a Role) is more relevant than a numeric or auto-generated ID. */ public final class NamedDocument extends BaseDocument { /** * The unique name identifier for this document. */ private final String name; /** * Initializes a new instance of the {@code NamedDocument} class with the specified * name, collection, and timestamp. * * @param name The unique string name of the document. * @param coll The module (collection) to which the document belongs. * @param ts The timestamp indicating the document's creation or last modification. */ public NamedDocument( final String name, final Module coll, final Instant ts) { super(coll, ts); this.name = name; } /** * Initializes a new instance of the {@code NamedDocument} class with the specified * name, collection, timestamp, and additional data. * * @param name The unique string name of the document. * @param coll The module (collection) to which the document belongs. * @param ts The timestamp indicating the document's creation or last modification. * @param data Additional key-value data to store in the document. */ public NamedDocument( final String name, final Module coll, final Instant ts, final Map data) { super(coll, ts, data); this.name = name; } /** * Gets the unique name of the document. * * @return A {@code String} representing the document's name. */ public String getName() { return name; } /** * Checks if this document is equal to another object. Two documents are considered equal * if they have the same name, timestamp, collection, and data. * * @param o The object to compare with this document for equality. * @return {@code true} if the specified object is equal to this document; otherwise, {@code false}. */ @Override public boolean equals(final Object o) { if (this == o) { return true; } if (o == null) { return false; } if (getClass() != o.getClass()) { return false; } NamedDocument c = (NamedDocument) o; return name.equals(c.name) && getTs().equals(c.getTs()) && getCollection().equals(c.getCollection()) && getData().equals(c.getData()); } /** * Returns a hash code value for this document based on its name, timestamp, collection, and data. * * @return An integer hash code for this document. */ @Override public int hashCode() { return Objects.hash(name, getTs(), getCollection(), getData()); } } ``` ## File: src/main/java/com/fauna/types/NamedDocumentRef.java ```java package com.fauna.types; import java.util.Objects; /** * Represents a reference to a document identified by a "name" instead of an "id". * This class is used for references to system collection documents where a unique name * (e.g., for a Role) is used instead of a numeric or auto-generated ID. */ public final class NamedDocumentRef extends BaseRef { private final String name; /** * Constructs a new {@code NamedDocumentRef} object with the specified name and collection. * * @param name The unique string name identifying the document reference. * @param coll The module (collection) to which the named document reference belongs. */ public NamedDocumentRef( final String name, final Module coll) { super(coll); this.name = name; } /** * Gets the unique name of the document reference. * * @return A {@code String} representing the name of the document reference. */ public String getName() { return name; } /** * Checks if this document reference is equal to another object. Two document references * are considered equal if they have the same name and belong to the same collection. * * @param o The object to compare with this document reference for equality. * @return {@code true} if the specified object is equal to this document reference; otherwise, {@code false}. */ @Override public boolean equals(final Object o) { if (this == o) { return true; } if (o == null) { return false; } if (getClass() != o.getClass()) { return false; } NamedDocumentRef c = (NamedDocumentRef) o; return name.equals(c.name) && getCollection().equals(c.getCollection()); } /** * Returns a hash code value for this document reference based on its name and collection. * * @return An integer hash code for this document reference. */ @Override public int hashCode() { return Objects.hash(name, getCollection()); } } ``` ## File: src/main/java/com/fauna/types/NonNullDocument.java ```java package com.fauna.types; import java.util.Objects; /** * Represents a document that is guaranteed to have a non-null value. * This class extends {@link NullableDocument} and enforces non-null data * for the document by disallowing null values in its constructor. * * @param The type of the document content. */ public final class NonNullDocument extends NullableDocument { /** * Constructs a {@code NonNullDocument} with the specified non-null value. * * @param val The document's content of type {@code T}. Must not be null. * @throws NullPointerException if {@code val} is null. */ public NonNullDocument(final T val) { super(val); } /** * Retrieves the non-null wrapped value of the document. * * @return The non-null wrapped value of type {@code T}. */ @Override public T get() { return getUnderlyingValue(); } /** * Retrieves the non-null wrapped value of the document. * This method provides compatibility for default serialization. * * @return The non-null wrapped value of type {@code T}. */ public T getValue() { return get(); } /** * Checks if this document is equal to another object. * Two {@code NonNullDocument} objects are considered equal if they hold values of the same type and content. * * @param o The object to compare with this document for equality. * @return {@code true} if the specified object is equal to this document; otherwise, {@code false}. */ @Override public boolean equals(final Object o) { if (this == o) { return true; } if (o == null) { return false; } if (getClass() != o.getClass()) { return false; } if (get().getClass() != ((NonNullDocument) o).get().getClass()) { return false; } return get().equals(((NonNullDocument) o).get()); } /** * Returns a hash code value for this document based on its non-null value. * * @return An integer hash code for this document. */ @Override public int hashCode() { return Objects.hash(get()); } } ``` ## File: src/main/java/com/fauna/types/NullableDocument.java ```java package com.fauna.types; /** * Represents a generic document wrapper that may hold a value representing a document. * This abstract class provides a base for documents that can optionally hold a value. * * @param The type of the document's content. */ public abstract class NullableDocument { private final T value; /** * Constructs a {@code NullableDocument} without a value, initializing it to {@code null}. */ public NullableDocument() { this.value = null; } /** * Constructs a {@code NullableDocument} with the specified value. * * @param val The value to wrap, which may be null. */ public NullableDocument(final T val) { this.value = val; } /** * Retrieves the document's value. This method must be implemented by subclasses * to specify how the value should be accessed. * * @return The document's content of type {@code T}. */ public abstract T get(); /** * Provides protected access to the underlying value, allowing subclasses * to directly access the stored value without additional logic. * * @return The underlying value of type {@code T}, which may be {@code null}. */ protected T getUnderlyingValue() { return value; } } ``` ## File: src/main/java/com/fauna/types/NullDocument.java ```java package com.fauna.types; import com.fauna.exception.NullDocumentException; import java.util.Objects; /** * Represents a document that is explicitly null, providing information about the cause of its null state. * This class extends {@link NullableDocument} and throws a {@link NullDocumentException} when accessed. * * @param The type of the document content, although it will not contain an actual value. */ public final class NullDocument extends NullableDocument { private final String id; private final Module coll; private final String cause; /** * Constructs a {@code NullDocument} with the specified ID, collection, and cause of nullity. * * @param id The unique identifier of the document. * @param coll The module (collection) to which this null document belongs. * @param cause A description of the reason why the document is null. */ public NullDocument(final String id, final Module coll, final String cause) { super(); this.id = id; this.coll = coll; this.cause = cause; } /** * Retrieves the cause of the document's null state. * * @return A {@code String} describing the cause of the null document. */ public String getCause() { return cause; } /** * Throws a {@link NullDocumentException} when called, as this document is explicitly null. * * @return Never returns a value, as it always throws an exception. * @throws NullDocumentException Always thrown to indicate that this is a null document. */ @Override public T get() { throw new NullDocumentException(id, coll, cause); } /** * Retrieves the ID of the null document. * * @return A {@code String} representing the document's ID. */ public String getId() { return id; } /** * Retrieves the collection associated with the null document. * * @return A {@code Module} representing the collection to which this null document belongs. */ public Module getCollection() { return coll; } /** * Checks if this null document is equal to another object. * Two {@code NullDocument} objects are considered equal if they have the same ID, collection, and cause. * * @param o The object to compare with this null document for equality. * @return {@code true} if the specified object is equal to this null document; otherwise, {@code false}. */ @Override public boolean equals(final Object o) { if (this == o) { return true; } if (o == null) { return false; } if (getClass() != o.getClass()) { return false; } var c = (NullDocument) o; return id.equals(c.getId()) && coll.equals(c.getCollection()) && cause.equals(c.getCause()); } /** * Returns a hash code value for this null document based on its ID, collection, and cause. * * @return An integer hash code for this null document. */ @Override public int hashCode() { return Objects.hash(id, coll, cause); } } ``` ## File: src/main/java/com/fauna/types/package-info.java ```java /** * Classes representing standard return types from Fauna. */ package com.fauna.types; ``` ## File: src/main/java/com/fauna/types/Page.java ```java package com.fauna.types; import com.fauna.query.AfterToken; import java.util.List; import java.util.Objects; import java.util.Optional; /** * Represents a page of data in a Fauna Set. Supports pagination with an optional `after` * token for retrieving additional pages. * * @param The type of data contained within the page. */ public final class Page { private final List data; private final String after; /** * Constructs a {@code Page} with the specified data and an optional after token. * * @param data The list of data items belonging to this page. * @param after The after token for pagination, which may be null if there are no more pages. */ public Page(final List data, final String after) { this.data = data; this.after = after; } /** * Retrieves the data items contained in this page. * * @return A {@code List} of data items belonging to this page. */ public List getData() { return data; } /** * Retrieves the optional after token for pagination. If present, this token can be used to * request the next page of results from Fauna. * * @return An {@code Optional} representing the after token, or an empty {@code Optional} if no token * is present. */ public Optional getAfter() { return AfterToken.fromString(after); } /** * Checks if this page is equal to another object. Two pages are considered equal * if they have the same data and after token. * * @param o The object to compare with this page for equality. * @return {@code true} if the specified object is equal to this page; otherwise, {@code false}. */ @Override public boolean equals(final Object o) { if (this == o) { return true; } if (o == null) { return false; } if (o instanceof Page) { @SuppressWarnings("rawtypes") Page c = (Page) o; return Objects.equals(after, c.after) && data.equals(c.data); } else { return false; } } /** * Returns a hash code value for this page based on its data and after token. * * @return An integer hash code for this page. */ @Override public int hashCode() { return Objects.hash(after, data); } } ```