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