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.HashSet;
009import java.util.List;
010import java.util.Objects;
011import java.util.Set;
012import java.util.concurrent.ConcurrentHashMap;
013import java.util.concurrent.ConcurrentMap;
014import java.util.function.Consumer;
015
016/** A network table that knows its subtable path. */
017public final class NetworkTable {
018  /** The path separator for sub-tables and keys. */
019  public static final char PATH_SEPARATOR = '/';
020
021  private final String m_path;
022  private final String m_pathWithSep;
023  private final NetworkTableInstance m_inst;
024
025  /**
026   * Gets the "base name" of a key. For example, "/foo/bar" becomes "bar". If the key has a trailing
027   * slash, returns an empty string.
028   *
029   * @param key key
030   * @return base name
031   */
032  public static String basenameKey(String key) {
033    final int slash = key.lastIndexOf(PATH_SEPARATOR);
034    if (slash == -1) {
035      return key;
036    }
037    return key.substring(slash + 1);
038  }
039
040  /**
041   * Normalizes an network table key to contain no consecutive slashes and optionally start with a
042   * leading slash. For example:
043   *
044   * <pre><code>
045   * normalizeKey("/foo/bar", true)  == "/foo/bar"
046   * normalizeKey("foo/bar", true)   == "/foo/bar"
047   * normalizeKey("/foo/bar", false) == "foo/bar"
048   * normalizeKey("foo//bar", false) == "foo/bar"
049   * </code></pre>
050   *
051   * @param key the key to normalize
052   * @param withLeadingSlash whether or not the normalized key should begin with a leading slash
053   * @return normalized key
054   */
055  public static String normalizeKey(String key, boolean withLeadingSlash) {
056    String normalized;
057    if (withLeadingSlash) {
058      normalized = PATH_SEPARATOR + key;
059    } else {
060      normalized = key;
061    }
062    normalized = normalized.replaceAll(PATH_SEPARATOR + "{2,}", String.valueOf(PATH_SEPARATOR));
063
064    if (!withLeadingSlash && normalized.charAt(0) == PATH_SEPARATOR) {
065      // remove leading slash, if present
066      normalized = normalized.substring(1);
067    }
068    return normalized;
069  }
070
071  /**
072   * Normalizes a network table key to start with exactly one leading slash ("/") and contain no
073   * consecutive slashes. For example, {@code "//foo/bar/"} becomes {@code "/foo/bar/"} and {@code
074   * "///a/b/c"} becomes {@code "/a/b/c"}.
075   *
076   * <p>This is equivalent to {@code normalizeKey(key, true)}
077   *
078   * @param key the key to normalize
079   * @return normalized key
080   */
081  public static String normalizeKey(String key) {
082    return normalizeKey(key, true);
083  }
084
085  /**
086   * Gets a list of the names of all the super tables of a given key. For example, the key
087   * "/foo/bar/baz" has a hierarchy of "/", "/foo", "/foo/bar", and "/foo/bar/baz".
088   *
089   * @param key the key
090   * @return List of super tables
091   */
092  public static List<String> getHierarchy(String key) {
093    final String normal = normalizeKey(key, true);
094    List<String> hierarchy = new ArrayList<>();
095    if (normal.length() == 1) {
096      hierarchy.add(normal);
097      return hierarchy;
098    }
099    for (int i = 1; ; i = normal.indexOf(PATH_SEPARATOR, i + 1)) {
100      if (i == -1) {
101        // add the full key
102        hierarchy.add(normal);
103        break;
104      } else {
105        hierarchy.add(normal.substring(0, i));
106      }
107    }
108    return hierarchy;
109  }
110
111  /** Constructor. Use NetworkTableInstance.getTable() or getSubTable() instead. */
112  NetworkTable(NetworkTableInstance inst, String path) {
113    m_path = path;
114    m_pathWithSep = path + PATH_SEPARATOR;
115    m_inst = inst;
116  }
117
118  /**
119   * Gets the instance for the table.
120   *
121   * @return Instance
122   */
123  public NetworkTableInstance getInstance() {
124    return m_inst;
125  }
126
127  @Override
128  public String toString() {
129    return "NetworkTable: " + m_path;
130  }
131
132  private final ConcurrentMap<String, NetworkTableEntry> m_entries = new ConcurrentHashMap<>();
133
134  /**
135   * Gets the entry for a sub key.
136   *
137   * @param key the key name
138   * @return Network table entry.
139   */
140  public NetworkTableEntry getEntry(String key) {
141    NetworkTableEntry entry = m_entries.get(key);
142    if (entry == null) {
143      entry = m_inst.getEntry(m_pathWithSep + key);
144      m_entries.putIfAbsent(key, entry);
145    }
146    return entry;
147  }
148
149  /**
150   * Listen to keys only within this table.
151   *
152   * @param listener listener to add
153   * @param flags {@link EntryListenerFlags} bitmask
154   * @return Listener handle
155   */
156  public int addEntryListener(TableEntryListener listener, int flags) {
157    final int prefixLen = m_path.length() + 1;
158    return m_inst.addEntryListener(
159        m_pathWithSep,
160        event -> {
161          String relativeKey = event.name.substring(prefixLen);
162          if (relativeKey.indexOf(PATH_SEPARATOR) != -1) {
163            // part of a sub table
164            return;
165          }
166          listener.valueChanged(this, relativeKey, event.getEntry(), event.value, event.flags);
167        },
168        flags);
169  }
170
171  /**
172   * Listen to a single key.
173   *
174   * @param key the key name
175   * @param listener listener to add
176   * @param flags {@link EntryListenerFlags} bitmask
177   * @return Listener handle
178   */
179  public int addEntryListener(String key, TableEntryListener listener, int flags) {
180    final NetworkTableEntry entry = getEntry(key);
181    return m_inst.addEntryListener(
182        entry, event -> listener.valueChanged(this, key, entry, event.value, event.flags), flags);
183  }
184
185  /**
186   * Remove an entry listener.
187   *
188   * @param listener listener handle
189   */
190  public void removeEntryListener(int listener) {
191    m_inst.removeEntryListener(listener);
192  }
193
194  /**
195   * Listen for sub-table creation. This calls the listener once for each newly created sub-table.
196   * It immediately calls the listener for any existing sub-tables.
197   *
198   * @param listener listener to add
199   * @param localNotify notify local changes as well as remote
200   * @return Listener handle
201   */
202  public int addSubTableListener(TableListener listener, boolean localNotify) {
203    int flags = EntryListenerFlags.kNew | EntryListenerFlags.kImmediate;
204    if (localNotify) {
205      flags |= EntryListenerFlags.kLocal;
206    }
207
208    final int prefixLen = m_path.length() + 1;
209    final NetworkTable parent = this;
210
211    return m_inst.addEntryListener(
212        m_pathWithSep,
213        new Consumer<>() {
214          final Set<String> m_notifiedTables = new HashSet<>();
215
216          @Override
217          public void accept(EntryNotification event) {
218            String relativeKey = event.name.substring(prefixLen);
219            int endSubTable = relativeKey.indexOf(PATH_SEPARATOR);
220            if (endSubTable == -1) {
221              return;
222            }
223            String subTableKey = relativeKey.substring(0, endSubTable);
224            if (m_notifiedTables.contains(subTableKey)) {
225              return;
226            }
227            m_notifiedTables.add(subTableKey);
228            listener.tableCreated(parent, subTableKey, parent.getSubTable(subTableKey));
229          }
230        },
231        flags);
232  }
233
234  /**
235   * Remove a sub-table listener.
236   *
237   * @param listener listener handle
238   */
239  public void removeTableListener(int listener) {
240    m_inst.removeEntryListener(listener);
241  }
242
243  /**
244   * Returns the table at the specified key. If there is no table at the specified key, it will
245   * create a new table
246   *
247   * @param key the name of the table relative to this one
248   * @return a sub table relative to this one
249   */
250  public NetworkTable getSubTable(String key) {
251    return new NetworkTable(m_inst, m_pathWithSep + key);
252  }
253
254  /**
255   * Checks the table and tells if it contains the specified key.
256   *
257   * @param key the key to search for
258   * @return true if the table as a value assigned to the given key
259   */
260  public boolean containsKey(String key) {
261    return !("".equals(key)) && getEntry(key).exists();
262  }
263
264  /**
265   * Checks the table and tells if it contains the specified sub table.
266   *
267   * @param key the key to search for
268   * @return true if there is a subtable with the key which contains at least one key/subtable of
269   *     its own
270   */
271  public boolean containsSubTable(String key) {
272    int[] handles =
273        NetworkTablesJNI.getEntries(m_inst.getHandle(), m_pathWithSep + key + PATH_SEPARATOR, 0);
274    return handles.length != 0;
275  }
276
277  /**
278   * Gets all keys in the table (not including sub-tables).
279   *
280   * @param types bitmask of types; 0 is treated as a "don't care".
281   * @return keys currently in the table
282   */
283  public Set<String> getKeys(int types) {
284    Set<String> keys = new HashSet<>();
285    int prefixLen = m_path.length() + 1;
286    for (EntryInfo info : m_inst.getEntryInfo(m_pathWithSep, types)) {
287      String relativeKey = info.name.substring(prefixLen);
288      if (relativeKey.indexOf(PATH_SEPARATOR) != -1) {
289        continue;
290      }
291      keys.add(relativeKey);
292      // populate entries as we go
293      if (m_entries.get(relativeKey) == null) {
294        m_entries.putIfAbsent(relativeKey, new NetworkTableEntry(m_inst, info.entry));
295      }
296    }
297    return keys;
298  }
299
300  /**
301   * Gets all keys in the table (not including sub-tables).
302   *
303   * @return keys currently in the table
304   */
305  public Set<String> getKeys() {
306    return getKeys(0);
307  }
308
309  /**
310   * Gets the names of all subtables in the table.
311   *
312   * @return subtables currently in the table
313   */
314  public Set<String> getSubTables() {
315    Set<String> keys = new HashSet<>();
316    int prefixLen = m_path.length() + 1;
317    for (EntryInfo info : m_inst.getEntryInfo(m_pathWithSep, 0)) {
318      String relativeKey = info.name.substring(prefixLen);
319      int endSubTable = relativeKey.indexOf(PATH_SEPARATOR);
320      if (endSubTable == -1) {
321        continue;
322      }
323      keys.add(relativeKey.substring(0, endSubTable));
324    }
325    return keys;
326  }
327
328  /**
329   * Deletes the specified key in this table. The key can not be null.
330   *
331   * @param key the key name
332   */
333  public void delete(String key) {
334    getEntry(key).delete();
335  }
336
337  /**
338   * Put a value in the table.
339   *
340   * @param key the key to be assigned to
341   * @param value the value that will be assigned
342   * @return False if the table key already exists with a different type
343   */
344  boolean putValue(String key, NetworkTableValue value) {
345    return getEntry(key).setValue(value);
346  }
347
348  /**
349   * Gets the current value in the table, setting it if it does not exist.
350   *
351   * @param key the key
352   * @param defaultValue the default value to set if key doesn't exist.
353   * @return False if the table key exists with a different type
354   */
355  boolean setDefaultValue(String key, NetworkTableValue defaultValue) {
356    return getEntry(key).setDefaultValue(defaultValue);
357  }
358
359  /**
360   * Gets the value associated with a key as an object.
361   *
362   * @param key the key of the value to look up
363   * @return the value associated with the given key, or nullptr if the key does not exist
364   */
365  NetworkTableValue getValue(String key) {
366    return getEntry(key).getValue();
367  }
368
369  /**
370   * Get the path of the NetworkTable.
371   *
372   * @return The path of the NetworkTable.
373   */
374  public String getPath() {
375    return m_path;
376  }
377
378  /**
379   * Save table values to a file. The file format used is identical to that used for SavePersistent.
380   *
381   * @param filename filename
382   * @throws PersistentException if error saving file
383   */
384  public void saveEntries(String filename) throws PersistentException {
385    m_inst.saveEntries(filename, m_pathWithSep);
386  }
387
388  /**
389   * Load table values from a file. The file format used is identical to that used for
390   * SavePersistent / LoadPersistent.
391   *
392   * @param filename filename
393   * @return List of warnings (errors result in an exception instead)
394   * @throws PersistentException if error saving file
395   */
396  public String[] loadEntries(String filename) throws PersistentException {
397    return m_inst.loadEntries(filename, m_pathWithSep);
398  }
399
400  @Override
401  public boolean equals(Object other) {
402    if (other == this) {
403      return true;
404    }
405    if (!(other instanceof NetworkTable)) {
406      return false;
407    }
408    NetworkTable ntOther = (NetworkTable) other;
409    return m_inst.equals(ntOther.m_inst) && m_path.equals(ntOther.m_path);
410  }
411
412  @Override
413  public int hashCode() {
414    return Objects.hash(m_inst, m_path);
415  }
416}