package eu.europeana.cloud.service.coordination.configuration;
import java.io.Closeable;
import java.io.IOException;
import java.nio.charset.Charset;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.CopyOnWriteArrayList;
import com.google.common.io.Closeables;
import eu.europeana.cloud.service.coordination.ZookeeperService;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.recipes.cache.ChildData;
import org.apache.curator.framework.recipes.cache.PathChildrenCache;
import org.apache.curator.framework.recipes.cache.PathChildrenCache.StartMode;
import org.apache.curator.framework.recipes.cache.PathChildrenCacheEvent;
import org.apache.curator.framework.recipes.cache.PathChildrenCacheEvent.Type;
import org.apache.curator.framework.recipes.cache.PathChildrenCacheListener;
import org.apache.zookeeper.KeeperException.NoNodeException;
import org.apache.zookeeper.KeeperException.NodeExistsException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Implementation of dynamic properties for ZooKeeper using Curator.
*
* This implementation requires the path to ZK where the dynamic properties are stored.
* For example: /eCloud/v2/ISTI/configuration/dynamicProperties
*
* Properties are direct ZK child nodes of the root parent ZK node.
* An example ZK child property node is /eCloud/v2/ISTI/configuration/dynamicProperties/mcs.swift.address
*
* All the properties are retrieved as {@link String}.
*
* The value is stored in the ZK child property node and can be updated at any time.
* All servers will receive a ZK Watcher callback and automatically update their value.
*/
public class ZookeeperDynamicPropertyManager implements DynamicPropertyManager, Closeable {
private static final Logger LOGGER = LoggerFactory.getLogger(ZookeeperDynamicPropertyManager.class);
private final CuratorFramework client;
private final String configRootPath;
private final PathChildrenCache pathChildrenCache;
private final Charset charset = Charset.forName("UTF-8");
private ConcurrentMap<String, DynamicPropertyListener> listeners = new ConcurrentHashMap<String, DynamicPropertyListener>();
/**
* Creates the pathChildrenCache using the CuratorFramework client and ZK root path node for the config
*
* @param zookeeper service that provides the connection with Zookeeper.
* @param path to ZK where the dynamic properties are stored (ie. /eCloud/v2/ISTI/configuration/dynamicProperties)
*/
public ZookeeperDynamicPropertyManager(ZookeeperService zookeeperService, String configRootPath) {
this.client = zookeeperService.getClient();
this.configRootPath = configRootPath;
this.pathChildrenCache = new PathChildrenCache(client, configRootPath, true);
try {
start();
} catch (Exception e) {
LOGGER.error(e.getMessage());
}
}
/**
* Adds a listener to the pathChildrenCache, initializes the cache, then starts the cache-management background thread
*
* @throws Exception
*/
public void start() throws Exception {
pathChildrenCache.getListenable().addListener(new PathChildrenCacheListener() {
public void childEvent(CuratorFramework aClient, PathChildrenCacheEvent event)
throws Exception {
Type eventType = event.getType();
ChildData data = event.getData();
String path = null;
if (data != null) {
path = data.getPath();
String key = removeRootPath(path);
byte[] value = data.getData();
String stringValue = new String(value, charset);
LOGGER.debug("received update to pathName [{}], eventType [{}]", path, eventType);
LOGGER.debug("key [{}], and value [{}]", key, stringValue);
Map<String, Object> added = null;
Map<String, Object> changed = null;
Map<String, Object> deleted = null;
if (eventType == Type.CHILD_ADDED) {
added = new HashMap<String, Object>(1);
added.put(key, stringValue);
} else if (eventType == Type.CHILD_UPDATED) {
changed = new HashMap<String, Object>(1);
changed.put(key, stringValue);
} else if (eventType == Type.CHILD_REMOVED) {
deleted = new HashMap<String, Object>(1);
deleted.put(key, stringValue);
}
fireEvent(added);
fireEvent(changed);
fireEvent(deleted);
}
}
});
pathChildrenCache.start(StartMode.NORMAL);
}
public Map<String, Object> getCurrentData() throws Exception {
LOGGER.debug("getCurrentData() retrieving current data.");
List<ChildData> children = pathChildrenCache.getCurrentData();
Map<String, Object> all = new HashMap<String, Object>(children.size());
for (ChildData child : children) {
String path = child.getPath();
String key = removeRootPath(path);
byte[] value = child.getData();
all.put(key, new String(value, charset));
}
LOGGER.debug("getCurrentData() retrieved [{}] config elements.", children.size());
return all;
}
@Override
public void addUpdateListener(DynamicPropertyListener l, String dynamicProperty) {
if (l != null) {
listeners.put(dynamicProperty, l);
}
}
@Override
public void removeUpdateListener(DynamicPropertyListener l) {
if (l != null) {
listeners.remove(l);
}
}
protected void fireEvent(Map<String, Object> updatedProperties) {
if (updatedProperties == null) {
return;
}
Iterator<String> dynamicPropertiesIter = listeners.keySet().iterator();
while (dynamicPropertiesIter.hasNext()) {
String dynamicProperty = dynamicPropertiesIter.next();
Object dynamicPropertyValue = updatedProperties.get(dynamicProperty);
if (dynamicPropertyValue != null) {
listeners.get(dynamicProperty).onUpdate((String) dynamicPropertyValue);
}
}
}
private String removeRootPath(String nodePath) {
return nodePath.replace(configRootPath + "/", "");
}
synchronized void setZkProperty(String key, String value) throws Exception {
final String path = configRootPath + "/" + key;
byte[] data = value.getBytes(charset);
try {
// attempt to create (intentionally doing this instead of checkExists())
client.create().creatingParentsIfNeeded().forPath(path, data);
} catch (NodeExistsException exc) {
// key already exists - update the data instead
client.setData().forPath(path, data);
}
}
synchronized String getZkProperty(String key) throws Exception {
final String path = configRootPath + "/" + key;
byte[] bytes = client.getData().forPath(path);
return new String(bytes, charset);
}
synchronized void deleteZkProperty(String key) throws Exception {
final String path = configRootPath + "/" + key;
try {
client.delete().forPath(path);
} catch (NoNodeException exc) {
// Node doesn't exist - NoOp
LOGGER.warn("Node doesn't exist", exc);
}
}
public void close() {
try {
Closeables.close(pathChildrenCache, true);
} catch (IOException e) {
LOGGER.warn(e.getMessage());
}
}
@Override
public void updateValue(String dynamicProperty, String dynamicPropertyUpdatedValue) {
try {
setZkProperty(dynamicProperty, dynamicPropertyUpdatedValue);
} catch (Exception e) {
LOGGER.warn(e.getMessage());
}
}
@Override
public String getCurrentValue(String dynamicProperty) {
try {
return getZkProperty(dynamicProperty);
} catch (Exception e) {
LOGGER.warn(e.getMessage());
}
return null;
}
}