You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@helix.apache.org by hu...@apache.org on 2020/04/01 22:47:10 UTC
[helix] 15/49: Add validation logic to MSD write operations (#759)
This is an automated email from the ASF dual-hosted git repository.
hulee pushed a commit to branch zooscalability
in repository https://gitbox.apache.org/repos/asf/helix.git
commit 59ca671d9b2ce9642b8cc4ea7df0ba3fc8828fc8
Author: Neal Sun <ne...@gmail.com>
AuthorDate: Tue Feb 18 09:38:13 2020 -0800
Add validation logic to MSD write operations (#759)
This PR add routing data validation logic MSD writing operations to ensure the writes leave routing data in a valid state. TrieRoutingData logic is modified to reduce duplicate code.
---
.../metadatastore/MetadataStoreRoutingData.java | 19 ++++
.../helix/rest/metadatastore/TrieRoutingData.java | 126 ++++++++++++---------
.../metadatastore/ZkMetadataStoreDirectory.java | 12 +-
.../resources/helix/PropertyStoreAccessor.java | 3 +-
.../resources/zookeeper/ZooKeeperAccessor.java | 19 +---
.../rest/metadatastore/TestTrieRoutingData.java | 76 +++++++++++--
.../helix/zookeeper/util/ZkValidationUtil.java | 38 +++++++
7 files changed, 206 insertions(+), 87 deletions(-)
diff --git a/helix-rest/src/main/java/org/apache/helix/rest/metadatastore/MetadataStoreRoutingData.java b/helix-rest/src/main/java/org/apache/helix/rest/metadatastore/MetadataStoreRoutingData.java
index 8d8b7e3..3bd9baa 100644
--- a/helix-rest/src/main/java/org/apache/helix/rest/metadatastore/MetadataStoreRoutingData.java
+++ b/helix-rest/src/main/java/org/apache/helix/rest/metadatastore/MetadataStoreRoutingData.java
@@ -45,4 +45,23 @@ public interface MetadataStoreRoutingData {
* @throws NoSuchElementException - when the path doesn't contain a sharding key
*/
String getMetadataStoreRealm(String path) throws IllegalArgumentException, NoSuchElementException;
+
+ /**
+ * Check if the provided sharding key can be inserted to the routing data. The insertion is
+ * invalid if: 1. the sharding key is a parent key to an existing sharding key; 2. the sharding
+ * key has a parent key that is an existing sharding key; 3. the sharding key already exists. In
+ * any of these cases, inserting the sharding key will cause ambiguity among 2 sharding keys,
+ * rendering the routing data invalid.
+ * @param shardingKey - the sharding key to be inserted
+ * @return true if the sharding key could be inserted, false otherwise
+ */
+ boolean isShardingKeyInsertionValid(String shardingKey);
+
+ /**
+ * Check if the provided sharding key and realm address pair exists in the routing data.
+ * @param shardingKey - the sharding key checked
+ * @param realmAddress - the realm address corresponding to the key
+ * @return true if the sharding key and realm address pair exist in the routing data
+ */
+ boolean containsKeyRealmPair(String shardingKey, String realmAddress);
}
diff --git a/helix-rest/src/main/java/org/apache/helix/rest/metadatastore/TrieRoutingData.java b/helix-rest/src/main/java/org/apache/helix/rest/metadatastore/TrieRoutingData.java
index 923f818..f82b718 100644
--- a/helix-rest/src/main/java/org/apache/helix/rest/metadatastore/TrieRoutingData.java
+++ b/helix-rest/src/main/java/org/apache/helix/rest/metadatastore/TrieRoutingData.java
@@ -26,7 +26,10 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
+
import org.apache.helix.rest.metadatastore.exceptions.InvalidRoutingDataException;
+import org.apache.helix.zookeeper.util.ZkValidationUtil;
+
/**
* This is a class that uses a data structure similar to trie to represent metadata store routing
@@ -54,15 +57,12 @@ public class TrieRoutingData implements MetadataStoreRoutingData {
}
public Map<String, String> getAllMappingUnderPath(String path) throws IllegalArgumentException {
- if (path.isEmpty() || !path.substring(0, 1).equals(DELIMITER)) {
- throw new IllegalArgumentException("Provided path is empty or does not have a leading \""
- + DELIMITER + "\" character: " + path);
+ if (!ZkValidationUtil.isPathValid(path)) {
+ throw new IllegalArgumentException("Provided path is not a valid Zookeeper path: " + path);
}
- TrieNode curNode;
- try {
- curNode = findTrieNode(path, false);
- } catch (NoSuchElementException e) {
+ TrieNode curNode = getLongestPrefixNodeAlongPath(path);
+ if (!curNode.getPath().equals(path)) {
return Collections.emptyMap();
}
@@ -84,59 +84,73 @@ public class TrieRoutingData implements MetadataStoreRoutingData {
public String getMetadataStoreRealm(String path)
throws IllegalArgumentException, NoSuchElementException {
- if (path.isEmpty() || !path.substring(0, 1).equals(DELIMITER)) {
- throw new IllegalArgumentException("Provided path is empty or does not have a leading \""
- + DELIMITER + "\" character: " + path);
+ if (!ZkValidationUtil.isPathValid(path)) {
+ throw new IllegalArgumentException("Provided path is not a valid Zookeeper path: " + path);
+ }
+
+ TrieNode node = getLongestPrefixNodeAlongPath(path);
+ if (!node.isShardingKey()) {
+ throw new NoSuchElementException(
+ "No sharding key found within the provided path. Path: " + path);
+ }
+ return node.getRealmAddress();
+ }
+
+ public boolean isShardingKeyInsertionValid(String shardingKey) {
+ if (!ZkValidationUtil.isPathValid(shardingKey)) {
+ throw new IllegalArgumentException(
+ "Provided shardingKey is not a valid Zookeeper path: " + shardingKey);
+ }
+
+ TrieNode node = getLongestPrefixNodeAlongPath(shardingKey);
+ return !node.isShardingKey() && !node.getPath().equals(shardingKey);
+ }
+
+ public boolean containsKeyRealmPair(String shardingKey, String realmAddress) {
+ if (!ZkValidationUtil.isPathValid(shardingKey)) {
+ throw new IllegalArgumentException(
+ "Provided shardingKey is not a valid Zookeeper path: " + shardingKey);
}
- TrieNode leafNode = findTrieNode(path, true);
- return leafNode.getRealmAddress();
+ TrieNode node = getLongestPrefixNodeAlongPath(shardingKey);
+ return node.getPath().equals(shardingKey) && node.getRealmAddress().equals(realmAddress);
}
- /**
- * If findLeafAlongPath is false, then starting from the root node, find the trie node that the
- * given path is pointing to and return it; raise NoSuchElementException if the path does
- * not point to any node. If findLeafAlongPath is true, then starting from the root node, find the
- * leaf node along the provided path; raise NoSuchElementException if the path does not
- * point to any node or if there is no leaf node along the path.
+ /*
+ * Given a path, find a trie node that represents the longest prefix of the path. For example,
+ * given "/a/b/c", the method starts at "/", and attempts to reach "/a", then attempts to reach
+ * "/a/b", then ends on "/a/b/c"; if any of the node doesn't exist, the traversal terminates and
+ * the last seen existing node is returned.
+ * Note:
+ * 1. When the returned TrieNode is a sharding key, it is the only sharding key along the
+ * provided path (the path points to this sharding key);
+ * 2. When the returned TrieNode is not a sharding key but it represents the provided path, the
+ * provided path is a prefix(parent) to a sharding key;
+ * 3. When the returned TrieNode is not a sharding key and it does not represent the provided
+ * path (meaning the traversal ended before the last node of the path is reached), the provided
+ * path is not associated with any sharding key and can be added as a sharding key without
+ * creating ambiguity cases among sharding keys.
* @param path - the path where the search is conducted
- * @param findLeafAlongPath - whether the search is for a leaf node on the path
- * @return the node pointed by the path or a leaf node along the path
- * @throws NoSuchElementException - when the path points to nothing or when no leaf node is
- * found
+ * @return a TrieNode that represents the longest prefix of the path
*/
- private TrieNode findTrieNode(String path, boolean findLeafAlongPath)
- throws NoSuchElementException {
+ private TrieNode getLongestPrefixNodeAlongPath(String path) {
if (path.equals(DELIMITER)) {
- if (findLeafAlongPath && !_rootNode.isShardingKey()) {
- throw new NoSuchElementException("No leaf node found along the path. Path: " + path);
- }
return _rootNode;
}
TrieNode curNode = _rootNode;
- if (findLeafAlongPath && curNode.isShardingKey()) {
- return curNode;
- }
- Map<String, TrieNode> curChildren = curNode.getChildren();
+ TrieNode nextNode;
for (String pathSection : path.substring(1).split(DELIMITER, 0)) {
- curNode = curChildren.get(pathSection);
- if (curNode == null) {
- throw new NoSuchElementException(
- "The provided path is missing from the trie. Path: " + path);
- }
- if (findLeafAlongPath && curNode.isShardingKey()) {
+ nextNode = curNode.getChildren().get(pathSection);
+ if (nextNode == null) {
return curNode;
}
- curChildren = curNode.getChildren();
- }
- if (findLeafAlongPath) {
- throw new NoSuchElementException("No leaf node found along the path. Path: " + path);
+ curNode = nextNode;
}
return curNode;
}
- /**
+ /*
* Checks for the edge case when the only sharding key in provided routing data is the delimiter
* or an empty string. When this is the case, the trie is valid and contains only one node, which
* is the root node, and the root node is a leaf node with a realm address associated with it.
@@ -154,7 +168,7 @@ public class TrieRoutingData implements MetadataStoreRoutingData {
return false;
}
- /**
+ /*
* Constructs a trie based on the provided routing data. It loops through all sharding keys and
* constructs the trie in a top down manner.
* @param routingData- a mapping from "sharding keys" to "realm addresses" to be parsed into a
@@ -169,9 +183,9 @@ public class TrieRoutingData implements MetadataStoreRoutingData {
for (Map.Entry<String, List<String>> entry : routingData.entrySet()) {
for (String shardingKey : entry.getValue()) {
// Missing leading delimiter is invalid
- if (shardingKey.isEmpty() || !shardingKey.substring(0, 1).equals(DELIMITER)) {
- throw new InvalidRoutingDataException("Sharding key does not have a leading \""
- + DELIMITER + "\" character: " + shardingKey);
+ if (!ZkValidationUtil.isPathValid(shardingKey)) {
+ throw new InvalidRoutingDataException(
+ "Sharding key is not a valid Zookeeper path: " + shardingKey);
}
// Root can only be a sharding key if it's the only sharding key. Since this method is
@@ -195,12 +209,14 @@ public class TrieRoutingData implements MetadataStoreRoutingData {
// If the node is already a leaf node, the current sharding key is invalid; if the node
// doesn't exist, construct a node and continue
if (nextNode != null && nextNode.isShardingKey()) {
- throw new InvalidRoutingDataException(shardingKey + " cannot be a sharding key because "
- + shardingKey.substring(0, nextDelimiterIndex)
- + " is its parent key and is also a sharding key.");
+ throw new InvalidRoutingDataException(
+ shardingKey + " cannot be a sharding key because " + shardingKey
+ .substring(0, nextDelimiterIndex)
+ + " is its parent key and is also a sharding key.");
} else if (nextNode == null) {
- nextNode = new TrieNode(new HashMap<>(), shardingKey.substring(0, nextDelimiterIndex),
- false, "");
+ nextNode =
+ new TrieNode(new HashMap<>(), shardingKey.substring(0, nextDelimiterIndex), false,
+ "");
curNode.addChild(keySection, nextNode);
}
prevDelimiterIndex = nextDelimiterIndex;
@@ -224,22 +240,22 @@ public class TrieRoutingData implements MetadataStoreRoutingData {
}
private static class TrieNode {
- /**
+ /*
* This field is a mapping between trie key and children nodes. For example, node "a" has
* children "ab" and "ac", therefore the keys are "b" and "c" respectively.
*/
private Map<String, TrieNode> _children;
- /**
+ /*
* This field states whether the path represented by the node is a sharding key
*/
private final boolean _isShardingKey;
- /**
+ /*
* This field contains the complete path/prefix leading to the current node. For example, the
* name of root node is "/", then the name of its child node
* is "/a", and the name of the child's child node is "/a/b".
*/
private final String _path;
- /**
+ /*
* This field represents the data contained in a node(which represents a path), and is only
* available to the terminal nodes.
*/
diff --git a/helix-rest/src/main/java/org/apache/helix/rest/metadatastore/ZkMetadataStoreDirectory.java b/helix-rest/src/main/java/org/apache/helix/rest/metadatastore/ZkMetadataStoreDirectory.java
index 3be9b72..a57e08c 100644
--- a/helix-rest/src/main/java/org/apache/helix/rest/metadatastore/ZkMetadataStoreDirectory.java
+++ b/helix-rest/src/main/java/org/apache/helix/rest/metadatastore/ZkMetadataStoreDirectory.java
@@ -37,6 +37,7 @@ import org.apache.helix.rest.metadatastore.exceptions.InvalidRoutingDataExceptio
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+
/**
* ZK-based MetadataStoreDirectory that listens on the routing data in routing ZKs with a update
* callback.
@@ -157,10 +158,18 @@ public class ZkMetadataStoreDirectory implements MetadataStoreDirectory, Routing
@Override
public boolean addShardingKey(String namespace, String realm, String shardingKey) {
- if (!_routingDataWriterMap.containsKey(namespace)) {
+ if (!_routingDataWriterMap.containsKey(namespace) || !_routingDataMap.containsKey(namespace)) {
throw new IllegalArgumentException(
"Failed to add sharding key: Namespace " + namespace + " is not found!");
}
+ if (_routingDataMap.get(namespace).containsKeyRealmPair(shardingKey, realm)) {
+ return true;
+ }
+ if (!_routingDataMap.get(namespace).isShardingKeyInsertionValid(shardingKey)) {
+ throw new IllegalArgumentException(
+ "Failed to add sharding key: Adding sharding key " + shardingKey
+ + " makes routing data invalid!");
+ }
return _routingDataWriterMap.get(namespace).addShardingKey(realm, shardingKey);
}
@@ -210,7 +219,6 @@ public class ZkMetadataStoreDirectory implements MetadataStoreDirectory, Routing
} catch (InvalidRoutingDataException e) {
LOG.error("Failed to refresh cached routing data for namespace {}", namespace, e);
}
-
}
@Override
diff --git a/helix-rest/src/main/java/org/apache/helix/rest/server/resources/helix/PropertyStoreAccessor.java b/helix-rest/src/main/java/org/apache/helix/rest/server/resources/helix/PropertyStoreAccessor.java
index 4ccc610..e50d67e 100644
--- a/helix-rest/src/main/java/org/apache/helix/rest/server/resources/helix/PropertyStoreAccessor.java
+++ b/helix-rest/src/main/java/org/apache/helix/rest/server/resources/helix/PropertyStoreAccessor.java
@@ -31,6 +31,7 @@ import org.apache.helix.zookeeper.datamodel.ZNRecord;
import org.apache.helix.manager.zk.ZNRecordSerializer;
import org.apache.helix.manager.zk.ZkBaseDataAccessor;
import org.apache.helix.rest.server.resources.zookeeper.ZooKeeperAccessor;
+import org.apache.helix.zookeeper.util.ZkValidationUtil;
import org.codehaus.jackson.node.ObjectNode;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -56,7 +57,7 @@ public class PropertyStoreAccessor extends AbstractHelixResource {
public Response getPropertyByPath(@PathParam("clusterId") String clusterId,
@PathParam("path") String path) {
path = "/" + path;
- if (!ZooKeeperAccessor.isPathValid(path)) {
+ if (!ZkValidationUtil.isPathValid(path)) {
LOG.info("The propertyStore path {} is invalid for cluster {}", path, clusterId);
return badRequest(
"Invalid path string. Valid path strings use slash as the directory separator and names the location of ZNode");
diff --git a/helix-rest/src/main/java/org/apache/helix/rest/server/resources/zookeeper/ZooKeeperAccessor.java b/helix-rest/src/main/java/org/apache/helix/rest/server/resources/zookeeper/ZooKeeperAccessor.java
index 0774bc7..bc2da05 100644
--- a/helix-rest/src/main/java/org/apache/helix/rest/server/resources/zookeeper/ZooKeeperAccessor.java
+++ b/helix-rest/src/main/java/org/apache/helix/rest/server/resources/zookeeper/ZooKeeperAccessor.java
@@ -35,6 +35,7 @@ import org.apache.helix.manager.zk.ZkBaseDataAccessor;
import org.apache.helix.rest.common.ContextPropertyKeys;
import org.apache.helix.rest.server.ServerContext;
import org.apache.helix.rest.server.resources.AbstractResource;
+import org.apache.helix.zookeeper.util.ZkValidationUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -72,7 +73,7 @@ public class ZooKeeperAccessor extends AbstractResource {
path = "/" + path;
// Check that the path supplied is valid
- if (!isPathValid(path)) {
+ if (!ZkValidationUtil.isPathValid(path)) {
String errMsg = "The given path is not a valid ZooKeeper path: " + path;
LOG.info(errMsg);
return badRequest(errMsg);
@@ -170,20 +171,4 @@ public class ZooKeeperAccessor extends AbstractResource {
private ZooKeeperCommand getZooKeeperCommandIfPresent(String command) {
return Enums.getIfPresent(ZooKeeperCommand.class, command).orNull();
}
-
- /**
- * Validates whether a given path string is a valid ZK path.
- *
- * Valid matches:
- * /
- * /abc
- * /abc/abc/abc/abc
- * Invalid matches:
- * null or empty string
- * /abc/
- * /abc/abc/abc/abc/
- **/
- public static boolean isPathValid(String path) {
- return path.matches("^/|(/[\\w-]+)+$");
- }
}
diff --git a/helix-rest/src/test/java/org/apache/helix/rest/metadatastore/TestTrieRoutingData.java b/helix-rest/src/test/java/org/apache/helix/rest/metadatastore/TestTrieRoutingData.java
index 4de68a6..dedde19 100644
--- a/helix-rest/src/test/java/org/apache/helix/rest/metadatastore/TestTrieRoutingData.java
+++ b/helix-rest/src/test/java/org/apache/helix/rest/metadatastore/TestTrieRoutingData.java
@@ -25,10 +25,12 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
+
import org.apache.helix.rest.metadatastore.exceptions.InvalidRoutingDataException;
import org.testng.Assert;
import org.testng.annotations.Test;
+
public class TestTrieRoutingData {
private TrieRoutingData _trie;
@@ -76,8 +78,8 @@ public class TestTrieRoutingData {
new TrieRoutingData(routingData);
Assert.fail("Expecting InvalidRoutingDataException");
} catch (InvalidRoutingDataException e) {
- Assert.assertTrue(
- e.getMessage().contains("Sharding key does not have a leading \"/\" character: b/c/d"));
+ Assert
+ .assertTrue(e.getMessage().contains("Sharding key is not a valid Zookeeper path: b/c/d"));
}
}
@@ -151,8 +153,7 @@ public class TestTrieRoutingData {
_trie.getAllMappingUnderPath("");
Assert.fail("Expecting IllegalArgumentException");
} catch (IllegalArgumentException e) {
- Assert.assertTrue(e.getMessage()
- .contains("Provided path is empty or does not have a leading \"/\" character: "));
+ Assert.assertTrue(e.getMessage().contains("Provided path is not a valid Zookeeper path: "));
}
}
@@ -162,8 +163,8 @@ public class TestTrieRoutingData {
_trie.getAllMappingUnderPath("test");
Assert.fail("Expecting IllegalArgumentException");
} catch (IllegalArgumentException e) {
- Assert.assertTrue(e.getMessage()
- .contains("Provided path is empty or does not have a leading \"/\" character: test"));
+ Assert
+ .assertTrue(e.getMessage().contains("Provided path is not a valid Zookeeper path: test"));
}
}
@@ -207,8 +208,7 @@ public class TestTrieRoutingData {
Assert.assertEquals(_trie.getMetadataStoreRealm(""), "realmAddress2");
Assert.fail("Expecting IllegalArgumentException");
} catch (IllegalArgumentException e) {
- Assert.assertTrue(e.getMessage()
- .contains("Provided path is empty or does not have a leading \"/\" character: "));
+ Assert.assertTrue(e.getMessage().contains("Provided path is not a valid Zookeeper path: "));
}
}
@@ -218,8 +218,8 @@ public class TestTrieRoutingData {
Assert.assertEquals(_trie.getMetadataStoreRealm("b/c/d/x/y/z"), "realmAddress2");
Assert.fail("Expecting IllegalArgumentException");
} catch (IllegalArgumentException e) {
- Assert.assertTrue(e.getMessage().contains(
- "Provided path is empty or does not have a leading \"/\" character: b/c/d/x/y/z"));
+ Assert.assertTrue(
+ e.getMessage().contains("Provided path is not a valid Zookeeper path: b/c/d/x/y/z"));
}
}
@@ -239,7 +239,7 @@ public class TestTrieRoutingData {
Assert.fail("Expecting NoSuchElementException");
} catch (NoSuchElementException e) {
Assert.assertTrue(
- e.getMessage().contains("The provided path is missing from the trie. Path: /x/y/z"));
+ e.getMessage().contains("No sharding key found within the provided path. Path: /x/y/z"));
}
}
@@ -249,7 +249,59 @@ public class TestTrieRoutingData {
_trie.getMetadataStoreRealm("/b/c");
Assert.fail("Expecting NoSuchElementException");
} catch (NoSuchElementException e) {
- Assert.assertTrue(e.getMessage().contains("No leaf node found along the path. Path: /b/c"));
+ Assert.assertTrue(
+ e.getMessage().contains("No sharding key found within the provided path. Path: /b/c"));
}
}
+
+ @Test(dependsOnMethods = "testConstructionNormal")
+ public void testIsShardingKeyInsertionValidNoSlash() {
+ try {
+ _trie.isShardingKeyInsertionValid("x/y/z");
+ Assert.fail("Expecting IllegalArgumentException");
+ } catch (IllegalArgumentException e) {
+ Assert.assertTrue(
+ e.getMessage().contains("Provided shardingKey is not a valid Zookeeper path: x/y/z"));
+ }
+ }
+
+ @Test(dependsOnMethods = "testConstructionNormal")
+ public void testIsShardingKeyInsertionValidSlashOnly() {
+ Assert.assertFalse(_trie.isShardingKeyInsertionValid("/"));
+ }
+
+ @Test(dependsOnMethods = "testConstructionNormal")
+ public void testIsShardingKeyInsertionValidNormal() {
+ Assert.assertTrue(_trie.isShardingKeyInsertionValid("/x/y/z"));
+ }
+
+ @Test(dependsOnMethods = "testConstructionNormal")
+ public void testIsShardingKeyInsertionValidParentKey() {
+ Assert.assertFalse(_trie.isShardingKeyInsertionValid("/b/c"));
+ }
+
+ @Test(dependsOnMethods = "testConstructionNormal")
+ public void testIsShardingKeyInsertionValidSameKey() {
+ Assert.assertFalse(_trie.isShardingKeyInsertionValid("/h/i"));
+ }
+
+ @Test(dependsOnMethods = "testConstructionNormal")
+ public void testIsShardingKeyInsertionValidChildKey() {
+ Assert.assertFalse(_trie.isShardingKeyInsertionValid("/h/i/k"));
+ }
+
+ @Test(dependsOnMethods = "testConstructionNormal")
+ public void testContainsKeyRealmPair() {
+ Assert.assertTrue(_trie.containsKeyRealmPair("/h/i", "realmAddress1"));
+ }
+
+ @Test(dependsOnMethods = "testConstructionNormal")
+ public void testContainsKeyRealmPairNoKey() {
+ Assert.assertFalse(_trie.containsKeyRealmPair("/h/i/k", "realmAddress1"));
+ }
+
+ @Test(dependsOnMethods = "testConstructionNormal")
+ public void testContainsKeyRealmPairNoRealm() {
+ Assert.assertFalse(_trie.containsKeyRealmPair("/h/i", "realmAddress0"));
+ }
}
diff --git a/zookeeper-api/src/main/java/org/apache/helix/zookeeper/util/ZkValidationUtil.java b/zookeeper-api/src/main/java/org/apache/helix/zookeeper/util/ZkValidationUtil.java
new file mode 100644
index 0000000..59070ac
--- /dev/null
+++ b/zookeeper-api/src/main/java/org/apache/helix/zookeeper/util/ZkValidationUtil.java
@@ -0,0 +1,38 @@
+package org.apache.helix.zookeeper.util;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+public class ZkValidationUtil {
+ /**
+ * Validates whether a given path string is a valid ZK path.
+ *
+ * Valid matches:
+ * /
+ * /abc
+ * /abc/abc/abc/abc
+ * Invalid matches:
+ * null or empty string
+ * /abc/
+ * /abc/abc/abc/abc/
+ **/
+ public static boolean isPathValid(String path) {
+ return path.matches("^/|(/[\\w-]+)+$");
+ }
+}