All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.faunadb.common.Connection Maven / Gradle / Ivy

The newest version!
package com.faunadb.common;

import com.codahale.metrics.MetricRegistry;
import com.codahale.metrics.Timer;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.faunadb.common.http.DriverVersionChecker;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOError;
import java.io.IOException;
import java.net.*;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Flow;
import java.util.concurrent.atomic.AtomicLong;
import java.util.stream.Collectors;

import static java.lang.String.format;

/**
 * The HTTP Connection adapter for FaunaDB drivers.
 *
 * 

Relies on {@link java.net.http.HttpClient} * for the underlying implementation.

*/ public class Connection { private static final String API_VERSION = "5"; private static final Duration DEFAULT_CONNECTION_TIMEOUT = Duration.ofSeconds(10); private static final Duration DEFAULT_REQUEST_TIMEOUT = Duration.ofSeconds(60); private static final String DEFAULT_USER_AGENT = "Fauna JVM Http Client"; private static final URL FAUNA_ROOT; static { try { FAUNA_ROOT = new URL("https://db.fauna.com"); } catch (MalformedURLException e) { throw new IOError(e); // won't happen } } public enum JvmDriver { JAVA("Java"), SCALA("Scala"); private String stringValue; JvmDriver(String stringValue) { this.stringValue = stringValue; } @Override public String toString() { return this.stringValue; } } /** * Returns a new {@link Builder} instance. * * @return a new {@link Builder} */ public static Builder builder() { return new Builder(); } private static class RuntimeEnvironmentHeader { private String runtime; private String driverVersion; private String os; private String env; RuntimeEnvironmentHeader(JvmDriver jvmDriver, String scalaVersion, boolean checkNewVersion) { this.driverVersion = Connection.class.getPackage().getSpecificationVersion(); this.os = System.getProperty("os.name"); this.env = this.getRuntimeEnv(); this.runtime = String.format("java-%s", System.getProperty("java.version")); if (jvmDriver == JvmDriver.SCALA) { this.runtime = String.format("%s,scala-%s", this.runtime, scalaVersion); } if (checkNewVersion && !DriverVersionChecker.isAlreadyChecked()) { DriverVersionChecker.checkLatestVersion(); } } @Override public String toString() { return String.format( "driver=jvm-%s; runtime=%s; env=%s; os=%s", this.driverVersion, this.runtime, this.env, this.os ).toLowerCase(); } private String getRuntimeEnv() { if (System.getenv("NETLIFY_IMAGES_CDN_DOMAIN") != null) { return "Netlify"; } if (System.getenv("VERCEL") != null) { return "Vercel"; } if (System.getenv("PATH") != null && System.getenv("PATH").contains(".heroku")) { return "Heroku"; } if (System.getenv("AWS_LAMBDA_FUNCTION_VERSION") != null) { return "AWS Lambda"; } if (System.getenv("_") != null && System.getenv("_").contains("google")) { return "GCP Cloud Functions"; } if (System.getenv("GOOGLE_CLOUD_PROJECT") != null) { return "GCP Compute Instances"; } if (System.getenv("ORYX_ENV_TYPE") != null && System.getenv("WEBSITE_INSTANCE_ID") != null && System.getenv("ORYX_ENV_TYPE") == "AppService") { return "Azure Compute"; } return "Unknown"; } } /** * A builder for the {@link Connection} instance. Use the {@link Connection#builder} method to create * an instance of the {@link Builder} class. */ public static class Builder { private URL faunaRoot; private String authToken; private MetricRegistry metricRegistry; private long lastSeenTxn; private HttpClient client; private JvmDriver jvmDriver; private String scalaVersion; private Optional queryTimeout = Optional.empty(); private Optional userAgent = Optional.empty(); private boolean checkNewDriverVersion = true; private Map customHeaders; private Builder() { } /** * Sets the FaunaDB root URL for the {@link Connection} instance. * * @param root the root URL, as a RFC 2396 formatted string. Example: https://db.fauna.com * @return this {@link Builder} object * @throws MalformedURLException if a malformed url is provided */ public Builder withFaunaRoot(String root) throws MalformedURLException { this.faunaRoot = new URL(root); return this; } /** * Sets the FaunaDB root URL for the {@link Connection} instance. * * @param root the root URL * @return this {@link Builder} object */ public Builder withFaunaRoot(URL root) { this.faunaRoot = root; return this; } /** * Sets the authentication token or key for the {@link Connection} instance. * * @param token the auth token or key * @return this {@link Builder} object */ public Builder withAuthToken(String token) { this.authToken = token; return this; } /** * Sets a {@link MetricRegistry} for the {@link Connection} instance. * The {@link MetricRegistry} will be used to track connection level statistics. * * @param registry the {@link MetricRegistry} instance. * @return this {@link Builder} object */ public Builder withMetrics(MetricRegistry registry) { this.metricRegistry = registry; return this; } /** * Sets the Fauna driver to use for the connection. * * @param jvmDriver the {@link JvmDriver} to use for this connection. * @return this {@link Builder} object */ public Builder withJvmDriver(JvmDriver jvmDriver) { this.jvmDriver = jvmDriver; return this; } /** * Sets the Scala version to use for a requests header * * @param scalaVersion * @return this {@link Builder} object */ public Builder withScalaVersion(String scalaVersion) { this.scalaVersion = scalaVersion; return this; } /** * Sets the checkNewVersion variable for checking latets driver version * * @param checkNewVersion * @return this {@link Builder} object */ public Builder withCheckNewDriverVersion(boolean checkNewVersion) { this.checkNewDriverVersion = checkNewVersion; return this; } /** * Sets the last seen transaction time for the connection. * * @param txnTime the last seen transaction time in microseconds. * @return this {@link Builder} object */ public Builder withLastSeenTxn(long txnTime) { this.lastSeenTxn = txnTime; return this; } /** * Sets the client to use for the connection. * * @param client the {@link HttpClient} to use for this connection. * @return this {@link Builder} object */ public Builder withHttpClient(HttpClient client) { this.client = client; return this; } /** * Sets the global query timeout for this connection. * * @param timeout the query timeout value * @return this {@link Builder} object */ public Builder withQueryTimeout(Duration timeout) { this.queryTimeout = Optional.ofNullable(timeout); return this; } /** * Sets the User-Agent header for this connection. * * @param userAgent the query timeout value * @return this {@link Builder} object */ public Builder withUserAgent(String userAgent) { this.userAgent = Optional.ofNullable(userAgent); return this; } public Builder withCustomHeaders(Map headers) { this.customHeaders = headers; return this; } /** * @return a newly constructed {@link Connection} with its configuration based on * the settings of the {@link Builder} instance. */ public Connection build() { System.setProperty("jdk.httpclient.keepalive.timeout", "3"); MetricRegistry registry; registry = Objects.requireNonNullElseGet(metricRegistry, MetricRegistry::new); URL root; root = Objects.requireNonNullElseGet(faunaRoot, () -> FAUNA_ROOT); HttpClient http; http = Objects.requireNonNullElseGet(client, () -> // TODO: [DRV-169] allow users to override default executor HttpClient.newBuilder() .connectTimeout(DEFAULT_CONNECTION_TIMEOUT) .build() ); String connectionUserAgent = userAgent.orElse(DEFAULT_USER_AGENT); String runtimeEnvironmentHeader = new RuntimeEnvironmentHeader(jvmDriver, scalaVersion, checkNewDriverVersion).toString(); return new Connection(root, authToken, http, registry, runtimeEnvironmentHeader, lastSeenTxn, queryTimeout, connectionUserAgent, customHeaders); } } private static final String X_FAUNADB_HOST = "X-FaunaDB-Host"; private static final String X_FAUNADB_BUILD = "X-FaunaDB-Build"; private static final String X_DRIVER_ENV = "X-Driver-Env"; private static final String X_QUERY_TIMEOUT = "X-Query-Timeout"; private static final String X_LAST_SEEN_TXN = "X-Last-Seen-Txn"; private static final String X_FAUNADB_API_VERSION = "X-FaunaDB-API-Version"; private static final String USER_AGENT = "User-Agent"; private final URL faunaRoot; private final String authHeader; private final String runtimeEnvironmentHeader; private HttpClient client; private final MetricRegistry registry; private final Optional defaultQueryTimeout; private final String userAgent; private final Map customHeaders; private final Logger log = LoggerFactory.getLogger(getClass()); private final ObjectMapper json = new ObjectMapper(); private final AtomicLong txnTime = new AtomicLong(0L); private Connection(URL faunaRoot, String authToken, HttpClient client, MetricRegistry registry, String runtimeEnvironmentHeader, long lastSeenTxn, Optional defaultQueryTimeout, String userAgent, Map customHeaders) { this.faunaRoot = faunaRoot; this.authHeader = generateAuthHeader(authToken); this.runtimeEnvironmentHeader = runtimeEnvironmentHeader; this.client = client; this.registry = registry; this.txnTime.set(lastSeenTxn); this.defaultQueryTimeout = defaultQueryTimeout; this.userAgent = userAgent; this.customHeaders = customHeaders; } /** * Creates a new {@link Connection} sharing its underneath I/O resources. Queries submitted to a * session connection will be authenticated with the token provided. * * @param authToken the token or key to be used to authenticate requests to the new {@link Connection} * @return a new {@link Connection} */ public Connection newSessionConnection(String authToken) { return new Connection(faunaRoot, authToken, client, registry, runtimeEnvironmentHeader, getLastTxnTime(), defaultQueryTimeout, userAgent, customHeaders); } /** * Get the freshest timestamp reported to this client. */ public long getLastTxnTime() { return txnTime.get(); } /** * Sync the freshest timestamp seen by this client. * * This has no effect if more stale than the currently stored timestamp. * WARNING: This should be used only when coordinating timestamps across * multiple clients. Moving the timestamp arbitrarily forward into * the future will cause transactions to stall. */ public void syncLastTxnTime(long newTxnTime) { for (;;) { long oldTxnTime = getLastTxnTime(); if (oldTxnTime >= newTxnTime || txnTime.compareAndSet(oldTxnTime, newTxnTime)) { return; } } } /** * Issues a {@code GET} request with no parameters. * * @param path the relative path of the resource. * @param queryTimeout the query timeout for the current request. * @return a {@link CompletableFuture} containing the HTTP Response. */ public CompletableFuture> get(String path, Optional queryTimeout) { return performRequest("GET", path, Optional.empty(), Map.of(), queryTimeout); } /** * Issues a {@code GET} request with the provided request parameters. * * @param path the relative path of the resource. * @param params a map containing the request parameters. * @param queryTimeout the query timeout for the current request. * @return a {@code CompletableFuture} containing the HTTP response. */ public CompletableFuture> get(String path, Map> params, Optional queryTimeout) { return performRequest("GET", path, Optional.empty(), params, queryTimeout); } /** * Issues a {@code POST} request with the provided JSON request body. * * @param path the relative path of the resource. * @param body the JSON tree that will be serialized into the request body. * @param queryTimeout the query timeout for the current request. * @return a {@link CompletableFuture} containing the HTTP response. */ public CompletableFuture> post(String path, JsonNode body, Optional queryTimeout) { return performRequest("POST", path, Optional.of(body), Map.of(), queryTimeout); } /** * Issues a {@code PUT} request with the provided JSON request body. * * @param path the relative path of the resource. * @param body the JSON tree that will be serialized into the request body. * @param queryTimeout the query timeout for the current request. * @return a {@link CompletableFuture} containing the HTTP response. */ public CompletableFuture> put(String path, JsonNode body, Optional queryTimeout) { return performRequest("PUT", path, Optional.of(body), Map.of(), queryTimeout); } /** * Issues a {@code PATCH} request with the provided JSON request body. * * @param path the relative path of the resource. * @param body the JSON tree that will be serialized into the request body. * @param queryTimeout the query timeout for the current request. * @return a {@link CompletableFuture} containing the HTTP response. */ public CompletableFuture> patch(String path, JsonNode body, Optional queryTimeout) { return performRequest("PATCH", path, Optional.of(body), Map.of(), queryTimeout); } private static URI appendUri(URI oldUri, String queryKey, List queryValues) throws URISyntaxException { String urlEncodedKey = URLEncoder.encode(queryKey, StandardCharsets.UTF_8); String urlEncodedValue = queryValues.stream() .map(u -> URLEncoder.encode(u, StandardCharsets.UTF_8)) .collect(Collectors.joining(",")); String query = urlEncodedKey + "=" + urlEncodedValue; return new URI(oldUri.getScheme(), oldUri.getAuthority(), oldUri.getPath(), oldUri.getQuery() == null ? query : oldUri.getQuery() + "&" + query, oldUri.getFragment()); } private CompletableFuture> performRequest(String httpMethod, String path, Optional body, Map> params, final Optional requestQueryTimeout) { final Timer.Context ctx = registry.timer("fauna-request").time(); final CompletableFuture> rv = new CompletableFuture<>(); HttpRequest request; try { request = makeHttpRequest(httpMethod, path, body, params, requestQueryTimeout, HttpClient.Version.HTTP_1_1); } catch (IllegalArgumentException| MalformedURLException | URISyntaxException | JsonProcessingException ex) { rv.completeExceptionally(ex); return rv; } sendRequest(request).whenCompleteAsync((response, throwable) -> { ctx.stop(); if (throwable != null) { logFailure(request, throwable); rv.completeExceptionally(throwable); return; } Optional txnTimeHeader = response.headers().firstValue("x-txn-time"); txnTimeHeader.ifPresent(s -> syncLastTxnTime(Long.parseLong(s))); logSuccess(request, response); rv.complete(response); }); return rv; } private CompletableFuture> sendRequest(HttpRequest req) { return client.sendAsync(req, HttpResponse.BodyHandlers.ofString()); } public CompletableFuture>>> streamRequest(HttpRequest req) { return client.sendAsync(req, HttpResponse.BodyHandlers.ofPublisher()); } public CompletableFuture>>> performStreamRequest(String httpMethod, String path, JsonNode body, Map> params) { final Timer.Context ctx = registry.timer("fauna-request").time(); final CompletableFuture>>> rv = new CompletableFuture<>(); HttpRequest request; try { request = makeHttpRequest(httpMethod, path, Optional.of(body), params, Optional.empty(), HttpClient.Version.HTTP_2); } catch (MalformedURLException | URISyntaxException | JsonProcessingException ex) { rv.completeExceptionally(ex); return rv; } streamRequest(request).whenCompleteAsync((response, throwable) -> { ctx.stop(); if (throwable != null) { logFailure(request, throwable); rv.completeExceptionally(throwable); return; } Optional txnTimeHeader = response.headers().firstValue("x-txn-time"); txnTimeHeader.ifPresent(s -> syncLastTxnTime(Long.parseLong(s))); rv.complete(response); }); return rv; } private HttpRequest makeHttpRequest(String httpMethod, String path, Optional body, Map> params, Optional requestQueryTimeout, HttpClient.Version httpVersion) throws MalformedURLException, URISyntaxException, JsonProcessingException { URI requestUri = URI.create(mkUrl(path)); // Encode all query parameters for (Map.Entry> entry : params.entrySet()) { List values = entry.getValue(); if (!values.isEmpty()) { requestUri = appendUri(requestUri, entry.getKey(), values); } } HttpRequest.BodyPublisher bodyPublisher = HttpRequest.BodyPublishers.noBody(); if (body.isPresent()) { byte[] jsonBody = json.writeValueAsBytes(body.get()); bodyPublisher = HttpRequest.BodyPublishers.ofByteArray(jsonBody); } // If a query timeout has been given for the current request, // override the one from the Connection if any // or use a default value Duration queryTimeout = requestQueryTimeout.or(() -> defaultQueryTimeout).orElse(DEFAULT_REQUEST_TIMEOUT); Optional lastTxnTime = (getLastTxnTime() > 0) ? Optional.of(getLastTxnTime()) : Optional.empty(); HttpRequest.Builder requestBuilder = HttpRequest.newBuilder() .uri(requestUri) .version(httpVersion) .timeout(queryTimeout) .method(httpMethod, bodyPublisher) .headers( mkHeaders(queryTimeout) ); lastTxnTime.ifPresent(time -> requestBuilder.header(X_LAST_SEEN_TXN, Long.toString(time))); return requestBuilder.build(); } private String[] mkHeaders(Duration queryTimeout) { Map defaultHeaders = Map.of( "Authorization", authHeader, X_FAUNADB_API_VERSION, API_VERSION, USER_AGENT, userAgent, X_QUERY_TIMEOUT, String.valueOf(queryTimeout.toMillis()), X_DRIVER_ENV, runtimeEnvironmentHeader, "Content-type", "application/json; charset=utf-8" ); Map combinedHeaders = new HashMap<>(defaultHeaders); if (customHeaders != null) { for (Map.Entry entry : customHeaders.entrySet()) { combinedHeaders.put(entry.getKey(), entry.getValue()); } } String[] headersAsArray = new String[combinedHeaders.size() * 2]; int index = 0; for (Map.Entry entry : combinedHeaders.entrySet()) { headersAsArray[index] = entry.getKey(); headersAsArray[index + 1] = entry.getValue(); index += 2; } return headersAsArray; } private String mkUrl(String path) throws MalformedURLException { return new URL(faunaRoot, path).toString(); } private void logSuccess(HttpRequest request, HttpResponse response) { if (log.isDebugEnabled()) { String data = request.bodyPublisher().map(Object::toString).orElse("NoBody"); String body = Optional.ofNullable(response.body()).orElse(""); String host = response.headers().firstValue(X_FAUNADB_HOST).orElse("Unknown"); String build = response.headers().firstValue(X_FAUNADB_BUILD).orElse("Unknown"); log.debug( format("Request: %s %s: [%s]. Response: Status=%d, Fauna Host: %s, Fauna Build: %s: %s", request.method(), request.uri(), data, response.statusCode(), host, build, body)); } } private void logFailure(HttpRequest request, Throwable ex) { log.info( format("Request: %s %s: %s. Failed: %s", request.method(), request.uri(), request.bodyPublisher().map(Object::toString).orElse("NoBody"), ex.getMessage()), ex); } private static String generateAuthHeader(String authToken) { return "Bearer " + authToken; } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy