etcd.client.DefaultEtcdClient Maven / Gradle / Ivy
/*
* Copyright (c) 2014 Intellectual Reserve, Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package etcd.client;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufInputStream;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.handler.codec.http.DefaultFullHttpRequest;
import io.netty.handler.codec.http.FullHttpRequest;
import io.netty.handler.codec.http.FullHttpResponse;
import io.netty.handler.codec.http.HttpHeaders;
import io.netty.handler.codec.http.HttpMethod;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.netty.handler.codec.http.HttpVersion;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.time.Duration;
import java.time.Instant;
import java.time.format.DateTimeFormatter;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
class DefaultEtcdClient implements EtcdClient {
private static final ObjectMapper MAPPER = new ObjectMapper();
private final HttpClient client;
private final EventLoopGroup eventLoopGroup;
DefaultEtcdClient(EtcdClientBuilder builder) {
EventLoopGroup eventLoopGroup = builder.eventLoopGroup;
if (eventLoopGroup == null) {
this.eventLoopGroup = eventLoopGroup = new NioEventLoopGroup();
} else {
this.eventLoopGroup = null;
}
client = new HttpClient(eventLoopGroup, builder.executor, builder.servers, builder.retryOnConnectFailure);
}
@Override
public DeleteRequest prepareDelete(String key) {
return new DeleteRequestImpl(client, key);
}
@Override
public GetRequest prepareGet(String key) {
return new GetRequestImpl(client, key);
}
@Override
public SetRequest prepareSet(String key) {
return new SetRequestImpl(client, key);
}
@Override
public WatchRequest watch(String Key) {
throw new UnsupportedOperationException("The watch API isn't supported yet.");
}
@Override
public void close() {
if (eventLoopGroup != null) {
eventLoopGroup.shutdownGracefully();
}
}
private class GetRequestImpl extends AbstractRequest implements GetRequest {
private final String key;
private boolean consistent = false;
private boolean recursive = false;
private boolean sorted = false;
private boolean wait = false;
private Long waitIndex = null;
public GetRequestImpl(HttpClient client, String key) {
super(client);
key = validateKey(key);
this.key = key;
}
@Override
protected FullHttpRequest buildRequest() {
final StringBuilder uriBuilder = new StringBuilder();
uriBuilder.append("/v2/keys").append(key);
final StringBuilder queryBuilder = new StringBuilder();
if (consistent) {
appendQueryStringSeparator(queryBuilder);
queryBuilder.append("consistent=true");
}
if (recursive) {
appendQueryStringSeparator(queryBuilder);
queryBuilder.append("recursive=true");
}
if (sorted) {
appendQueryStringSeparator(queryBuilder);
queryBuilder.append("sorted=true");
}
if (wait) {
appendQueryStringSeparator(queryBuilder);
queryBuilder.append("wait=true");
}
if (waitIndex != null) {
appendQueryStringSeparator(queryBuilder);
queryBuilder.append("waitIndex=").append(waitIndex);
}
uriBuilder.append(queryBuilder);
return new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, uriBuilder.toString());
}
@Override
protected Result createResult(FullHttpResponse response) {
if (!response.getStatus().equals(HttpResponseStatus.OK)) {
throwException(response);
}
return marshalResult(response);
}
@Override
public GetRequest consistent() {
consistent = true;
return this;
}
@Override
public GetRequest recursive() {
recursive = true;
return this;
}
@Override
public GetRequest sorted() {
sorted = true;
return this;
}
@Override
public GetRequest waitForChange() {
wait = true;
return this;
}
@Override
public GetRequest waitIndex(long index) {
waitIndex = index;
return this;
}
}
private void throwException(FullHttpResponse response) {
try {
final ErrorBody errorBody = MAPPER.readValue(new ByteBufInputStream(response.content()), ErrorBody.class);
final String message = errorBody.message == null ? "Error executing request" : errorBody.message;
if (response.getStatus().code() == HttpResponseStatus.NOT_FOUND.code()) {
throw new KeyNotFoundException(message, errorBody.errorCode, errorBody.index, errorBody.cause);
}
final Long etcdIndex = Long.valueOf(response.headers().get("X-Etcd-Index"));
throw new EtcdRequestException(message, errorBody.errorCode, etcdIndex, errorBody.cause);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private class DeleteRequestImpl extends AbstractRequest implements DeleteRequest {
private final String key;
private String previousValue;
private Long previousIndex;
private boolean directory;
private boolean recursive;
public DeleteRequestImpl(HttpClient client, String key) {
super(client);
this.key = validateKey(key);
}
@Override
protected FullHttpRequest buildRequest() {
final StringBuilder uriBuilder = new StringBuilder();
uriBuilder.append("/v2/keys").append(key);
final StringBuilder queryBuilder = new StringBuilder();
if (previousValue != null) {
appendQueryStringSeparator(queryBuilder);
queryBuilder.append("prevValue=").append(urlEncode(previousValue));
}
if (previousIndex != null) {
appendQueryStringSeparator(queryBuilder);
queryBuilder.append("prevIndex=").append(previousIndex);
}
if (directory) {
appendQueryStringSeparator(queryBuilder);
queryBuilder.append("dir=true");
}
if (recursive) {
appendQueryStringSeparator(queryBuilder);
queryBuilder.append("recursive=true");
}
uriBuilder.append(queryBuilder);
return new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.DELETE, uriBuilder.toString());
}
@Override
protected Result createResult(FullHttpResponse response) {
if (!response.getStatus().equals(HttpResponseStatus.OK)) {
throwException(response);
}
return marshalResult(response);
}
@Override
public DeleteRequest previousValue(String value) {
previousValue = value;
return this;
}
@Override
public DeleteRequest previousIndex(long index) {
previousIndex = index;
return this;
}
@Override
public DeleteRequest directory() {
directory = true;
return this;
}
@Override
public DeleteRequest recursive() {
recursive = true;
return this;
}
}
private class SetRequestImpl extends AbstractRequest implements SetRequest {
private final String key;
private boolean directory = false;
private Duration timeToLive;
private String value;
private boolean mustExist;
private boolean mustNotExist;
private String previousValue;
private Long previousIndex;
private boolean inOrder;
private SetRequestImpl(HttpClient client, String key) {
super(client);
this.key = validateKey(key);
}
@Override
protected FullHttpRequest buildRequest() {
final HttpMethod method = inOrder ? HttpMethod.POST : HttpMethod.PUT;
final DefaultFullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, method, "/v2/keys" + key);
final StringBuilder body = new StringBuilder();
if (value != null) {
body.append("value=").append(urlEncode(value));
}
if (timeToLive != null) {
appendFieldSeparator(body);
body.append("ttl=").append(timeToLive.getSeconds());
}
if (directory) {
appendFieldSeparator(body);
body.append("dir=true");
}
if (mustExist && mustNotExist) {
throw new EtcdException("In what universe does it even makes sense for something to be required to both exist and not exist?");
}
if (mustExist) {
appendFieldSeparator(body);
body.append("prevExist=true");
}
if (mustNotExist) {
appendFieldSeparator(body);
body.append("prevExist=false");
}
if (previousValue != null) {
appendFieldSeparator(body);
body.append("prevValue=").append(urlEncode(previousValue));
}
if (previousIndex != null) {
appendFieldSeparator(body);
body.append("prevIndex=").append(previousIndex);
}
final byte[] content = body.toString().getBytes();
request.headers().add(HttpHeaders.Names.CONTENT_TYPE, HttpHeaders.Values.APPLICATION_X_WWW_FORM_URLENCODED + ";charset=utf-8");
request.headers().add(HttpHeaders.Names.CONTENT_LENGTH, content.length);
request.content().writeBytes(content);
return request;
}
@Override
protected Result createResult(FullHttpResponse response) {
if (!(response.getStatus().equals(HttpResponseStatus.CREATED) || response.getStatus().equals(HttpResponseStatus.OK))) {
throwException(response);
}
return marshalResult(response);
}
@Override
public SetRequest value(String value) {
this.value = value;
return this;
}
@Override
public SetRequest timeToLive(Duration duration) {
this.timeToLive = duration;
return this;
}
@Override
public SetRequest directory() {
directory = true;
return this;
}
@Override
public SetRequest mustExist() {
mustExist = true;
return this;
}
@Override
public SetRequest mustNotExist() {
mustNotExist = true;
return this;
}
@Override
public SetRequest previousValue(String value) {
this.previousValue = value;
return this;
}
@Override
public SetRequest previousIndex(long index) {
this.previousIndex = index;
return this;
}
@Override
public SetRequest inOrder() {
inOrder = true;
return this;
}
}
private void appendQueryStringSeparator(StringBuilder queryString) {
if (queryString.length() == 0) {
queryString.append('?');
} else if (queryString.length() > 0) {
queryString.append('&');
}
}
private void appendFieldSeparator(StringBuilder body) {
if (body.length() > 0) {
body.append('&');
}
}
private static String validateKey(String key) {
if (!key.startsWith("/")) {
key = "/" + key;
}
return key;
}
private String urlEncode(String value) {
try {
return URLEncoder.encode(value, "UTF-8");
} catch (UnsupportedEncodingException e) {
throw new Error(e);
}
}
private Result marshalResult(FullHttpResponse response) {
try {
final EtcdMeta meta = new EtcdMeta(
convertLong(response.headers().get("X-Etcd-Index")),
convertLong(response.headers().get("X-Raft-Index")),
convertLong(response.headers().get("X-Raft-Term"))
);
final ByteBuf content = response.content();
if (content.readableBytes() > 0) {
final ByteBufInputStream inputStream = new ByteBufInputStream(content);
final JsonResult json = MAPPER.readValue(inputStream, JsonResult.class);
return new Result() {
@Override
public EtcdMeta getResponseMeta() {
return meta;
}
@Override
public Action getAction() {
return json.action;
}
@Override
public Node getNode() {
return json.node;
}
@Override
public Optional getPreviousNode() {
return Optional.ofNullable(json.previousNode);
}
@Override
public String toString() {
return "Result {" +
"meta = " + getResponseMeta() +
", action = " + getAction() +
", node = " + getNode() +
", prevNode = " + getPreviousNode().orElse(null) +
"}";
}
};
} else {
throw new EtcdException("Empty response from server.");
}
} catch (IOException e) {
throw new EtcdException(e);
}
}
private static long convertLong(String value) {
if (value == null) {
return -1;
}
try {
return Long.valueOf(value);
} catch (NumberFormatException e) {
return -1;
}
}
private static class JsonResult {
private final Action action;
private final Node node;
private final Node previousNode;
@JsonCreator
private JsonResult(
@JsonProperty("action") String action,
@JsonProperty("node") JsonNode node,
@JsonProperty("prevNode") JsonNode previousNode) {
this.action = Action.valueOf(action.toUpperCase());
this.node = node;
this.previousNode = previousNode;
}
}
private static class JsonNode implements Node {
private final long createdIndex;
private final Long modifiedIndex;
private final String key;
private final String value;
private final Instant expiration;
private final Duration timeToLive;
private final boolean directory;
private final List extends Node> nodes;
@JsonCreator
private JsonNode(
@JsonProperty("createdIndex") long createdIndex,
@JsonProperty("modifiedIndex") Long modifiedIndex,
@JsonProperty("key") String key,
@JsonProperty("value") String value,
@JsonProperty("expiration") String expiration,
@JsonProperty("ttl") Long timeToLive,
@JsonProperty("dir") boolean directory,
@JsonProperty("nodes") List nodes) {
this.createdIndex = createdIndex;
this.modifiedIndex = modifiedIndex;
this.key = key;
this.value = value;
this.expiration = expiration == null ? null : parseDate(expiration);
this.timeToLive = timeToLive == null ? null : Duration.ofSeconds(timeToLive);
this.directory = directory;
this.nodes = nodes == null ? Collections.emptyList() : nodes;
}
private Instant parseDate(String expiration) {
return DateTimeFormatter.ISO_OFFSET_DATE_TIME.parse(expiration, Instant::from);
}
@Override
public long getCreatedIndex() {
return createdIndex;
}
@Override
public Optional getModifiedIndex() {
return Optional.ofNullable(modifiedIndex);
}
@Override
public String getKey() {
return key;
}
@Override
public Optional getValue() {
return Optional.ofNullable(value);
}
@Override
public Optional getExpiration() {
return Optional.ofNullable(expiration);
}
@Override
public Optional getTimetoLive() {
return Optional.ofNullable(timeToLive);
}
@Override
public boolean isDirectory() {
return directory;
}
@Override
public List extends Node> getNodes() {
return nodes;
}
@Override
public String toString() {
return "JsonNode{" +
"createdIndex=" + createdIndex +
", modifiedIndex=" + modifiedIndex +
", key='" + key + '\'' +
", value='" + value + '\'' +
", expiration=" + expiration +
", timeToLive=" + timeToLive +
", directory=" + directory +
", nodes=" + nodes +
'}';
}
}
private static class ErrorBody {
private final int errorCode;
private final String cause;
private final String message;
private final Long index;
private ErrorBody(
@JsonProperty("errorCode") int errorCode,
@JsonProperty("cause") String cause,
@JsonProperty("message") String message,
@JsonProperty("index") Long index) {
this.errorCode = errorCode;
this.cause = cause;
this.message = message;
this.index = index;
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy