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