Java tutorial
/* * Copyright 2013 Yen Pai ypai@reign.io * * 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 io.reign.conf; import io.reign.AbstractService; import io.reign.DataSerializer; import io.reign.JsonDataSerializer; import io.reign.PathType; import io.reign.mesg.ParsedRequestMessage; import io.reign.mesg.RequestMessage; import io.reign.mesg.ResponseMessage; import io.reign.mesg.ResponseStatus; import io.reign.mesg.SimpleResponseMessage; import io.reign.util.ZkClientUtil; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; import org.apache.zookeeper.CreateMode; import org.apache.zookeeper.KeeperException; import org.apache.zookeeper.KeeperException.Code; import org.apache.zookeeper.data.ACL; import org.apache.zookeeper.data.Stat; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Remote/centralized configuration service. * * @author ypai * */ public class ConfService extends AbstractService { static final DataSerializer DEFAULT_CONF_SERIALIZER = new JsonDataSerializer(); private static final Logger logger = LoggerFactory.getLogger(ConfService.class); private final ZkClientUtil zkUtil = new ZkClientUtil(); // private final Map<String, DataSerializer> dataSerializerMap = new ConcurrentHashMap<String, DataSerializer>(17, // 0.9f, 1); public ConfService() { // dataSerializerMap.put("json", new JsonDataSerializer()); // dataSerializerMap.put("js", dataSerializerMap.get("json")); // dataSerializerMap.put("properties", dataSerializerMap.get("json")); // dataSerializerMap.put("properties", new ConfPropertiesSerializer<ConfProperties>(false)); } @Override public void init() { } @Override public void destroy() { } // /** // * // * @param extension // * "file" extension of path: would be "properties" in the following path: // * /my-cluster/my-config.properties // * @param dataSerializer // */ // public void registerSerializer(String extension, DataSerializer dataSerializer) { // dataSerializerMap.put(extension, dataSerializer); // } static DataSerializer getDataSerializer(String path, Map<String, DataSerializer> dataSerializerMap) { int lastDotIndex = path.lastIndexOf("."); String extension = path.substring(lastDotIndex + 1); DataSerializer result = dataSerializerMap.get(extension); if (result == null) { throw new IllegalArgumentException("Could not derive serializer from given information: path=" + path); } return result; } static boolean isValidConfPath(String path) { if (path == null) { return false; } int lastSlashIndex = path.lastIndexOf("/"); int lastDotIndex = path.lastIndexOf("."); if (lastDotIndex == -1 || lastSlashIndex > lastDotIndex || lastDotIndex - lastSlashIndex < 2 || lastDotIndex < 1 || lastDotIndex == path.length() - 1) { return false; } return true; } static void throwExceptionIfInvalidConfPath(String path) { if (!isValidConfPath(path)) { throw new IllegalArgumentException("Invalid configuration path: path=" + path); } } public <T> void observe(String clusterId, String relativeConfPath, ConfObserver<T> observer) { observe(PathType.CONF, clusterId, relativeConfPath, observer); } <T> void observe(PathType pathType, String clusterId, String relativeConfPath, ConfObserver<T> observer) { throwExceptionIfInvalidConfPath(relativeConfPath); String absolutePath = getPathScheme().getAbsolutePath(pathType, getPathScheme().joinPaths(clusterId, relativeConfPath)); observer.setClusterId(clusterId); // observer.setDataSerializerMap(dataSerializerMap); getObserverManager().put(absolutePath, observer); } /** * Picks serializer based on path "file extension". * * @param <T> * @param clusterId * @param relativeConfPath * @return */ public <T> T getConf(String clusterId, String relativeConfPath) { throwExceptionIfInvalidConfPath(relativeConfPath); return getConf(clusterId, relativeConfPath, null); } public <T> T getConf(String clusterId, String relativeConfPath, ConfObserver<T> observer) { throwExceptionIfInvalidConfPath(relativeConfPath); // DataSerializer confSerializer = getDataSerializer(relativeConfPath, dataSerializerMap); if (observer != null) { observe(clusterId, relativeConfPath, observer); } return (T) getConfAbsolutePath(PathType.CONF, clusterId, relativeConfPath, DEFAULT_CONF_SERIALIZER, null); } public <T> void putConf(String clusterId, String relativeConfPath, T conf) { putConf(clusterId, relativeConfPath, conf, null); } /** * Picks serializer based on path "file extension". * * @param <T> * @param clusterId * @param relativeConfPath * @param conf */ public <T> void putConf(String clusterId, String relativeConfPath, T conf, ConfObserver<T> observer) { // DataSerializer confSerializer = getDataSerializer(relativeConfPath, dataSerializerMap); putConfAbsolutePath( getPathScheme().getAbsolutePath(PathType.CONF, getPathScheme().joinPaths(clusterId, relativeConfPath)), conf, DEFAULT_CONF_SERIALIZER, getDefaultZkAclList()); if (observer != null) { observe(clusterId, relativeConfPath, observer); } } /** * * @param relativePath */ public void removeConf(String clusterId, String relativeConfPath) { String path = getPathScheme().getAbsolutePath(PathType.CONF, getPathScheme().joinPaths(clusterId, relativeConfPath)); try { getZkClient().delete(path, -1); } catch (KeeperException e) { if (e.code() != Code.NONODE) { logger.error("removeConf(): error trying to delete node: " + e + ": path=" + path, e); } } catch (Exception e) { logger.error("removeConf(): error trying to delete node: " + e + ": path=" + path, e); } } /** * * @param <T> * @param pathContext * @param pathType * @param clusterId * @param relativeConfPath * @param confSerializer * @param observer * @param useCache * @return */ <T> T getConfAbsolutePath(PathType pathType, String clusterId, String relativeConfPath, DataSerializer<T> confSerializer, ConfObserver<T> observer) { String absolutePath = getPathScheme().getAbsolutePath(pathType, getPathScheme().joinPaths(clusterId, relativeConfPath)); return getConfAbsolutePath(absolutePath, confSerializer, observer); } /** * * @param <T> * @param absolutePath * @param confSerializer * @param observer * @param useCache * @return */ <T> T getConfAbsolutePath(String absolutePath, DataSerializer<T> confSerializer, ConfObserver<T> observer) { T result = getConfValue(absolutePath, confSerializer); /** add observer if given **/ if (observer != null) { getObserverManager().put(absolutePath, observer); } return result; } <T> T getConfValue(String absolutePath, DataSerializer<T> confSerializer) { throwExceptionIfInvalidConfPath(absolutePath); byte[] bytes = null; T result = null; try { // not in cache, so load from ZK Stat stat = new Stat(); bytes = getZkClient().getData(absolutePath, true, stat); result = bytes != null ? confSerializer.deserialize(bytes) : null; } catch (KeeperException e) { if (e.code() == Code.NONODE) { // set up watch on that node try { getZkClient().exists(absolutePath, true); } catch (Exception e1) { logger.error("getConfValue(): error trying to watch node: " + e1 + ": path=" + absolutePath, e1); } logger.debug( "getConfValue(): error trying to fetch node info: {}: node does not exist: path={}", e.getMessage(), absolutePath); } else { logger.error("getConfValue(): error trying to fetch node info: " + e, e); } } catch (Exception e) { logger.error("getConfValue(): error trying to fetch node info: " + e, e); } return result; } /** * * @param <T> * @param pathContext * @param pathType * @param clusterId * @param relativeConfPath * @param conf * @param confSerializer * @param aclList */ <T> void putConfAbsolutePath(String absolutePath, T conf, DataSerializer<T> confSerializer, List<ACL> aclList) { throwExceptionIfInvalidConfPath(absolutePath); try { // write to ZK byte[] leafData = confSerializer.serialize(conf); String pathUpdated = zkUtil.updatePath(getZkClient(), getPathScheme(), absolutePath, leafData, aclList, CreateMode.PERSISTENT, -1); logger.debug("putConfAbsolutePath(): saved configuration: path={}", pathUpdated); } catch (Exception e) { logger.error( "putConfAbsolutePath(): error while saving configuration: " + e + ": path=" + absolutePath, e); } } void putConfAbsolutePath(String absolutePath, byte[] bytes, List<ACL> aclList) { throwExceptionIfInvalidConfPath(absolutePath); try { // write to ZK String pathUpdated = zkUtil.updatePath(getZkClient(), getPathScheme(), absolutePath, bytes, aclList, CreateMode.PERSISTENT, -1); logger.debug("putConfAbsolutePath(): saved configuration: path={}", pathUpdated); } catch (Exception e) { logger.error( "putConfAbsolutePath(): error while saving configuration: " + e + ": path=" + absolutePath, e); } } @Override public ResponseMessage handleMessage(RequestMessage requestMessage) { ResponseMessage responseMessage = new SimpleResponseMessage(); try { /** preprocess request **/ String requestBody = (String) requestMessage.getBody(); int newlineIndex = requestBody.indexOf("\n"); String resourceLine = requestBody; String confBody = null; if (newlineIndex != -1) { resourceLine = requestBody.substring(0, newlineIndex); confBody = requestBody.substring(newlineIndex + 1); } // get meta ParsedRequestMessage parsedRequestMessage = new ParsedRequestMessage(requestMessage); String meta = parsedRequestMessage.getMeta(); String resource = parsedRequestMessage.getResource(); // get resource; strip beginning and ending slashes "/" if (resource.startsWith("/")) { resource = resource.substring(1); } if (resource.endsWith("/")) { resource = resource.substring(0, resource.length() - 1); } /** get response **/ String[] tokens = getPathScheme().tokenizePath(resource); String clusterId = null; String relativePath = null; if (tokens.length == 1) { // list configurations in cluster clusterId = tokens[0]; } else if (tokens.length > 1) { clusterId = tokens[0]; String[] relativePathTokens = Arrays.<String>copyOfRange(tokens, 1, tokens.length); relativePath = getPathScheme().joinTokens(relativePathTokens); } if (logger.isDebugEnabled()) { logger.debug("clusterId={}; relativePath={}; meta={}", new Object[] { clusterId, relativePath, meta }); } /** decide what to do based on meta value **/ if (meta == null) { // get conf or list conf(s) available if (clusterId != null && relativePath != null) { if (isValidConfPath(relativePath)) { Object conf = getConf(clusterId, relativePath); if (conf != null) { responseMessage.setBody(conf); } } else { // list items available String absolutePath = getPathScheme().getAbsolutePath(PathType.CONF, getPathScheme().joinPaths(clusterId, relativePath)); List<String> childList = getZkClient().getChildren(absolutePath, false); responseMessage.setBody(childList); } } else if (clusterId != null && relativePath == null) { // list configs in cluster String absolutePath = getPathScheme().getAbsolutePath(PathType.CONF, clusterId); List<String> childList = getZkClient().getChildren(absolutePath, false); responseMessage.setBody(childList); } else { // both clusterId and relativePath are null: just list available clusters String absolutePath = getPathScheme().getAbsolutePath(PathType.CONF); List<String> childList = getZkClient().getChildren(absolutePath, false); responseMessage.setBody(childList); } } else if ("update".equals(meta) || "put".equals(meta)) { // edit conf (add, remove, or update properties) Map conf; if ("put".equals(meta)) { logger.info("PUT configuration: clusterId={}; path={}", clusterId, relativePath); conf = new HashMap<String, Object>(); } else { logger.info("UPDATE configuration: clusterId={}; path={}", clusterId, relativePath); conf = getConf(clusterId, relativePath); // set existing configuration in response now so even if we get unexpected error, it will show in // response responseMessage.setBody(conf); // create one to build upon if doesn't exist yet if (conf == null) { // if this is a new configuration conf = new HashMap<String, Object>(); } } // TODO: use UTF-8 universally in the future? // read in api body as properties for easier processing Map<String, String> updateConf = new HashMap<String, String>(); if (confBody != null) { String[] confBodyLines = confBody.split("\n"); for (String confLine : confBodyLines) { int equalsIndex = confLine.indexOf("="); if (equalsIndex != -1) { String key = confLine.substring(0, equalsIndex).trim(); String value = confLine.substring(equalsIndex + 1).trim(); updateConf.put(key, value); } else { logger.debug("Could not parse line: ignoring: '" + confLine + "'"); } } } boolean isUpdate = "update".equals(meta); for (Object keyObject : updateConf.keySet()) { String key = (String) keyObject; if (isUpdate && key.startsWith("+")) { String newKey = key.substring(1); // only add if doesn't already exist if (conf.get(newKey) == null) { conf.put(newKey, castValueIfNecessary(updateConf.get(key))); } } else if (isUpdate && key.startsWith("-")) { key = key.substring(1); // remove key conf.remove(key); } else { // add or overwrite existing property conf.put(key, castToMatchExistingTypeIfNecessary(updateConf.get(key), conf.get(key))); } } // for putConf(clusterId, relativePath, conf); // set if it's a "put" responseMessage.setBody(conf); } else if ("delete".equals(meta)) { logger.info("DELETE configuration: clusterId={}; path={}", clusterId, relativePath); // delete conf removeConf(clusterId, relativePath); } else { responseMessage.setComment("Invalid request (do not understand '" + meta + "'): " + requestBody); } } catch (KeeperException e) { if (e.code() == KeeperException.Code.NONODE) { if (logger.isDebugEnabled()) { logger.debug("" + e, e); } responseMessage.setStatus(ResponseStatus.OK, "No configuration found."); } else { responseMessage.setStatus(ResponseStatus.ERROR_UNEXPECTED, "" + e); } } catch (Exception e) { logger.error("handleMessage(): " + e, e); responseMessage.setStatus(ResponseStatus.ERROR_UNEXPECTED, "" + e); } responseMessage.setId(requestMessage.getId()); return responseMessage; } /** * * @param value * @param matchValue * @return object of the same type as matchValue */ static Object castToMatchExistingTypeIfNecessary(String value, Object matchValue) { // try to cast value Object castedValue = castValueIfNecessary(value); if (matchValue == null || castedValue == null || !value.equals(castedValue) || matchValue instanceof String) { // favor explicit cast return castedValue; } else if (matchValue instanceof Integer) { return Integer.parseInt(value); } else if (matchValue instanceof Long) { return Long.parseLong(value); } else if (matchValue instanceof Double) { return Double.parseDouble(value); } else if (matchValue instanceof Float) { return Float.parseFloat(value); } else if (matchValue instanceof Short) { return Short.parseShort(value); } else if (matchValue instanceof Byte) { return Byte.parseByte(value); } logger.warn("matchType(): cannot handle type, so defaulting to String: {}", matchValue.getClass().getName()); return value; } /** * * @param value * @param matchValue * @return object of the same type as matchValue */ static Object castValueIfNecessary(String value) { if (value == null) { return value; } else if (value.startsWith("(int)")) { return Integer.parseInt(value.substring(5)); } else if (value.startsWith("(long)")) { return Long.parseLong(value.substring(6)); } else if (value.startsWith("(double)")) { return Double.parseDouble(value.substring(8)); } else if (value.startsWith("(float)")) { return Float.parseFloat(value.substring(7)); } else if (value.startsWith("(short)")) { return Short.parseShort(value.substring(7)); } else if (value.startsWith("(byte)")) { return Byte.parseByte(value.substring(6)); } else if (value.startsWith("(string)")) { return value.substring(8); } else { return value; } } }