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

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 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 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