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}