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.EnumSet; 009import java.util.HashSet; 010import java.util.List; 011import java.util.Objects; 012import java.util.Set; 013import java.util.concurrent.ConcurrentHashMap; 014import java.util.concurrent.ConcurrentMap; 015import java.util.function.Consumer; 016 017/** A network table that knows its subtable path. */ 018public final class NetworkTable { 019 /** The path separator for sub-tables and keys. */ 020 public static final char PATH_SEPARATOR = '/'; 021 022 private final String m_path; 023 private final String m_pathWithSep; 024 private final NetworkTableInstance m_inst; 025 026 /** 027 * Gets the "base name" of a key. For example, "/foo/bar" becomes "bar". If the key has a trailing 028 * slash, returns an empty string. 029 * 030 * @param key key 031 * @return base name 032 */ 033 public static String basenameKey(String key) { 034 final int slash = key.lastIndexOf(PATH_SEPARATOR); 035 if (slash == -1) { 036 return key; 037 } 038 return key.substring(slash + 1); 039 } 040 041 /** 042 * Normalizes an network table key to contain no consecutive slashes and optionally start with a 043 * leading slash. For example: 044 * 045 * <pre><code> 046 * normalizeKey("/foo/bar", true) == "/foo/bar" 047 * normalizeKey("foo/bar", true) == "/foo/bar" 048 * normalizeKey("/foo/bar", false) == "foo/bar" 049 * normalizeKey("foo//bar", false) == "foo/bar" 050 * </code></pre> 051 * 052 * @param key the key to normalize 053 * @param withLeadingSlash whether or not the normalized key should begin with a leading slash 054 * @return normalized key 055 */ 056 public static String normalizeKey(String key, boolean withLeadingSlash) { 057 String normalized; 058 if (withLeadingSlash) { 059 normalized = PATH_SEPARATOR + key; 060 } else { 061 normalized = key; 062 } 063 normalized = normalized.replaceAll(PATH_SEPARATOR + "{2,}", String.valueOf(PATH_SEPARATOR)); 064 065 if (!withLeadingSlash && normalized.charAt(0) == PATH_SEPARATOR) { 066 // remove leading slash, if present 067 normalized = normalized.substring(1); 068 } 069 return normalized; 070 } 071 072 /** 073 * Normalizes a network table key to start with exactly one leading slash ("/") and contain no 074 * consecutive slashes. For example, {@code "//foo/bar/"} becomes {@code "/foo/bar/"} and {@code 075 * "///a/b/c"} becomes {@code "/a/b/c"}. 076 * 077 * <p>This is equivalent to {@code normalizeKey(key, true)} 078 * 079 * @param key the key to normalize 080 * @return normalized key 081 */ 082 public static String normalizeKey(String key) { 083 return normalizeKey(key, true); 084 } 085 086 /** 087 * Gets a list of the names of all the super tables of a given key. For example, the key 088 * "/foo/bar/baz" has a hierarchy of "/", "/foo", "/foo/bar", and "/foo/bar/baz". 089 * 090 * @param key the key 091 * @return List of super tables 092 */ 093 public static List<String> getHierarchy(String key) { 094 final String normal = normalizeKey(key, true); 095 List<String> hierarchy = new ArrayList<>(); 096 if (normal.length() == 1) { 097 hierarchy.add(normal); 098 return hierarchy; 099 } 100 for (int i = 1; ; i = normal.indexOf(PATH_SEPARATOR, i + 1)) { 101 if (i == -1) { 102 // add the full key 103 hierarchy.add(normal); 104 break; 105 } else { 106 hierarchy.add(normal.substring(0, i)); 107 } 108 } 109 return hierarchy; 110 } 111 112 /** Constructor. Use NetworkTableInstance.getTable() or getSubTable() instead. */ 113 NetworkTable(NetworkTableInstance inst, String path) { 114 m_path = path; 115 m_pathWithSep = path + PATH_SEPARATOR; 116 m_inst = inst; 117 } 118 119 /** 120 * Gets the instance for the table. 121 * 122 * @return Instance 123 */ 124 public NetworkTableInstance getInstance() { 125 return m_inst; 126 } 127 128 @Override 129 public String toString() { 130 return "NetworkTable: " + m_path; 131 } 132 133 /** 134 * Get (generic) topic. 135 * 136 * @param name topic name 137 * @return Topic 138 */ 139 public Topic getTopic(String name) { 140 return m_inst.getTopic(m_pathWithSep + name); 141 } 142 143 /** 144 * Get boolean topic. 145 * 146 * @param name topic name 147 * @return BooleanTopic 148 */ 149 public BooleanTopic getBooleanTopic(String name) { 150 return m_inst.getBooleanTopic(m_pathWithSep + name); 151 } 152 153 /** 154 * Get long topic. 155 * 156 * @param name topic name 157 * @return IntegerTopic 158 */ 159 public IntegerTopic getIntegerTopic(String name) { 160 return m_inst.getIntegerTopic(m_pathWithSep + name); 161 } 162 163 /** 164 * Get float topic. 165 * 166 * @param name topic name 167 * @return FloatTopic 168 */ 169 public FloatTopic getFloatTopic(String name) { 170 return m_inst.getFloatTopic(m_pathWithSep + name); 171 } 172 173 /** 174 * Get double topic. 175 * 176 * @param name topic name 177 * @return DoubleTopic 178 */ 179 public DoubleTopic getDoubleTopic(String name) { 180 return m_inst.getDoubleTopic(m_pathWithSep + name); 181 } 182 183 /** 184 * Get String topic. 185 * 186 * @param name topic name 187 * @return StringTopic 188 */ 189 public StringTopic getStringTopic(String name) { 190 return m_inst.getStringTopic(m_pathWithSep + name); 191 } 192 193 /** 194 * Get raw topic. 195 * 196 * @param name topic name 197 * @return RawTopic 198 */ 199 public RawTopic getRawTopic(String name) { 200 return m_inst.getRawTopic(m_pathWithSep + name); 201 } 202 203 /** 204 * Get boolean[] topic. 205 * 206 * @param name topic name 207 * @return BooleanArrayTopic 208 */ 209 public BooleanArrayTopic getBooleanArrayTopic(String name) { 210 return m_inst.getBooleanArrayTopic(m_pathWithSep + name); 211 } 212 213 /** 214 * Get long[] topic. 215 * 216 * @param name topic name 217 * @return IntegerArrayTopic 218 */ 219 public IntegerArrayTopic getIntegerArrayTopic(String name) { 220 return m_inst.getIntegerArrayTopic(m_pathWithSep + name); 221 } 222 223 /** 224 * Get float[] topic. 225 * 226 * @param name topic name 227 * @return FloatArrayTopic 228 */ 229 public FloatArrayTopic getFloatArrayTopic(String name) { 230 return m_inst.getFloatArrayTopic(m_pathWithSep + name); 231 } 232 233 /** 234 * Get double[] topic. 235 * 236 * @param name topic name 237 * @return DoubleArrayTopic 238 */ 239 public DoubleArrayTopic getDoubleArrayTopic(String name) { 240 return m_inst.getDoubleArrayTopic(m_pathWithSep + name); 241 } 242 243 /** 244 * Get String[] topic. 245 * 246 * @param name topic name 247 * @return StringArrayTopic 248 */ 249 public StringArrayTopic getStringArrayTopic(String name) { 250 return m_inst.getStringArrayTopic(m_pathWithSep + name); 251 } 252 253 private final ConcurrentMap<String, NetworkTableEntry> m_entries = new ConcurrentHashMap<>(); 254 255 /** 256 * Gets the entry for a sub key. 257 * 258 * @param key the key name 259 * @return Network table entry. 260 */ 261 public NetworkTableEntry getEntry(String key) { 262 NetworkTableEntry entry = m_entries.get(key); 263 if (entry == null) { 264 entry = m_inst.getEntry(m_pathWithSep + key); 265 NetworkTableEntry oldEntry = m_entries.putIfAbsent(key, entry); 266 if (oldEntry != null) { 267 entry = oldEntry; 268 } 269 } 270 return entry; 271 } 272 273 /** 274 * Returns the table at the specified key. If there is no table at the specified key, it will 275 * create a new table 276 * 277 * @param key the name of the table relative to this one 278 * @return a sub table relative to this one 279 */ 280 public NetworkTable getSubTable(String key) { 281 return new NetworkTable(m_inst, m_pathWithSep + key); 282 } 283 284 /** 285 * Checks the table and tells if it contains the specified key. 286 * 287 * @param key the key to search for 288 * @return true if the table as a value assigned to the given key 289 */ 290 public boolean containsKey(String key) { 291 return !("".equals(key)) && getTopic(key).exists(); 292 } 293 294 /** 295 * Checks the table and tells if it contains the specified sub table. 296 * 297 * @param key the key to search for 298 * @return true if there is a subtable with the key which contains at least one key/subtable of 299 * its own 300 */ 301 public boolean containsSubTable(String key) { 302 Topic[] topics = m_inst.getTopics(m_pathWithSep + key + PATH_SEPARATOR, 0); 303 return topics.length != 0; 304 } 305 306 /** 307 * Gets topic information for all keys in the table (not including sub-tables). 308 * 309 * @param types bitmask of types (NetworkTableType values); 0 is treated as a "don't care". 310 * @return topic information for keys currently in the table 311 */ 312 public List<TopicInfo> getTopicInfo(int types) { 313 List<TopicInfo> infos = new ArrayList<>(); 314 int prefixLen = m_path.length() + 1; 315 for (TopicInfo info : m_inst.getTopicInfo(m_pathWithSep, types)) { 316 String relativeKey = info.name.substring(prefixLen); 317 if (relativeKey.indexOf(PATH_SEPARATOR) != -1) { 318 continue; 319 } 320 infos.add(info); 321 } 322 return infos; 323 } 324 325 /** 326 * Gets topic information for all keys in the table (not including sub-tables). 327 * 328 * @return topic information for keys currently in the table 329 */ 330 public List<TopicInfo> getTopicInfo() { 331 return getTopicInfo(0); 332 } 333 334 /** 335 * Gets all topics in the table (not including sub-tables). 336 * 337 * @param types bitmask of types (NetworkTableType values); 0 is treated as a "don't care". 338 * @return topic for keys currently in the table 339 */ 340 public List<Topic> getTopics(int types) { 341 List<Topic> topics = new ArrayList<>(); 342 int prefixLen = m_path.length() + 1; 343 for (TopicInfo info : m_inst.getTopicInfo(m_pathWithSep, types)) { 344 String relativeKey = info.name.substring(prefixLen); 345 if (relativeKey.indexOf(PATH_SEPARATOR) != -1) { 346 continue; 347 } 348 topics.add(info.getTopic()); 349 } 350 return topics; 351 } 352 353 /** 354 * Gets all topics in the table (not including sub-tables). 355 * 356 * @return topic for keys currently in the table 357 */ 358 public List<Topic> getTopics() { 359 return getTopics(0); 360 } 361 362 /** 363 * Gets all keys in the table (not including sub-tables). 364 * 365 * @param types bitmask of types; 0 is treated as a "don't care". 366 * @return keys currently in the table 367 */ 368 public Set<String> getKeys(int types) { 369 Set<String> keys = new HashSet<>(); 370 int prefixLen = m_path.length() + 1; 371 for (TopicInfo info : m_inst.getTopicInfo(m_pathWithSep, types)) { 372 String relativeKey = info.name.substring(prefixLen); 373 if (relativeKey.indexOf(PATH_SEPARATOR) != -1) { 374 continue; 375 } 376 keys.add(relativeKey); 377 } 378 return keys; 379 } 380 381 /** 382 * Gets all keys in the table (not including sub-tables). 383 * 384 * @return keys currently in the table 385 */ 386 public Set<String> getKeys() { 387 return getKeys(0); 388 } 389 390 /** 391 * Gets the names of all subtables in the table. 392 * 393 * @return subtables currently in the table 394 */ 395 public Set<String> getSubTables() { 396 Set<String> keys = new HashSet<>(); 397 int prefixLen = m_path.length() + 1; 398 for (TopicInfo info : m_inst.getTopicInfo(m_pathWithSep, 0)) { 399 String relativeKey = info.name.substring(prefixLen); 400 int endSubTable = relativeKey.indexOf(PATH_SEPARATOR); 401 if (endSubTable == -1) { 402 continue; 403 } 404 keys.add(relativeKey.substring(0, endSubTable)); 405 } 406 return keys; 407 } 408 409 /** 410 * Put a value in the table. 411 * 412 * @param key the key to be assigned to 413 * @param value the value that will be assigned 414 * @return False if the table key already exists with a different type 415 */ 416 public boolean putValue(String key, NetworkTableValue value) { 417 return getEntry(key).setValue(value); 418 } 419 420 /** 421 * Gets the current value in the table, setting it if it does not exist. 422 * 423 * @param key the key 424 * @param defaultValue the default value to set if key doesn't exist. 425 * @return False if the table key exists with a different type 426 */ 427 public boolean setDefaultValue(String key, NetworkTableValue defaultValue) { 428 return getEntry(key).setDefaultValue(defaultValue); 429 } 430 431 /** 432 * Gets the value associated with a key as an object. 433 * 434 * @param key the key of the value to look up 435 * @return the value associated with the given key, or nullptr if the key does not exist 436 */ 437 public NetworkTableValue getValue(String key) { 438 return getEntry(key).getValue(); 439 } 440 441 /** 442 * Get the path of the NetworkTable. 443 * 444 * @return The path of the NetworkTable. 445 */ 446 public String getPath() { 447 return m_path; 448 } 449 450 /** A listener that listens to events on topics in a {@link NetworkTable}. */ 451 @FunctionalInterface 452 public interface TableEventListener { 453 /** 454 * Called when an event occurs on a topic in a {@link NetworkTable}. 455 * 456 * @param table the table the topic exists in 457 * @param key the key associated with the topic that changed 458 * @param event the event 459 */ 460 void accept(NetworkTable table, String key, NetworkTableEvent event); 461 } 462 463 /** 464 * Listen to topics only within this table. 465 * 466 * @param eventKinds set of event kinds to listen to 467 * @param listener listener to add 468 * @return Listener handle 469 */ 470 public int addListener(EnumSet<NetworkTableEvent.Kind> eventKinds, TableEventListener listener) { 471 final int prefixLen = m_path.length() + 1; 472 return m_inst.addListener( 473 new String[] {m_pathWithSep}, 474 eventKinds, 475 event -> { 476 String topicName = null; 477 if (event.topicInfo != null) { 478 topicName = event.topicInfo.name; 479 } else if (event.valueData != null) { 480 topicName = event.valueData.getTopic().getName(); 481 } 482 if (topicName == null) { 483 return; 484 } 485 String relativeKey = topicName.substring(prefixLen); 486 if (relativeKey.indexOf(PATH_SEPARATOR) != -1) { 487 // part of a sub table 488 return; 489 } 490 listener.accept(this, relativeKey, event); 491 }); 492 } 493 494 /** 495 * Listen to a single key. 496 * 497 * @param key the key name 498 * @param eventKinds set of event kinds to listen to 499 * @param listener listener to add 500 * @return Listener handle 501 */ 502 public int addListener( 503 String key, EnumSet<NetworkTableEvent.Kind> eventKinds, TableEventListener listener) { 504 NetworkTableEntry entry = getEntry(key); 505 return m_inst.addListener(entry, eventKinds, event -> listener.accept(this, key, event)); 506 } 507 508 /** A listener that listens to new tables in a {@link NetworkTable}. */ 509 @FunctionalInterface 510 public interface SubTableListener { 511 /** 512 * Called when a new table is created within a {@link NetworkTable}. 513 * 514 * @param parent the parent of the table 515 * @param name the name of the new table 516 * @param table the new table 517 */ 518 void tableCreated(NetworkTable parent, String name, NetworkTable table); 519 } 520 521 /** 522 * Listen for sub-table creation. This calls the listener once for each newly created sub-table. 523 * It immediately calls the listener for any existing sub-tables. 524 * 525 * @param listener listener to add 526 * @return Listener handle 527 */ 528 public int addSubTableListener(SubTableListener listener) { 529 final int prefixLen = m_path.length() + 1; 530 final NetworkTable parent = this; 531 532 return m_inst.addListener( 533 new String[] {m_pathWithSep}, 534 EnumSet.of(NetworkTableEvent.Kind.kPublish, NetworkTableEvent.Kind.kImmediate), 535 new Consumer<NetworkTableEvent>() { 536 final Set<String> m_notifiedTables = new HashSet<>(); 537 538 @Override 539 public void accept(NetworkTableEvent event) { 540 if (event.topicInfo == null) { 541 return; // should not happen 542 } 543 String relativeKey = event.topicInfo.name.substring(prefixLen); 544 int endSubTable = relativeKey.indexOf(PATH_SEPARATOR); 545 if (endSubTable == -1) { 546 return; 547 } 548 String subTableKey = relativeKey.substring(0, endSubTable); 549 if (m_notifiedTables.contains(subTableKey)) { 550 return; 551 } 552 m_notifiedTables.add(subTableKey); 553 listener.tableCreated(parent, subTableKey, parent.getSubTable(subTableKey)); 554 } 555 }); 556 } 557 558 /** 559 * Remove a listener. 560 * 561 * @param listener listener handle 562 */ 563 public void removeListener(int listener) { 564 m_inst.removeListener(listener); 565 } 566 567 @Override 568 public boolean equals(Object other) { 569 if (other == this) { 570 return true; 571 } 572 if (!(other instanceof NetworkTable)) { 573 return false; 574 } 575 NetworkTable ntOther = (NetworkTable) other; 576 return m_inst.equals(ntOther.m_inst) && m_path.equals(ntOther.m_path); 577 } 578 579 @Override 580 public int hashCode() { 581 return Objects.hash(m_inst, m_path); 582 } 583}