001// Copyright (c) FIRST and other WPILib contributors. 002// Open Source Software; you can modify and/or share it under the terms of 003// the WPILib BSD license file in the root directory of this project. 004 005package edu.wpi.first.networktables; 006 007import java.util.ArrayList; 008import java.util.HashSet; 009import java.util.List; 010import java.util.Objects; 011import java.util.Set; 012import java.util.concurrent.ConcurrentHashMap; 013import java.util.concurrent.ConcurrentMap; 014import java.util.function.Consumer; 015 016/** A network table that knows its subtable path. */ 017public final class NetworkTable { 018 /** The path separator for sub-tables and keys. */ 019 public static final char PATH_SEPARATOR = '/'; 020 021 private final String m_path; 022 private final String m_pathWithSep; 023 private final NetworkTableInstance m_inst; 024 025 /** 026 * Gets the "base name" of a key. For example, "/foo/bar" becomes "bar". If the key has a trailing 027 * slash, returns an empty string. 028 * 029 * @param key key 030 * @return base name 031 */ 032 public static String basenameKey(String key) { 033 final int slash = key.lastIndexOf(PATH_SEPARATOR); 034 if (slash == -1) { 035 return key; 036 } 037 return key.substring(slash + 1); 038 } 039 040 /** 041 * Normalizes an network table key to contain no consecutive slashes and optionally start with a 042 * leading slash. For example: 043 * 044 * <pre><code> 045 * normalizeKey("/foo/bar", true) == "/foo/bar" 046 * normalizeKey("foo/bar", true) == "/foo/bar" 047 * normalizeKey("/foo/bar", false) == "foo/bar" 048 * normalizeKey("foo//bar", false) == "foo/bar" 049 * </code></pre> 050 * 051 * @param key the key to normalize 052 * @param withLeadingSlash whether or not the normalized key should begin with a leading slash 053 * @return normalized key 054 */ 055 public static String normalizeKey(String key, boolean withLeadingSlash) { 056 String normalized; 057 if (withLeadingSlash) { 058 normalized = PATH_SEPARATOR + key; 059 } else { 060 normalized = key; 061 } 062 normalized = normalized.replaceAll(PATH_SEPARATOR + "{2,}", String.valueOf(PATH_SEPARATOR)); 063 064 if (!withLeadingSlash && normalized.charAt(0) == PATH_SEPARATOR) { 065 // remove leading slash, if present 066 normalized = normalized.substring(1); 067 } 068 return normalized; 069 } 070 071 /** 072 * Normalizes a network table key to start with exactly one leading slash ("/") and contain no 073 * consecutive slashes. For example, {@code "//foo/bar/"} becomes {@code "/foo/bar/"} and {@code 074 * "///a/b/c"} becomes {@code "/a/b/c"}. 075 * 076 * <p>This is equivalent to {@code normalizeKey(key, true)} 077 * 078 * @param key the key to normalize 079 * @return normalized key 080 */ 081 public static String normalizeKey(String key) { 082 return normalizeKey(key, true); 083 } 084 085 /** 086 * Gets a list of the names of all the super tables of a given key. For example, the key 087 * "/foo/bar/baz" has a hierarchy of "/", "/foo", "/foo/bar", and "/foo/bar/baz". 088 * 089 * @param key the key 090 * @return List of super tables 091 */ 092 public static List<String> getHierarchy(String key) { 093 final String normal = normalizeKey(key, true); 094 List<String> hierarchy = new ArrayList<>(); 095 if (normal.length() == 1) { 096 hierarchy.add(normal); 097 return hierarchy; 098 } 099 for (int i = 1; ; i = normal.indexOf(PATH_SEPARATOR, i + 1)) { 100 if (i == -1) { 101 // add the full key 102 hierarchy.add(normal); 103 break; 104 } else { 105 hierarchy.add(normal.substring(0, i)); 106 } 107 } 108 return hierarchy; 109 } 110 111 /** Constructor. Use NetworkTableInstance.getTable() or getSubTable() instead. */ 112 NetworkTable(NetworkTableInstance inst, String path) { 113 m_path = path; 114 m_pathWithSep = path + PATH_SEPARATOR; 115 m_inst = inst; 116 } 117 118 /** 119 * Gets the instance for the table. 120 * 121 * @return Instance 122 */ 123 public NetworkTableInstance getInstance() { 124 return m_inst; 125 } 126 127 @Override 128 public String toString() { 129 return "NetworkTable: " + m_path; 130 } 131 132 private final ConcurrentMap<String, NetworkTableEntry> m_entries = new ConcurrentHashMap<>(); 133 134 /** 135 * Gets the entry for a sub key. 136 * 137 * @param key the key name 138 * @return Network table entry. 139 */ 140 public NetworkTableEntry getEntry(String key) { 141 NetworkTableEntry entry = m_entries.get(key); 142 if (entry == null) { 143 entry = m_inst.getEntry(m_pathWithSep + key); 144 m_entries.putIfAbsent(key, entry); 145 } 146 return entry; 147 } 148 149 /** 150 * Listen to keys only within this table. 151 * 152 * @param listener listener to add 153 * @param flags {@link EntryListenerFlags} bitmask 154 * @return Listener handle 155 */ 156 public int addEntryListener(TableEntryListener listener, int flags) { 157 final int prefixLen = m_path.length() + 1; 158 return m_inst.addEntryListener( 159 m_pathWithSep, 160 event -> { 161 String relativeKey = event.name.substring(prefixLen); 162 if (relativeKey.indexOf(PATH_SEPARATOR) != -1) { 163 // part of a sub table 164 return; 165 } 166 listener.valueChanged(this, relativeKey, event.getEntry(), event.value, event.flags); 167 }, 168 flags); 169 } 170 171 /** 172 * Listen to a single key. 173 * 174 * @param key the key name 175 * @param listener listener to add 176 * @param flags {@link EntryListenerFlags} bitmask 177 * @return Listener handle 178 */ 179 public int addEntryListener(String key, TableEntryListener listener, int flags) { 180 final NetworkTableEntry entry = getEntry(key); 181 return m_inst.addEntryListener( 182 entry, event -> listener.valueChanged(this, key, entry, event.value, event.flags), flags); 183 } 184 185 /** 186 * Remove an entry listener. 187 * 188 * @param listener listener handle 189 */ 190 public void removeEntryListener(int listener) { 191 m_inst.removeEntryListener(listener); 192 } 193 194 /** 195 * Listen for sub-table creation. This calls the listener once for each newly created sub-table. 196 * It immediately calls the listener for any existing sub-tables. 197 * 198 * @param listener listener to add 199 * @param localNotify notify local changes as well as remote 200 * @return Listener handle 201 */ 202 public int addSubTableListener(TableListener listener, boolean localNotify) { 203 int flags = EntryListenerFlags.kNew | EntryListenerFlags.kImmediate; 204 if (localNotify) { 205 flags |= EntryListenerFlags.kLocal; 206 } 207 208 final int prefixLen = m_path.length() + 1; 209 final NetworkTable parent = this; 210 211 return m_inst.addEntryListener( 212 m_pathWithSep, 213 new Consumer<>() { 214 final Set<String> m_notifiedTables = new HashSet<>(); 215 216 @Override 217 public void accept(EntryNotification event) { 218 String relativeKey = event.name.substring(prefixLen); 219 int endSubTable = relativeKey.indexOf(PATH_SEPARATOR); 220 if (endSubTable == -1) { 221 return; 222 } 223 String subTableKey = relativeKey.substring(0, endSubTable); 224 if (m_notifiedTables.contains(subTableKey)) { 225 return; 226 } 227 m_notifiedTables.add(subTableKey); 228 listener.tableCreated(parent, subTableKey, parent.getSubTable(subTableKey)); 229 } 230 }, 231 flags); 232 } 233 234 /** 235 * Remove a sub-table listener. 236 * 237 * @param listener listener handle 238 */ 239 public void removeTableListener(int listener) { 240 m_inst.removeEntryListener(listener); 241 } 242 243 /** 244 * Returns the table at the specified key. If there is no table at the specified key, it will 245 * create a new table 246 * 247 * @param key the name of the table relative to this one 248 * @return a sub table relative to this one 249 */ 250 public NetworkTable getSubTable(String key) { 251 return new NetworkTable(m_inst, m_pathWithSep + key); 252 } 253 254 /** 255 * Checks the table and tells if it contains the specified key. 256 * 257 * @param key the key to search for 258 * @return true if the table as a value assigned to the given key 259 */ 260 public boolean containsKey(String key) { 261 return !("".equals(key)) && getEntry(key).exists(); 262 } 263 264 /** 265 * Checks the table and tells if it contains the specified sub table. 266 * 267 * @param key the key to search for 268 * @return true if there is a subtable with the key which contains at least one key/subtable of 269 * its own 270 */ 271 public boolean containsSubTable(String key) { 272 int[] handles = 273 NetworkTablesJNI.getEntries(m_inst.getHandle(), m_pathWithSep + key + PATH_SEPARATOR, 0); 274 return handles.length != 0; 275 } 276 277 /** 278 * Gets all keys in the table (not including sub-tables). 279 * 280 * @param types bitmask of types; 0 is treated as a "don't care". 281 * @return keys currently in the table 282 */ 283 public Set<String> getKeys(int types) { 284 Set<String> keys = new HashSet<>(); 285 int prefixLen = m_path.length() + 1; 286 for (EntryInfo info : m_inst.getEntryInfo(m_pathWithSep, types)) { 287 String relativeKey = info.name.substring(prefixLen); 288 if (relativeKey.indexOf(PATH_SEPARATOR) != -1) { 289 continue; 290 } 291 keys.add(relativeKey); 292 // populate entries as we go 293 if (m_entries.get(relativeKey) == null) { 294 m_entries.putIfAbsent(relativeKey, new NetworkTableEntry(m_inst, info.entry)); 295 } 296 } 297 return keys; 298 } 299 300 /** 301 * Gets all keys in the table (not including sub-tables). 302 * 303 * @return keys currently in the table 304 */ 305 public Set<String> getKeys() { 306 return getKeys(0); 307 } 308 309 /** 310 * Gets the names of all subtables in the table. 311 * 312 * @return subtables currently in the table 313 */ 314 public Set<String> getSubTables() { 315 Set<String> keys = new HashSet<>(); 316 int prefixLen = m_path.length() + 1; 317 for (EntryInfo info : m_inst.getEntryInfo(m_pathWithSep, 0)) { 318 String relativeKey = info.name.substring(prefixLen); 319 int endSubTable = relativeKey.indexOf(PATH_SEPARATOR); 320 if (endSubTable == -1) { 321 continue; 322 } 323 keys.add(relativeKey.substring(0, endSubTable)); 324 } 325 return keys; 326 } 327 328 /** 329 * Deletes the specified key in this table. The key can not be null. 330 * 331 * @param key the key name 332 */ 333 public void delete(String key) { 334 getEntry(key).delete(); 335 } 336 337 /** 338 * Put a value in the table. 339 * 340 * @param key the key to be assigned to 341 * @param value the value that will be assigned 342 * @return False if the table key already exists with a different type 343 */ 344 boolean putValue(String key, NetworkTableValue value) { 345 return getEntry(key).setValue(value); 346 } 347 348 /** 349 * Gets the current value in the table, setting it if it does not exist. 350 * 351 * @param key the key 352 * @param defaultValue the default value to set if key doesn't exist. 353 * @return False if the table key exists with a different type 354 */ 355 boolean setDefaultValue(String key, NetworkTableValue defaultValue) { 356 return getEntry(key).setDefaultValue(defaultValue); 357 } 358 359 /** 360 * Gets the value associated with a key as an object. 361 * 362 * @param key the key of the value to look up 363 * @return the value associated with the given key, or nullptr if the key does not exist 364 */ 365 NetworkTableValue getValue(String key) { 366 return getEntry(key).getValue(); 367 } 368 369 /** 370 * Get the path of the NetworkTable. 371 * 372 * @return The path of the NetworkTable. 373 */ 374 public String getPath() { 375 return m_path; 376 } 377 378 /** 379 * Save table values to a file. The file format used is identical to that used for SavePersistent. 380 * 381 * @param filename filename 382 * @throws PersistentException if error saving file 383 */ 384 public void saveEntries(String filename) throws PersistentException { 385 m_inst.saveEntries(filename, m_pathWithSep); 386 } 387 388 /** 389 * Load table values from a file. The file format used is identical to that used for 390 * SavePersistent / LoadPersistent. 391 * 392 * @param filename filename 393 * @return List of warnings (errors result in an exception instead) 394 * @throws PersistentException if error saving file 395 */ 396 public String[] loadEntries(String filename) throws PersistentException { 397 return m_inst.loadEntries(filename, m_pathWithSep); 398 } 399 400 @Override 401 public boolean equals(Object other) { 402 if (other == this) { 403 return true; 404 } 405 if (!(other instanceof NetworkTable)) { 406 return false; 407 } 408 NetworkTable ntOther = (NetworkTable) other; 409 return m_inst.equals(ntOther.m_inst) && m_path.equals(ntOther.m_path); 410 } 411 412 @Override 413 public int hashCode() { 414 return Objects.hash(m_inst, m_path); 415 } 416}