diff --git a/src/main/java/edu/wpi/first/networktables/NetworkTable.java b/src/main/java/edu/wpi/first/networktables/NetworkTable.java index 2ff3c2917b..226b7c3a23 100644 --- a/src/main/java/edu/wpi/first/networktables/NetworkTable.java +++ b/src/main/java/edu/wpi/first/networktables/NetworkTable.java @@ -7,7 +7,9 @@ package edu.wpi.first.networktables; +import java.util.ArrayList; import java.util.HashSet; +import java.util.List; import java.util.Objects; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; @@ -28,6 +30,93 @@ public final class NetworkTable { private final String pathWithSep; private final NetworkTableInstance inst; + /** + * Gets the "base name" of a key. For example, "/foo/bar" becomes "bar". + * If the key has a trailing slash, returns an empty string. + * @param key key + * @return base name + */ + public static String basenameKey(String key) { + final int slash = key.lastIndexOf(PATH_SEPARATOR); + if (slash == -1) { + return key; + } + return key.substring(slash + 1); + } + + /** + * Normalizes an network table key to contain no consecutive slashes and + * optionally start with a leading slash. For example: + * + *

+   * normalizeKey("/foo/bar", true)  == "/foo/bar"
+   * normalizeKey("foo/bar", true)   == "/foo/bar"
+   * normalizeKey("/foo/bar", false) == "foo/bar"
+   * normalizeKey("foo//bar", false) == "foo/bar"
+   * 
+ * + * @param key the key to normalize + * @param withLeadingSlash whether or not the normalized key should begin + * with a leading slash + * @return normalized key + */ + public static String normalizeKey(String key, boolean withLeadingSlash) { + String normalized; + if (withLeadingSlash) { + normalized = PATH_SEPARATOR + key; + } else { + normalized = key; + } + normalized = normalized.replaceAll(PATH_SEPARATOR + "{2,}", String.valueOf(PATH_SEPARATOR)); + + if (!withLeadingSlash && normalized.charAt(0) == PATH_SEPARATOR) { + // remove leading slash, if present + normalized = normalized.substring(1); + } + return normalized; + } + + /** + * Normalizes a network table key to start with exactly one leading slash + * ("/") and contain no consecutive slashes. For example, + * {@code "//foo/bar/"} becomes {@code "/foo/bar/"} and + * {@code "///a/b/c"} becomes {@code "/a/b/c"}. + * + *

This is equivalent to {@code normalizeKey(key, true)} + * + * @param key the key to normalize + * @return normalized key + */ + public static String normalizeKey(String key) { + return normalizeKey(key, true); + } + + /** + * Gets a list of the names of all the super tables of a given key. For + * example, the key "/foo/bar/baz" has a hierarchy of "/", "/foo", + * "/foo/bar", and "/foo/bar/baz". + * @param key the key + * @return List of super tables + */ + public static List getHierarchy(String key) { + final String normal = normalizeKey(key, true); + List hierarchy = new ArrayList<>(); + if (normal.length() == 1) { + hierarchy.add(normal); + return hierarchy; + } + for (int i = 1; ; i = normal.indexOf(PATH_SEPARATOR, i + 1)) { + if (i == -1) { + // add the full key + hierarchy.add(normal); + break; + } else { + hierarchy.add(normal.substring(0, i)); + } + } + return hierarchy; + } + /** * Constructor. Use NetworkTableInstance.getTable() or getSubTable() instead. */ diff --git a/src/main/native/cpp/networktables/NetworkTable.cpp b/src/main/native/cpp/networktables/NetworkTable.cpp index 07361bba9d..138cfcea76 100644 --- a/src/main/native/cpp/networktables/NetworkTable.cpp +++ b/src/main/native/cpp/networktables/NetworkTable.cpp @@ -22,6 +22,56 @@ bool NetworkTable::s_enable_ds = true; bool NetworkTable::s_running = false; unsigned int NetworkTable::s_port = NT_DEFAULT_PORT; +StringRef NetworkTable::BasenameKey(StringRef key) { + size_t slash = key.rfind(PATH_SEPARATOR_CHAR); + if (slash == StringRef::npos) return key; + return key.substr(slash + 1); +} + +std::string NetworkTable::NormalizeKey(StringRef key, bool withLeadingSlash) { + llvm::SmallString<128> buf; + return NormalizeKey(key, buf, withLeadingSlash); +} + +StringRef NetworkTable::NormalizeKey(StringRef key, + llvm::SmallVectorImpl& buf, + bool withLeadingSlash) { + buf.clear(); + if (withLeadingSlash) buf.push_back(PATH_SEPARATOR_CHAR); + // for each path element, add it with a slash following + llvm::SmallVector parts; + key.split(parts, PATH_SEPARATOR_CHAR, -1, false); + for (auto i = parts.begin(); i != parts.end(); ++i) { + buf.append(i->begin(), i->end()); + buf.push_back(PATH_SEPARATOR_CHAR); + } + // remove trailing slash if the input key didn't have one + if (!key.empty() && key.back() != PATH_SEPARATOR_CHAR) buf.pop_back(); + return StringRef(buf.data(), buf.size()); +} + +std::vector NetworkTable::GetHierarchy(StringRef key) { + std::vector hierarchy; + hierarchy.emplace_back(1, PATH_SEPARATOR_CHAR); + // for each path element, add it to the end of what we built previously + llvm::SmallString<128> path; + llvm::SmallVector parts; + key.split(parts, PATH_SEPARATOR_CHAR, -1, false); + if (!parts.empty()) { + for (auto i = parts.begin(); i != parts.end(); ++i) { + path += PATH_SEPARATOR_CHAR; + path += *i; + hierarchy.emplace_back(path.str()); + } + // handle trailing slash + if (key.back() == PATH_SEPARATOR_CHAR) { + path += PATH_SEPARATOR_CHAR; + hierarchy.emplace_back(path.str()); + } + } + return hierarchy; +} + void NetworkTable::Initialize() { if (s_running) Shutdown(); auto inst = NetworkTableInstance::GetDefault(); diff --git a/src/main/native/include/networktables/NetworkTable.h b/src/main/native/include/networktables/NetworkTable.h index aa563d0de6..2e23a2154d 100644 --- a/src/main/native/include/networktables/NetworkTable.h +++ b/src/main/native/include/networktables/NetworkTable.h @@ -11,6 +11,7 @@ #include #include +#include "llvm/ArrayRef.h" #include "llvm/StringMap.h" #include "networktables/NetworkTableEntry.h" #include "networktables/TableEntryListener.h" @@ -54,6 +55,44 @@ class NetworkTable final : public ITable { friend class NetworkTableInstance; public: + /** + * Gets the "base name" of a key. For example, "/foo/bar" becomes "bar". + * If the key has a trailing slash, returns an empty string. + * @param key key + * @return base name + */ + static StringRef BasenameKey(StringRef key); + + /** + * Normalizes an network table key to contain no consecutive slashes and + * optionally start with a leading slash. For example: + * + *


+   * normalizeKey("/foo/bar", true)  == "/foo/bar"
+   * normalizeKey("foo/bar", true)   == "/foo/bar"
+   * normalizeKey("/foo/bar", false) == "foo/bar"
+   * normalizeKey("foo//bar", false) == "foo/bar"
+   * 
+ * + * @param key the key to normalize + * @param withLeadingSlash whether or not the normalized key should begin + * with a leading slash + * @return normalized key + */ + static std::string NormalizeKey(StringRef key, bool withLeadingSlash = true); + + static StringRef NormalizeKey(StringRef key, llvm::SmallVectorImpl& buf, + bool withLeadingSlash = true); + + /** + * Gets a list of the names of all the super tables of a given key. For + * example, the key "/foo/bar/baz" has a hierarchy of "/", "/foo", + * "/foo/bar", and "/foo/bar/baz". + * @param key the key + * @return List of super tables + */ + static std::vector GetHierarchy(StringRef key); + /** * Constructor. Use NetworkTableInstance::GetTable() or GetSubTable() * instead. diff --git a/src/test/java/edu/wpi/first/networktables/NetworkTableTest.java b/src/test/java/edu/wpi/first/networktables/NetworkTableTest.java new file mode 100644 index 0000000000..d8b2967d67 --- /dev/null +++ b/src/test/java/edu/wpi/first/networktables/NetworkTableTest.java @@ -0,0 +1,67 @@ +package edu.wpi.first.networktables; + +import java.util.ArrayList; +import java.util.List; +import junit.framework.TestCase; + +public class NetworkTableTest extends TestCase { + + public void testBasenameKey() { + assertEquals("simple", NetworkTable.basenameKey("simple")); + assertEquals("simple", NetworkTable.basenameKey("one/two/many/simple")); + assertEquals("simple", + NetworkTable.basenameKey("//////an/////awful/key////simple")); + } + + public void testNormalizeKeySlash() { + assertEquals("/", NetworkTable.normalizeKey("///")); + assertEquals("/no/normal/req", NetworkTable.normalizeKey("/no/normal/req")); + assertEquals("/no/leading/slash", + NetworkTable.normalizeKey("no/leading/slash")); + assertEquals( + "/what/an/awful/key/", + NetworkTable.normalizeKey("//////what////an/awful/////key///")); + } + + public void testNormalizeKeyNoSlash() { + assertEquals("a", NetworkTable.normalizeKey("a", false)); + assertEquals("a", NetworkTable.normalizeKey("///a", false)); + assertEquals("leading/slash", + NetworkTable.normalizeKey("/leading/slash", false)); + assertEquals("no/leading/slash", + NetworkTable.normalizeKey("no/leading/slash", false)); + assertEquals( + "what/an/awful/key/", + NetworkTable.normalizeKey("//////what////an/awful/////key///", false)); + } + + public void testGetHierarchyEmpty() { + List expected = new ArrayList<>(); + expected.add("/"); + assertEquals(expected, NetworkTable.getHierarchy("")); + } + + public void testGetHierarchyRoot() { + List expected = new ArrayList<>(); + expected.add("/"); + assertEquals(expected, NetworkTable.getHierarchy("/")); + } + + public void testGetHierarchyNormal() { + List expected = new ArrayList<>(); + expected.add("/"); + expected.add("/foo"); + expected.add("/foo/bar"); + expected.add("/foo/bar/baz"); + assertEquals(expected, NetworkTable.getHierarchy("/foo/bar/baz")); + } + + public void testGetHierarchyTrailingSlash() { + List expected = new ArrayList<>(); + expected.add("/"); + expected.add("/foo"); + expected.add("/foo/bar"); + expected.add("/foo/bar/"); + assertEquals(expected, NetworkTable.getHierarchy("/foo/bar/")); + } +} diff --git a/src/test/native/cpp/NetworkTableTest.cpp b/src/test/native/cpp/NetworkTableTest.cpp index 706f059934..044eaf19cc 100644 --- a/src/test/native/cpp/NetworkTableTest.cpp +++ b/src/test/native/cpp/NetworkTableTest.cpp @@ -13,6 +13,54 @@ class NetworkTableTest : public ::testing::Test {}; +TEST_F(NetworkTableTest, BasenameKey) { + EXPECT_EQ("simple", NetworkTable::BasenameKey("simple")); + EXPECT_EQ("simple", NetworkTable::BasenameKey("one/two/many/simple")); + EXPECT_EQ("simple", + NetworkTable::BasenameKey("//////an/////awful/key////simple")); +} + +TEST_F(NetworkTableTest, NormalizeKeySlash) { + EXPECT_EQ("/", NetworkTable::NormalizeKey("///")); + EXPECT_EQ("/no/normal/req", NetworkTable::NormalizeKey("/no/normal/req")); + EXPECT_EQ("/no/leading/slash", + NetworkTable::NormalizeKey("no/leading/slash")); + EXPECT_EQ("/what/an/awful/key/", + NetworkTable::NormalizeKey("//////what////an/awful/////key///")); +} + +TEST_F(NetworkTableTest, NormalizeKeyNoSlash) { + EXPECT_EQ("a", NetworkTable::NormalizeKey("a", false)); + EXPECT_EQ("a", NetworkTable::NormalizeKey("///a", false)); + EXPECT_EQ("leading/slash", + NetworkTable::NormalizeKey("/leading/slash", false)); + EXPECT_EQ("no/leading/slash", + NetworkTable::NormalizeKey("no/leading/slash", false)); + EXPECT_EQ( + "what/an/awful/key/", + NetworkTable::NormalizeKey("//////what////an/awful/////key///", false)); +} + +TEST_F(NetworkTableTest, GetHierarchyEmpty) { + std::vector expected{"/"}; + ASSERT_EQ(expected, NetworkTable::GetHierarchy("")); +} + +TEST_F(NetworkTableTest, GetHierarchyRoot) { + std::vector expected{"/"}; + ASSERT_EQ(expected, NetworkTable::GetHierarchy("/")); +} + +TEST_F(NetworkTableTest, GetHierarchyNormal) { + std::vector expected{"/", "/foo", "/foo/bar", "/foo/bar/baz"}; + ASSERT_EQ(expected, NetworkTable::GetHierarchy("/foo/bar/baz")); +} + +TEST_F(NetworkTableTest, GetHierarchyTrailingSlash) { + std::vector expected{"/", "/foo", "/foo/bar", "/foo/bar/"}; + ASSERT_EQ(expected, NetworkTable::GetHierarchy("/foo/bar/")); +} + TEST_F(NetworkTableTest, ContainsKey) { auto inst = nt::NetworkTableInstance::Create(); auto nt = inst.GetTable("containskey");