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