Java tutorial
/* * Copyright 2015 BlackBerry Limited. * * 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 com.blackberry.bdp.common.versioned; //import com.fasterxml.jackson.annotation.JsonIgnore; //import com.fasterxml.jackson.annotation.JsonProperty; import java.lang.reflect.Field; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import org.apache.curator.framework.CuratorFramework; import org.apache.zookeeper.CreateMode; import org.apache.zookeeper.data.Stat; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.blackberry.bdp.common.exception.ComparableClassMismatchException; import com.blackberry.bdp.common.exception.DeleteException; import com.blackberry.bdp.common.exception.InvalidUserRoleException; import com.blackberry.bdp.common.exception.JsonMergeException; import com.blackberry.bdp.common.exception.MissingConfigurationException; import com.blackberry.bdp.common.exception.VersionMismatchException; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ObjectNode; import java.io.IOException; @JsonIgnoreProperties({ "curator", "zkPath", "mode", "retries", "backoff", "backoffExponent" }) public abstract class ZkVersioned<T extends ZkVersioned<T>> { private static final Logger LOG = LoggerFactory.getLogger(ZkVersioned.class); protected static ObjectMapper mapper; private CuratorFramework curator; private String zkPath; private final Map<String, Map<Class, Class>> roleToMixInMapping = new HashMap<>(); protected Integer version = null; private CreateMode mode = CreateMode.PERSISTENT; private long backoff = 1000; private long retries = 3; private long backoffExponent = 1; public ZkVersioned() { mapper = getNewMapper(); } public ZkVersioned(CuratorFramework curator, String zkPath) { this(); this.curator = curator; this.zkPath = zkPath; } private static ObjectMapper getNewMapper() { ObjectMapper newMapper = new ObjectMapper(); newMapper.configure(DeserializationFeature.USE_BIG_INTEGER_FOR_INTS, true); return newMapper; } public void registerMixIn(String role, Class objClass, Class mixinClass) { Map<Class, Class> roleToClass = roleToMixInMapping.get(role); if (roleToClass == null) { roleToClass = new HashMap<>(); roleToMixInMapping.put(role, roleToClass); } roleToClass.put(objClass, mixinClass); LOG.info("here's the role to mix-in map: {}", roleToMixInMapping); } public final void reload(ZkVersioned newVersion) throws IllegalArgumentException, IllegalAccessException, ComparableClassMismatchException { if (!this.getClass().equals(newVersion.getClass())) { throw new ComparableClassMismatchException(String.format("Versioned class %s cannot be compared to %s", this.getClass(), newVersion.getClass())); } if (this.getVersion() >= newVersion.getVersion()) { return; } for (Field myField : this.getClass().getDeclaredFields()) { if (!myField.isAnnotationPresent(JsonIgnore.class)) { if (!myField.get(this).equals(myField.get(newVersion))) { // Field mis-match, inherit the new version's value LOG.info("Assigning {}.{}={} (old version: {}, old value: {}, new version {}", this.getClass().getName(), myField.getName(), myField.get(newVersion), this.getVersion(), myField.get(this), newVersion.getVersion()); myField.set(this, myField.get(newVersion)); } } } } /** * Fetches the new object from ZK * * @throws Exception */ public final void reload() throws Exception { Stat newZkStat = curator.checkExists().forPath(zkPath); if (newZkStat == null) { throw new MissingConfigurationException("Configuration doesn't exist in ZK at " + zkPath); } ZkVersioned newObj = mapper.readValue(curator.getData().forPath(zkPath), getClass()); newObj.setVersion(newZkStat.getVersion()); reload(newObj); this.version = newZkStat.getVersion(); } /** * Deletes an object from ZK * * @throws DeleteException * @throws Exception */ public synchronized void delete() throws DeleteException, Exception { Stat stat = this.curator.checkExists().forPath(zkPath); if (stat == null) { LOG.error("Cannot delete {} object at non-existent path: {}", this, zkPath); throw new DeleteException(String.format("Cannot delete object at non-existent path: %s", zkPath)); } curator.delete().deletingChildrenIfNeeded().forPath(zkPath); } public synchronized static void delete(CuratorFramework curator, String zkPath) throws DeleteException, Exception { Stat stat = curator.checkExists().forPath(zkPath); if (stat == null) { LOG.error("Cannot delete object at non-existent path: {}", zkPath); throw new DeleteException(String.format("Cannot delete object at non-existent path: %s", zkPath)); } LOG.info("deleting object at path {}", zkPath); curator.delete().deletingChildrenIfNeeded().forPath(zkPath); } public String toJSON() throws JsonProcessingException { return mapper.writeValueAsString(this); } public String toJSON(String role) throws JsonProcessingException, InvalidUserRoleException { if (!roleToMixInMapping.containsKey(role)) { throw new InvalidUserRoleException(String.format("The role %s does not apply to %s", role, getClass())); } ObjectMapper mixinMapper = getNewMapper(); for (Class objClass : roleToMixInMapping.get(role).keySet()) { mixinMapper.addMixIn(objClass, roleToMixInMapping.get(role).get(objClass)); } return mixinMapper.writeValueAsString(this); } public JsonNode toJsonNode() throws IOException { return mapper.readTree(toJSON()); } public JsonNode toJsonNode(String role) throws IOException, JsonProcessingException, InvalidUserRoleException { return mapper.readTree(toJSON(role)); } private void writeJsonToZooKeeper(String jsonString) throws Exception { // remove the version as that never gets written to ZK ObjectNode node = (ObjectNode) mapper.readTree(jsonString); node.remove("version"); jsonString = node.toString(); LOG.info("Attempt at saving {} to {} as {}", this, this.zkPath, jsonString); Stat stat = this.curator.checkExists().forPath(zkPath); for (int i = 0; i < retries; i++) { try { if (stat == null) { if (version != null) { throw new VersionMismatchException("New objects must have null version"); } LOG.info("Saving initial object in non-existent zkPath: {}", zkPath); curator.create().creatingParentsIfNeeded().withMode(mode).forPath(zkPath, jsonString.getBytes()); stat = this.curator.checkExists().forPath(zkPath); this.setVersion(stat.getVersion()); } else { if (version == null) { throw new VersionMismatchException("Cannot update existing objects with null version"); } if (this.version != stat.getVersion()) { throw new VersionMismatchException( String.format("Object with version %s cannot be saved to existing version %s", this.version, stat.getVersion())); } else { Stat newStat = curator.setData().forPath(zkPath, jsonString.getBytes()); this.setVersion(newStat.getVersion()); LOG.info("Saved new {} version {}", this.getClass(), newStat.getVersion()); } } break; } catch (VersionMismatchException vme) { throw vme; } catch (Exception e) { if (i <= retries) { LOG.warn("Failed attempt {}/{} to write to {}. Retrying in {} seconds", i, retries, zkPath, (backoff / 1000), e); Thread.sleep(backoff); backoff *= backoffExponent; } else { throw new Exception( String.format("Failed to write to %s and no retries left--giving up", zkPath), e); } } } } public synchronized void save() throws JsonProcessingException, VersionMismatchException, Exception { writeJsonToZooKeeper(toJSON()); } public synchronized void save(String role) throws Exception { JsonNode roleBasedJsonNode = toJsonNode(role); LOG.info("Role based json node : {}", roleBasedJsonNode); String jsonToWriteToZk; try { JsonNode existingJsonNode = get(this.getClass(), this.curator, this.zkPath).toJsonNode(); LOG.info("existing json node from zk : {}", existingJsonNode); jsonToWriteToZk = merge(existingJsonNode, roleBasedJsonNode).toString(); LOG.info("saving existing object with role {} yields JSON {}", role, jsonToWriteToZk); } catch (MissingConfigurationException mce) { jsonToWriteToZk = roleBasedJsonNode.toString(); LOG.info("saving with role {} yields JSON {}", role, jsonToWriteToZk); } writeJsonToZooKeeper(jsonToWriteToZk); } /** * Iterates over json1 which is intended to be a full representation of a complete JSON * structure. It compares nodes on json1 against nodes on json2 which should contain * either the same identical structure of json1 or a subset of JSON structure contained * in json1. * * If identically named nodes on json1 and json2 vary in type (ObjectNode vs ArrayNode * for example) then an exception is thrown since json2 must not contain any additional * structure than what is found in json1. * * Explicit Null Node Handling Regardless of Node type: * * This pertains to the value of a node being explicity equal to null. See further below * for handling of non-existent nodes * * If a node is null on json1 and not null on json2 then the node on json1 is set to the * value of the node on json2. * * If a node is not null on json1 and is null on json2 then the node on json1 is made null. * * Non-existent Node Handling: * * Since json1 is intended to be a full representation of a complete JSON structure * nodes on json2 that don't exist on json1 are completely ignored. Only if the same * node exists on both json1 and json2 will the structures be merged. * * ArrayNode Handling * * If the node being compared is an ArrayNode then the elements on json2 are iterated * over. If the index on json1 exists on json1 then the two elements are merged. If the * index doesn't exist on json1 then the element is added to the ArrayNode on json1. * Note: The existence of the element on json1 is determined by index and when an * element is added to json1 it's index increases by one. That shouldn't be a problem * though as for there to ever be more elements in json2, the index pointer will always * be one larger than the max index of json1. * * ArrayNode Handling when json1 contains more elements than json2: * * Elements are removed from json1 if they have higher indexes than the size of json2 * minus 1 * * @param json1 * @param json2 * @return * @throws com.blackberry.bdp.common.exception.JsonMergeException */ public static JsonNode merge(JsonNode json1, JsonNode json2) throws JsonMergeException { Iterator<String> json1Fields = json1.fieldNames(); LOG.info("Merged called on json1 ({}), json2 ({})", json1.getNodeType(), json2.getNodeType()); while (json1Fields.hasNext()) { String nodeName = json1Fields.next(); JsonNode json1Node = json1.get(nodeName); // Check if json2 has the node and run explicit null checks if (!json2.has(nodeName)) { LOG.info("Not comparing {} since it doesn't exist on json2", nodeName); continue; } else if (json1Node.isNull() && json2.hasNonNull(nodeName)) { ((ObjectNode) json1).replace(nodeName, json2.get(nodeName)); LOG.info("explicit null {} on json1 replaced with non-null from json2", nodeName); continue; } else if (json1.hasNonNull(nodeName) && json2.get(nodeName).isNull()) { ((ObjectNode) json1).replace(nodeName, json2.get(nodeName)); LOG.info("non-null {} on json1 replaced with explicitly null on json2", nodeName); continue; } JsonNode json2Node = json2.get(nodeName); if (json1Node.getNodeType().equals(json2Node.getNodeType()) == false) { throw new JsonMergeException(String.format("json1 (%s) cannot be merged with json2 (%s)", json1.getNodeType(), json2.getNodeType())); } LOG.info("need to compare \"{}\" which is a {}", nodeName, json1Node.getNodeType()); if (json1Node.isObject()) { LOG.info("Calling merge on object {}", nodeName); merge(json1Node, json2.get(nodeName)); } else if (json1Node instanceof ObjectNode) { throw new JsonMergeException("{} is instance of ObjectNode and wasn't isObject()--what gives?!"); } else if (json1Node.isArray()) { ArrayNode json1Array = (ArrayNode) json1Node; ArrayNode json2Array = (ArrayNode) json2Node; LOG.info("ArrayNode {} json1 has {} elements and json2 has {} elements", nodeName, json1Array.size(), json2Array.size()); int indexNo = 0; Iterator<JsonNode> json2Iter = json2Array.iterator(); while (json2Iter.hasNext()) { JsonNode json2Element = json2Iter.next(); if (json1Array.has(indexNo)) { LOG.info("Need to merge ArrayNode {} element {}", nodeName, indexNo); merge(json1Node.get(indexNo), json2Element); } else { LOG.info("ArrayNode {} element {} not found on json1, adding", nodeName, indexNo); json1Array.add(json2Element); } indexNo++; } while (json1Array.size() > json2Array.size()) { int indexToRemove = json1Array.size() - 1; json1Array.remove(indexToRemove); LOG.info("ArrayNode {} index {} on json1 removed since greater than size of json2 ({})", nodeName, indexToRemove, json2Array.size()); } } else { LOG.info("{} ({}) has fallen through known merge types", nodeName, json1Node.getNodeType()); ((ObjectNode) json1).replace(nodeName, json2Node); LOG.info("json1 node {} replaced with json2's node", nodeName); } } return json1; } /** * Returns a VersionedObject from a specific CuratorFramework and ZK Path * * @param <T> * @param type * @param curator * @param zkPath * @return * @throws Exception */ public static <T extends ZkVersioned> T get(Class<T> type, CuratorFramework curator, String zkPath) throws Exception { mapper = getNewMapper(); Stat stat = curator.checkExists().forPath(zkPath); if (stat == null) { throw new MissingConfigurationException("Configuration doesn't exist in ZK at " + zkPath); } byte[] jsonBytes = curator.getData().forPath(zkPath); T obj = mapper.readValue(jsonBytes, type); obj.setVersion(stat.getVersion()); obj.setCurator(curator); obj.setZkPath(zkPath); return obj; } /** * Returns all VersionedObjects from a specific CuratorFramework and ZK Root Path * * @param <T> * @param type * @param curator * @param zkPathRoot * @return * @throws Exception */ public static <T extends ZkVersioned> ArrayList<T> getAll(Class<T> type, CuratorFramework curator, String zkPathRoot) throws Exception { mapper = getNewMapper(); Stat stat = curator.checkExists().forPath(zkPathRoot); if (stat == null) { throw new MissingConfigurationException("Configuration doesn't exist in ZK at " + zkPathRoot); } ArrayList<T> objList = new ArrayList<>(); for (String objectId : Util.childrenInZkPath(curator, zkPathRoot)) { String objPath = String.format("%s/%s", zkPathRoot, objectId); Stat objStat = curator.checkExists().forPath(objPath); byte[] jsonBytes = curator.getData().forPath(objPath); if (jsonBytes.length != 0) { T obj = mapper.readValue(jsonBytes, type); obj.setVersion(objStat.getVersion()); obj.setCurator(curator); obj.setZkPath(objPath); objList.add(obj); } else { LOG.error("The byte array in {} was empty", objPath); } } return objList; } /** * @return the version */ public Integer getVersion() { return version; } public void setVersion(Integer version) { this.version = version; } public void setCurator(CuratorFramework curator) { this.curator = curator; } public void setZkPath(String zkPath) { this.zkPath = zkPath; } /** * @return the zkPath */ public String getZkPath() { return zkPath; } /** * @return the mode */ public CreateMode getMode() { return mode; } public void setMode(CreateMode mode) { this.mode = mode; } /** * @return the backoff */ public long getBackoff() { return backoff; } public void setBackoff(long backoff) { this.backoff = backoff; } /** * @return the retries */ public long getRetries() { return retries; } public void setRetries(long retries) { this.retries = retries; } /** * @return the backoffExponent */ public long getBackoffExponent() { return backoffExponent; } public void setBackoffExponent(long backoffExponent) { this.backoffExponent = backoffExponent; } }