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}