package org.github.etcd.rest; import java.util.ArrayList; import java.util.Arrays; import java.util.Date; import java.util.List; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.ScheduledThreadPoolExecutor; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicLong; import javax.ws.rs.DELETE; import javax.ws.rs.FormParam; import javax.ws.rs.GET; import javax.ws.rs.PUT; import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.Produces; import javax.ws.rs.QueryParam; import org.github.etcd.service.rest.EtcdMember; import org.github.etcd.service.rest.EtcdMembers; import org.github.etcd.service.rest.EtcdNode; import org.github.etcd.service.rest.EtcdResponse; import org.github.etcd.service.rest.EtcdSelfStats; import org.github.etcd.service.rest.EtcdSelfStats.LeaderInfo; @Path("/") public class EtcdResourceImpl implements EtcdResource { private EtcdSelfStats selfStats; private EtcdMembers members; private String version = "etcd 2.0.11"; private AtomicLong createdIndex = new AtomicLong(); private AtomicLong modifiedIndex = new AtomicLong(); private ScheduledExecutorService scheduler; private ConcurrentMap<String, ScheduledFuture<?>> scheduledTasks = new ConcurrentHashMap<>(); private EtcdTreeNode rootNode; public EtcdResourceImpl() { EtcdNode node = new EtcdNode("/"); node.setCreatedIndex(createdIndex.getAndIncrement()); node.setModifiedIndex(modifiedIndex.getAndIncrement()); rootNode = new EtcdTreeNode(node); selfStats = new EtcdSelfStats(); selfStats.setId("123456"); selfStats.setName("etcd_simulator"); selfStats.setState("StateLeader"); selfStats.setLeaderInfo(new LeaderInfo()); selfStats.getLeaderInfo().setLeader(selfStats.getId()); members = new EtcdMembers(); EtcdMember selfMember = new EtcdMember(); selfMember.setId(selfStats.getId()); selfMember.setName(selfStats.getName()); selfMember.setState(selfStats.getState()); selfMember.setClientURLs(Arrays.asList("http://simulator:2379/")); selfMember.setPeerURLs(Arrays.asList("http://simulator:2380/")); members.setMembers(Arrays.asList(selfMember)); scheduler = new ScheduledThreadPoolExecutor(4); } @Override public void close() throws Exception { for (ScheduledFuture<?> task : scheduledTasks.values()) { task.cancel(true); } scheduledTasks.clear(); scheduler.shutdown(); rootNode = null; } @Override @GET @Path("/version") @Produces("text/plain") public String getVersion() { return version; } @Override @GET @Path("/v2/stats/self") @Produces("application/json") public EtcdSelfStats getSelfStats() { return selfStats; } @Override @GET @Path("/v2/members") @Produces("application/json") public EtcdMembers getMembers() { return members; } @Override @GET @Path("/v2/keys/{key:(.*)?}") @Produces("application/json") public EtcdResponse getNode(@PathParam("key") String key) { System.out.println("EtcdResourceImpl.getNode(" + key + ")"); EtcdTreeNode treeNode = rootNode; if (key != null && key.length() > 0) { String[] parts = key.split("/"); for (String part : parts) { treeNode = treeNode.getChildren().get(part); if (treeNode == null) { throw new RuntimeException("Node: /" + key + " not found"); } } } EtcdResponse response = new EtcdResponse(); response.setAction("get"); response.setNode(treeNode.getClonedContent(true)); return response; } @Override @PUT @Path("/v2/keys/{key}") @Produces("application/json") public EtcdResponse putNode(@PathParam("key") String key, @FormParam("dir") Boolean directory, @FormParam("value") String value, @FormParam("ttl") String ttl, @FormParam("prevExist") Boolean update) { System.out.println("EtcdResourceImpl.putNode(" + key + ")"); boolean updating = update != null ? update : false; boolean isDir = directory != null ? directory : false; if (key == null || key.length() == 0) { throw new RuntimeException("Modifying root node is not allowed"); } EtcdTreeNode parent = rootNode; String[] parts = key.split("/"); StringBuffer currentKey = new StringBuffer(key.length()); currentKey.append('/'); // create parent directories if they do not exist already for (int i=0; i<parts.length - 1; i++) { currentKey.append(parts[i]); EtcdTreeNode temp = parent.getChildren().get(parts[i]); if (temp == null) { if (updating) { throw new RuntimeException("Directory: " + currentKey.toString() + " does not exist!"); } else { EtcdNode content = new EtcdNode(currentKey.toString()); content.setCreatedIndex(createdIndex.getAndIncrement()); content.setModifiedIndex(modifiedIndex.getAndIncrement()); temp = new EtcdTreeNode(content); parent.getChildren().put(parts[i], temp); } } parent = temp; currentKey.append('/'); } currentKey.append(parts[parts.length - 1]); EtcdTreeNode temp = parent.getChildren().get(parts[parts.length - 1]); if (updating && temp == null) { throw new RuntimeException("Node: " + currentKey.toString() + " does not exist!"); } if (!updating && temp != null) { throw new RuntimeException("Node: " + currentKey.toString() + " already exists. Do an update instead?"); } EtcdResponse response = new EtcdResponse(); response.setAction("set"); Long ttlSeconds = ttl == null || ttl.isEmpty() ? null : Long.parseLong(ttl); if (temp != null) { response.setPrevNode(temp.getClonedContent(false)); temp.getContent().setModifiedIndex(modifiedIndex.getAndIncrement()); temp.getContent().setValue(value); temp.setExpiration(ttlSeconds); } else { EtcdNode content = isDir ? new EtcdNode(currentKey.toString()) : new EtcdNode(currentKey.toString(), value); temp = new EtcdTreeNode(content); temp.setExpiration(ttlSeconds); content.setCreatedIndex(createdIndex.getAndIncrement()); content.setModifiedIndex(modifiedIndex.getAndIncrement()); parent.getChildren().put(parts[parts.length - 1], temp); } ScheduledFuture<?> previousTask = scheduledTasks.remove(key); if (previousTask != null) { System.err.println("Cancelling expiration for: " + key); previousTask.cancel(false); } if (ttlSeconds != null) { System.err.println("Scheduling expiration for: " + key + " after: " + ttlSeconds + " seconds"); scheduledTasks.put(key, scheduler.schedule(new ExpireNodeTask(key, isDir), ttlSeconds, TimeUnit.SECONDS)); } response.setNode(temp.getClonedContent(false)); return response; } private class ExpireNodeTask implements Runnable { private final String key; private final boolean isDir; public ExpireNodeTask(String key, boolean isDir) { this.key = key; this.isDir = isDir; } @Override public void run() { scheduledTasks.remove(key); System.err.println("Expiring: " + key); try { if (isDir) { deleteNode(key, null, true); } else { deleteNode(key, null, null); } } catch (Exception e) { e.printStackTrace(); } } } @Override @DELETE @Path("/v2/keys/{key}") @Produces("application/json") public EtcdResponse deleteNode(@PathParam("key") String key, @QueryParam("dir") Boolean directory, @QueryParam("recursive") Boolean recursive) { boolean isDir = directory != null ? directory : false; boolean isRecursive = recursive != null ? recursive : false; if (isRecursive) { isDir = true; } System.out.println("EtcdResourceImpl.deleteNode(" + key + ")"); EtcdTreeNode parent = rootNode; EtcdTreeNode treeNode = rootNode; String[] parts = key.split("/"); for (int i=0; i<parts.length; i++) { parent = treeNode; treeNode = parent.getChildren().get(parts[i]); if (treeNode == null) { throw new RuntimeException("Node: /" + key + " not found"); } } if (treeNode.hasChildren() && !isRecursive) { throw new RuntimeException("Cannot delete non empty directory: " + key); } if (treeNode.getContent().isDir() && !treeNode.hasChildren() && !isDir) { throw new RuntimeException("Node is directory: " + key); } System.err.println("Deleting: " + key); treeNode = parent.getChildren().remove(parts[parts.length - 1]); EtcdResponse response = new EtcdResponse(); response.setAction("delete"); response.setNode(treeNode.getClonedContent(false)); response.setPrevNode(response.getNode()); return response; } private static class EtcdTreeNode { private EtcdNode content; private Long expirationTime; private ConcurrentMap<String, EtcdTreeNode> children; public EtcdTreeNode(EtcdNode content) { this.content = content; if (content.isDir()) { children = new ConcurrentHashMap<>(); } } public EtcdNode getContent() { return content; } public boolean hasChildren() { return children != null && children.size() > 0; } public ConcurrentMap<String, EtcdTreeNode> getChildren() { return children; } public void setExpiration(Long ttl) { if (ttl == null) { expirationTime = null; } else { expirationTime = System.currentTimeMillis() + ttl * 1000L; } } public EtcdNode getClonedContent(boolean withChildren) { EtcdNode result = new EtcdNode(); result.setKey(content.getKey()); result.setDir(content.isDir()); result.setValue(content.getValue()); result.setCreatedIndex(content.getCreatedIndex()); result.setModifiedIndex(content.getModifiedIndex()); if (expirationTime != null) { result.setExpiration(new Date(expirationTime).toString()); result.setTtl((expirationTime - System.currentTimeMillis()) / 1000); } if (withChildren && children != null && children.size() > 0) { List<EtcdNode> nodes = new ArrayList<>(children.size()); for (EtcdTreeNode childNode : children.values()) { nodes.add(childNode.getClonedContent(false)); } result.setNodes(nodes); } return result; } } }