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