
com.justinsb.etcd.EtcdClient Maven / Gradle / Ivy
package com.justinsb.etcd;
import com.google.common.base.Charsets;
import com.google.common.base.Splitter;
import com.google.common.collect.Lists;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.SettableFuture;
import com.google.gson.*;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.StatusLine;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpDelete;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPut;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.concurrent.FutureCallback;
import org.apache.http.impl.nio.client.CloseableHttpAsyncClient;
import org.apache.http.impl.nio.client.HttpAsyncClients;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.util.EntityUtils;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutionException;
@SuppressWarnings("WeakerAccess")
public class EtcdClient {
private static final Gson gson = new GsonBuilder().create();
private final CloseableHttpAsyncClient httpClient;
private final URI baseUri;
private static CloseableHttpAsyncClient buildDefaultHttpClient() {
// TODO: Increase timeout??
final RequestConfig requestConfig = RequestConfig.custom().build();
final CloseableHttpAsyncClient httpClient = HttpAsyncClients.custom().setDefaultRequestConfig(requestConfig).build();
httpClient.start();
return httpClient;
}
public EtcdClient(final URI baseUri) {
this(baseUri, buildDefaultHttpClient());
}
public EtcdClient(final URI baseUri, final CloseableHttpAsyncClient httpClient) {
String uri = baseUri.toString();
if (!uri.endsWith("/")) {
uri += "/";
this.baseUri = URI.create(uri);
} else {
this.baseUri = baseUri;
}
this.httpClient = httpClient;
}
/**
* Retrieves a key. Returns null if not found.
*/
public EtcdResult get(final String key) throws EtcdClientException {
final URI uri = buildKeyUri("v2/keys", key, "");
final HttpGet request = new HttpGet(uri);
final EtcdResult result = syncExecute(request, new int[]{200, 404}, 100);
if (result.isError()) {
if (result.errorCode == 100) {
return null;
}
}
return result;
}
/**
* Deletes the given key
*/
public EtcdResult delete(final String key) throws EtcdClientException {
final URI uri = buildKeyUri("v2/keys", key, "");
final HttpDelete request = new HttpDelete(uri);
return syncExecute(request, new int[]{200, 404});
}
/**
* Sets a key to a new value
*/
public EtcdResult set(final String key, final String value) throws EtcdClientException {
return set(key, value, null);
}
/**
* Sets a key to a new value with an (optional) ttl
*/
public EtcdResult set(final String key, final String value, final Integer ttl) throws EtcdClientException {
final List data = Lists.newArrayList();
data.add(new BasicNameValuePair("value", value));
if (ttl != null) {
data.add(new BasicNameValuePair("ttl", Integer.toString(ttl)));
}
return set0(key, data, new int[]{200, 201});
}
/**
* Creates a directory
*/
public EtcdResult createDirectory(final String key) throws EtcdClientException {
final List data = Lists.newArrayList();
data.add(new BasicNameValuePair("dir", "true"));
return set0(key, data, new int[]{200, 201});
}
/**
* Lists a directory
*/
public List listDirectory(final String key) throws EtcdClientException {
final EtcdResult result = get(key + "/");
if (result == null || result.node == null) {
return null;
}
return result.node.nodes;
}
/**
* Delete a directory
*/
public EtcdResult deleteDirectory(final String key) throws EtcdClientException {
final URI uri = buildKeyUri("v2/keys", key, "?dir=true");
final HttpDelete request = new HttpDelete(uri);
return syncExecute(request, new int[]{202});
}
/**
* Sets a key to a new value, if the value is a specified value
*/
public EtcdResult cas(final String key, final String prevValue, final String value) throws EtcdClientException {
final List data = Lists.newArrayList();
data.add(new BasicNameValuePair("value", value));
data.add(new BasicNameValuePair("prevValue", prevValue));
return set0(key, data, new int[]{200, 412}, 101);
}
/**
* Watches the given subtree
*/
public ListenableFuture watch(final String key) throws EtcdClientException {
return watch(key, null, false);
}
/**
* Watches the given subtree
*/
public ListenableFuture watch(final String key, final Long index, final boolean recursive) throws EtcdClientException {
String suffix = "?wait=true";
if (index != null) {
suffix += "&waitIndex=" + index;
}
if (recursive) {
suffix += "&recursive=true";
}
final URI uri = buildKeyUri("v2/keys", key, suffix);
final HttpGet request = new HttpGet(uri);
return asyncExecute(request, new int[]{200});
}
/**
* Gets the etcd version
*/
public String getVersion() throws EtcdClientException {
final URI uri = baseUri.resolve("version");
final HttpGet request = new HttpGet(uri);
// Technically not JSON, but it'll work
// This call is the odd one out
final JsonResponse s = syncExecuteJson(request, 200);
if (s.httpStatusCode != 200) {
throw new EtcdClientException("Error while fetching versions", s.httpStatusCode);
}
return s.json;
}
private EtcdResult set0(final String key, final List data, final int[] httpErrorCodes, final int... expectedErrorCodes)
throws EtcdClientException {
final URI uri = buildKeyUri("v2/keys", key, "");
final HttpPut request = new HttpPut(uri);
final UrlEncodedFormEntity entity = new UrlEncodedFormEntity(data, Charsets.UTF_8);
request.setEntity(entity);
return syncExecute(request, httpErrorCodes, expectedErrorCodes);
}
public EtcdResult listChildren(final String key) throws EtcdClientException {
final URI uri = buildKeyUri("v2/keys", key, "/");
final HttpGet request = new HttpGet(uri);
return syncExecute(request, new int[]{200});
}
protected ListenableFuture asyncExecute(final HttpUriRequest request, final int[] expectedHttpStatusCodes, final int... expectedErrorCodes)
throws EtcdClientException {
final ListenableFuture json = asyncExecuteJson(request, expectedHttpStatusCodes);
return Futures.transformAsync(json, json1 -> {
final EtcdResult result = jsonToEtcdResult(json1, expectedErrorCodes);
return Futures.immediateFuture(result);
});
}
protected EtcdResult syncExecute(final HttpUriRequest request, final int[] expectedHttpStatusCodes, final int... expectedErrorCodes) throws EtcdClientException {
try {
return asyncExecute(request, expectedHttpStatusCodes, expectedErrorCodes).get();
} catch (final InterruptedException e) {
Thread.currentThread().interrupt();
throw new EtcdClientException("Interrupted during request", e);
} catch (final ExecutionException e) {
throw unwrap(e);
}
}
private EtcdClientException unwrap(final ExecutionException e) {
final Throwable cause = e.getCause();
if (cause instanceof EtcdClientException) {
return (EtcdClientException) cause;
}
return new EtcdClientException("Error executing request", e);
}
private EtcdResult jsonToEtcdResult(final JsonResponse response, final int... expectedErrorCodes) throws EtcdClientException {
if (response == null || response.json == null) {
return null;
}
final EtcdResult result = parseEtcdResult(response.json);
if (result.isError()) {
if (!contains(expectedErrorCodes, result.errorCode)) {
throw new EtcdClientException(result.message, result);
}
}
return result;
}
private EtcdResult parseEtcdResult(final String json) throws EtcdClientException {
try {
return gson.fromJson(json, EtcdResult.class);
} catch (final JsonParseException e) {
throw new EtcdClientException("Error parsing response from etcd", e);
}
}
private static boolean contains(final int[] list, final int find) {
for (final int aList : list) {
if (aList == find) {
return true;
}
}
return false;
}
protected List syncExecuteList(final HttpUriRequest request) throws EtcdClientException {
final JsonResponse response = syncExecuteJson(request, 200);
if (response.json == null) {
return null;
}
if (response.httpStatusCode != 200) {
final EtcdResult etcdResult = parseEtcdResult(response.json);
throw new EtcdClientException("Error listing keys", etcdResult);
}
try {
final List ret = new ArrayList<>();
final JsonParser parser = new JsonParser();
final JsonArray array = parser.parse(response.json).getAsJsonArray();
for (int i = 0; i < array.size(); i++) {
final EtcdResult next = gson.fromJson(array.get(i), EtcdResult.class);
ret.add(next);
}
return ret;
} catch (final JsonParseException e) {
throw new EtcdClientException("Error parsing response from etcd", e);
}
}
protected JsonResponse syncExecuteJson(final HttpUriRequest request, final int... expectedHttpStatusCodes) throws EtcdClientException {
try {
return asyncExecuteJson(request, expectedHttpStatusCodes).get();
} catch (final InterruptedException e) {
Thread.currentThread().interrupt();
throw new EtcdClientException("Interrupted during request processing", e);
} catch (final ExecutionException e) {
throw unwrap(e);
}
}
protected ListenableFuture asyncExecuteJson(final HttpUriRequest request, final int[] expectedHttpStatusCodes) throws EtcdClientException {
final ListenableFuture response = asyncExecuteHttp(request);
return Futures.transformAsync(response, httpResponse -> {
final JsonResponse json = extractJsonResponse(httpResponse, expectedHttpStatusCodes);
return Futures.immediateFuture(json);
});
}
/**
* We need the status code & the response to parse an error response.
*/
static class JsonResponse {
final String json;
final int httpStatusCode;
public JsonResponse(final String json, final int statusCode) {
this.json = json;
this.httpStatusCode = statusCode;
}
}
protected JsonResponse extractJsonResponse(final HttpResponse httpResponse, final int[] expectedHttpStatusCodes) throws EtcdClientException {
try {
final StatusLine statusLine = httpResponse.getStatusLine();
final int statusCode = statusLine.getStatusCode();
String json = null;
if (httpResponse.getEntity() != null) {
try {
json = EntityUtils.toString(httpResponse.getEntity());
} catch (final IOException e) {
throw new EtcdClientException("Error reading response", e);
}
}
if (!contains(expectedHttpStatusCodes, statusCode)) {
if (statusCode != 400 || json == null) {
throw new EtcdClientException("Error response from etcd: " + statusLine.getReasonPhrase(), statusCode);
}
}
return new JsonResponse(json, statusCode);
} finally {
close(httpResponse);
}
}
private URI buildKeyUri(final String prefix, String key, final String suffix) {
final StringBuilder sb = new StringBuilder();
sb.append(prefix);
if (key.startsWith("/")) {
key = key.substring(1);
}
for (final String token : Splitter.on('/').split(key)) {
sb.append("/");
sb.append(urlEscape(token));
}
sb.append(suffix);
return baseUri.resolve(sb.toString());
}
protected ListenableFuture asyncExecuteHttp(final HttpUriRequest request) {
final SettableFuture future = SettableFuture.create();
httpClient.execute(request, new FutureCallback() {
public void completed(final HttpResponse result) {
future.set(result);
}
public void failed(final Exception ex) {
future.setException(ex);
}
public void cancelled() {
future.setException(new InterruptedException());
}
});
return future;
}
public static void close(final HttpResponse response) {
if (response == null) {
return;
}
final HttpEntity entity = response.getEntity();
if (entity != null) {
EntityUtils.consumeQuietly(entity);
}
}
protected static String urlEscape(final String s) {
try {
return URLEncoder.encode(s, "UTF-8");
} catch (final UnsupportedEncodingException e) {
throw new IllegalStateException();
}
}
public static String format(final Object o) {
try {
return gson.toJson(o);
} catch (final Exception e) {
return "Error formatting: " + e.getMessage();
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy