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}