public final class ConnectionInfo {
  /**
   * The remote identifier (as set on the remote node by
   * {@link NetworkTableInstance#setNetworkIdentity(String)}).
   */
  public final String remote_id;

  /**
   * The IP address of the remote node.
   */
  public final String remote_ip;

  /**
   * The port number of the remote node.
   */
  public final int remote_port;

  /**
   * The last time any update was received from the remote node (same scale as
   * returned by {@link NetworkTablesJNI#now()}).
   */
  public final long last_update;

  /**
   * The protocol version being used for this connection.  This is in protocol
   * layer format, so 0x0200 = 2.0, 0x0300 = 3.0).
   */
  public final int protocol_version;

  /** Constructor.
   * This should generally only be used internally to NetworkTables.
   * @param remote_id Remote identifier
   * @param remote_ip Remote IP address
   * @param remote_port Remote port number
   * @param last_update Last time an update was received
   * @param protocol_version The protocol version used for the connection
   */
  public ConnectionInfo(String remote_id, String remote_ip, int remote_port, long last_update, int protocol_version) {
    this.remote_id = remote_id;
    this.remote_ip = remote_ip;
    this.remote_port = remote_port;
    this.last_update = last_update;
    this.protocol_version = protocol_version;
  }
} public final class ConnectionNotification {
  /**
   *  Listener that was triggered.
   */
  public final int listener;

  /**
   * True if event is due to connection being established.
   */
  public final boolean connected;

  /**
   * Connection information.
   */
  public final ConnectionInfo conn;

  /** Constructor.
   * This should generally only be used internally to NetworkTables.
   * @param inst Instance
   * @param listener Listener that was triggered
   * @param connected Connected if true
   * @param conn Connection information
   */
  public ConnectionNotification(NetworkTableInstance inst, int listener, boolean connected, ConnectionInfo conn) {
    this.inst = inst;
    this.listener = listener;
    this.connected = connected;
    this.conn = conn;
  }

  private final NetworkTableInstance inst;

  public NetworkTableInstance getInstance() {
    return inst;
  }
} public final class EntryInfo {
  /** Entry handle. */
  public final int entry;

  /** Entry name. */
  public final String name;

  /** Entry type. */
  public final NetworkTableType type;

  /** Entry flags. */
  public final int flags;

  /** Timestamp of last change to entry (type or value). */
  public final long last_change;

  /** Constructor.
   * This should generally only be used internally to NetworkTables.
   * @param inst Instance
   * @param entry Entry handle
   * @param name Name
   * @param type Type (integer version of {@link NetworkTableType})
   * @param flags Flags
   * @param last_change Timestamp of last change
   */
  public EntryInfo(NetworkTableInstance inst, int entry, String name, int type, int flags, long last_change) {
    this.inst = inst;
    this.entry = entry;
    this.name = name;
    this.type = NetworkTableType.getFromInt(type);
    this.flags = flags;
    this.last_change = last_change;
  }

  /* Network table instance. */
  private final NetworkTableInstance inst;

  /* Cached entry object. */
  private NetworkTableEntry entryObject;

  /**
   * Get the entry as an object.
   * @return NetworkTableEntry for this entry.
   */
  NetworkTableEntry getEntry() {
    if (entryObject == null) {
      entryObject = new NetworkTableEntry(inst, entry);
    }
    return entryObject;
  }
} public final class EntryNotification {
  /**
   * Listener that was triggered.
   */
  public final int listener;

  /**
   * Entry handle.
   */
  public final int entry;

  /**
   * Entry name.
   */
  public final String name;

  /**
   * The new value.
   */
  public final NetworkTableValue value;

  /**
   * Update flags.  For example, {@link EntryListenerFlags#kNew} if the key did
   * not previously exist.
   */
  public final int flags;

  /** Constructor.
   * This should generally only be used internally to NetworkTables.
   * @param inst Instance
   * @param listener Listener that was triggered
   * @param entry Entry handle
   * @param name Entry name
   * @param value The new value
   * @param flags Update flags
   */
  public EntryNotification(NetworkTableInstance inst, int listener, int entry, String name, NetworkTableValue value, int flags) {
    this.inst = inst;
    this.listener = listener;
    this.entry = entry;
    this.name = name;
    this.value = value;
    this.flags = flags;
  }

  /* Network table instance. */
  private final NetworkTableInstance inst;

  /* Cached entry object. */
  NetworkTableEntry entryObject;

  /**
   * Get the entry as an object.
   * @return NetworkTableEntry for this entry.
   */
  public NetworkTableEntry getEntry() {
    if (entryObject == null) {
      entryObject = new NetworkTableEntry(inst, entry);
    }
    return entryObject;
  }
} public final class NetworkTableEntry {
  /**
   * Flag values (as returned by {@link #getFlags()}).
   */
  public static final int kPersistent = 0x01;

  /**
   * Construct from native handle.
   * @param inst Instance
   * @param handle Native handle
   */
  public NetworkTableEntry(NetworkTableInstance inst, int handle) {
    m_inst = inst;
    m_handle = handle;
  }

  /**
   * Determines if the native handle is valid.
   * @return True if the native handle is valid, false otherwise.
   */
  public boolean isValid() {
    return m_handle != 0;
  }

  /**
   * Gets the native handle for the entry.
   * @return Native handle
   */
  public int getHandle() {
    return m_handle;
  }

  /**
   * Gets the instance for the entry.
   * @return Instance
   */
  public NetworkTableInstance getInstance() {
    return m_inst;
  }

  /**
   * Determines if the entry currently exists.
   * @return True if the entry exists, false otherwise.
   */
  public boolean exists() {
    return NetworkTablesJNI.getType(m_handle) != 0;
  }

  /**
   * Gets the name of the entry (the key).
   * @return the entry's name
   */
  public String getName() {
    return NetworkTablesJNI.getEntryName(m_handle);
  }

  /**
   * Gets the type of the entry.
   * @return the entry's type
   */
  public NetworkTableType getType() {
    return NetworkTableType.getFromInt(NetworkTablesJNI.getType(m_handle));
  }

  /**
   * Returns the flags.
   * @return the flags (bitmask)
   */
  public int getFlags() {
    return NetworkTablesJNI.getEntryFlags(m_handle);
  }

  /**
   * Gets the last time the entry's value was changed.
   * @return Entry last change time
   */
  public long getLastChange() {
    return NetworkTablesJNI.getEntryLastChange(m_handle);
  }

  /**
   * Gets combined information about the entry.
   * @return Entry information
   */
  public EntryInfo getInfo() {
    return NetworkTablesJNI.getEntryInfoHandle(m_inst, m_handle);
  }

  /**
   * Gets the entry's value.
   * Returns a value with type NetworkTableType.kUnassigned if the value
   * does not exist.
   * @return the entry's value
   */
  public NetworkTableValue getValue() {
    return NetworkTablesJNI.getValue(m_handle);
  }

  /**
   * Gets the entry's value as a boolean. If the entry does not exist or is of
   * different type, it will return the default value.
   * @param defaultValue the value to be returned if no value is found
   * @return the entry's value or the given default value
   */
  public boolean getBoolean(boolean defaultValue) {
    return NetworkTablesJNI.getBoolean(m_handle, defaultValue);
  }

  /**
   * Gets the entry's value as a double. If the entry does not exist or is of
   * different type, it will return the default value.
   * @param defaultValue the value to be returned if no value is found
   * @return the entry's value or the given default value
   */
  public double getDouble(double defaultValue) {
    return NetworkTablesJNI.getDouble(m_handle, defaultValue);
  }

  /**
   * Gets the entry's value as a double. If the entry does not exist or is of
   * different type, it will return the default value.
   * @param defaultValue the value to be returned if no value is found
   * @return the entry's value or the given default value
   */
  public Number getNumber(Number defaultValue) {
    return NetworkTablesJNI.getDouble(m_handle, defaultValue.doubleValue());
  }

  /**
   * Gets the entry's value as a string. If the entry does not exist or is of
   * different type, it will return the default value.
   * @param defaultValue the value to be returned if no value is found
   * @return the entry's value or the given default value
   */
  public String getString(String defaultValue) {
    return NetworkTablesJNI.getString(m_handle, defaultValue);
  }

  /**
   * Gets the entry's value as a raw value (byte array). If the entry does not
   * exist or is of different type, it will return the default value.
   * @param defaultValue the value to be returned if no value is found
   * @return the entry's value or the given default value
   */
  public byte[] getRaw(byte[] defaultValue) {
    return NetworkTablesJNI.getRaw(m_handle, defaultValue);
  }

  /**
   * Gets the entry's value as a boolean array. If the entry does not exist
   * or is of different type, it will return the default value.
   * @param defaultValue the value to be returned if no value is found
   * @return the entry's value or the given default value
   */
  public boolean[] getBooleanArray(boolean[] defaultValue) {
    return NetworkTablesJNI.getBooleanArray(m_handle, defaultValue);
  }

  /**
   * Gets the entry's value as a boolean array. If the entry does not exist
   * or is of different type, it will return the default value.
   * @param defaultValue the value to be returned if no value is found
   * @return the entry's value or the given default value
   */
  public Boolean[] getBooleanArray(Boolean[] defaultValue) {
    return NetworkTableValue.fromNative(NetworkTablesJNI.getBooleanArray(m_handle, NetworkTableValue.toNative(defaultValue)));
  }

  /**
   * Gets the entry's value as a double array. If the entry does not exist
   * or is of different type, it will return the default value.
   * @param defaultValue the value to be returned if no value is found
   * @return the entry's value or the given default value
   */
  public double[] getDoubleArray(double[] defaultValue) {
    return NetworkTablesJNI.getDoubleArray(m_handle, defaultValue);
  }

  /**
   * Gets the entry's value as a double array. If the entry does not exist
   * or is of different type, it will return the default value.
   * @param defaultValue the value to be returned if no value is found
   * @return the entry's value or the given default value
   */
  public Double[] getDoubleArray(Double[] defaultValue) {
    return NetworkTableValue.fromNative(NetworkTablesJNI.getDoubleArray(m_handle, NetworkTableValue.toNative(defaultValue)));
  }

  /**
   * Gets the entry's value as a double array. If the entry does not exist
   * or is of different type, it will return the default value.
   * @param defaultValue the value to be returned if no value is found
   * @return the entry's value or the given default value
   */
  public Number[] getNumberArray(Number[] defaultValue) {
    return NetworkTableValue.fromNative(NetworkTablesJNI.getDoubleArray(m_handle, NetworkTableValue.toNative(defaultValue)));
  }

  /**
   * Gets the entry's value as a string array. If the entry does not exist
   * or is of different type, it will return the default value.
   * @param defaultValue the value to be returned if no value is found
   * @return the entry's value or the given default value
   */
  public String[] getStringArray(String[] defaultValue) {
    return NetworkTablesJNI.getStringArray(m_handle, defaultValue);
  }

  /**
   * Sets the entry's value if it does not exist.
   * @param defaultValue the default value to set
   * @return False if the entry exists with a different type
   * @throws IllegalArgumentException if the value is not a known type
   */
  public boolean setDefaultValue(Object defaultValue) {
    if (defaultValue instanceof NetworkTableValue) {
      long time = ((NetworkTableValue)defaultValue).getTime();
      Object o = ((NetworkTableValue)defaultValue).getValue();
      switch (((NetworkTableValue)defaultValue).getType()) {
        case kBoolean:
          return NetworkTablesJNI.setDefaultBoolean(m_handle, time, ((Boolean)o).booleanValue());
        case kDouble:
          return NetworkTablesJNI.setDefaultDouble(m_handle, time, ((Number)o).doubleValue());
        case kString:
          return NetworkTablesJNI.setDefaultString(m_handle, time, (String)o);
        case kRaw:
          return NetworkTablesJNI.setDefaultRaw(m_handle, time, (byte[])o);
        case kBooleanArray:
          return NetworkTablesJNI.setDefaultBooleanArray(m_handle, time, (boolean[])o);
        case kDoubleArray:
          return NetworkTablesJNI.setDefaultDoubleArray(m_handle, time, (double[])o);
        case kStringArray:
          return NetworkTablesJNI.setDefaultStringArray(m_handle, time, (String[])o);
        case kRpc:
          // TODO
        default:
          return true;
      }
    } else if (defaultValue instanceof Boolean) {
      return setDefaultBoolean((Boolean)defaultValue);
    } else if (defaultValue instanceof Number) {
      return setDefaultNumber((Number)defaultValue);
    } else if (defaultValue instanceof String) {
      return setDefaultString((String)defaultValue);
    } else if (defaultValue instanceof byte[]) {
      return setDefaultRaw((byte[])defaultValue);
    } else if (defaultValue instanceof boolean[]) {
      return setDefaultBooleanArray((boolean[])defaultValue);
    } else if (defaultValue instanceof double[]) {
      return setDefaultDoubleArray((double[])defaultValue);
    } else if (defaultValue instanceof Boolean[]) {
      return setDefaultBooleanArray((Boolean[])defaultValue);
    } else if (defaultValue instanceof Number[]) {
      return setDefaultNumberArray((Number[])defaultValue);
    } else if (defaultValue instanceof String[]) {
      return setDefaultStringArray((String[])defaultValue);
    } else {
      throw new IllegalArgumentException("Value of type " + defaultValue.getClass().getName()
        + " cannot be put into a table");
    }
  }

  /**
   * Sets the entry's value if it does not exist.
   * @param defaultValue the default value to set
   * @return False if the entry exists with a different type
   */
  public boolean setDefaultBoolean(boolean defaultValue) {
    return NetworkTablesJNI.setDefaultBoolean(m_handle, 0, defaultValue);
  }

  /**
   * Sets the entry's value if it does not exist.
   * @param defaultValue the default value to set
   * @return False if the entry exists with a different type
   */
  public boolean setDefaultDouble(double defaultValue) {
    return NetworkTablesJNI.setDefaultDouble(m_handle, 0, defaultValue);
  }

  /**
   * Sets the entry's value if it does not exist.
   * @param defaultValue the default value to set
   * @return False if the entry exists with a different type
   */
  public boolean setDefaultNumber(Number defaultValue) {
    return NetworkTablesJNI.setDefaultDouble(m_handle, 0, defaultValue.doubleValue());
  }

  /**
   * Sets the entry's value if it does not exist.
   * @param defaultValue the default value to set
   * @return False if the entry exists with a different type
   */
  public boolean setDefaultString(String defaultValue) {
    return NetworkTablesJNI.setDefaultString(m_handle, 0, defaultValue);
  }

  /**
   * Sets the entry's value if it does not exist.
   * @param defaultValue the default value to set
   * @return False if the entry exists with a different type
   */
  public boolean setDefaultRaw(byte[] defaultValue) {
    return NetworkTablesJNI.setDefaultRaw(m_handle, 0, defaultValue);
  }

  /**
   * Sets the entry's value if it does not exist.
   * @param defaultValue the default value to set
   * @return False if the entry exists with a different type
   */
  public boolean setDefaultBooleanArray(boolean[] defaultValue) {
    return NetworkTablesJNI.setDefaultBooleanArray(m_handle, 0, defaultValue);
  }

  /**
   * Sets the entry's value if it does not exist.
   * @param defaultValue the default value to set
   * @return False if the entry exists with a different type
   */
  public boolean setDefaultBooleanArray(Boolean[] defaultValue) {
    return NetworkTablesJNI.setDefaultBooleanArray(m_handle, 0, NetworkTableValue.toNative(defaultValue));
  }

  /**
   * Sets the entry's value if it does not exist.
   * @param defaultValue the default value to set
   * @return False if the entry exists with a different type
   */
  public boolean setDefaultDoubleArray(double[] defaultValue) {
    return NetworkTablesJNI.setDefaultDoubleArray(m_handle, 0, defaultValue);
  }

  /**
   * Sets the entry's value if it does not exist.
   * @param defaultValue the default value to set
   * @return False if the entry exists with a different type
   */
  public boolean setDefaultNumberArray(Number[] defaultValue) {
    return NetworkTablesJNI.setDefaultDoubleArray(m_handle, 0, NetworkTableValue.toNative(defaultValue));
  }

  /**
   * Sets the entry's value if it does not exist.
   * @param defaultValue the default value to set
   * @return False if the entry exists with a different type
   */
  public boolean setDefaultStringArray(String[] defaultValue) {
    return NetworkTablesJNI.setDefaultStringArray(m_handle, 0, defaultValue);
  }

  /**
   * Sets the entry's value
   * @param value the value that will be assigned
   * @return False if the table key already exists with a different type
   * @throws IllegalArgumentException if the value is not a known type
   */
  public boolean setValue(Object value) {
    if (value instanceof NetworkTableValue) {
      long time = ((NetworkTableValue)value).getTime();
      Object o = ((NetworkTableValue)value).getValue();
      switch (((NetworkTableValue)value).getType()) {
        case kBoolean:
          return NetworkTablesJNI.setBoolean(m_handle, time, ((Boolean)o).booleanValue(), false);
        case kDouble:
          return NetworkTablesJNI.setDouble(m_handle, time, ((Number)o).doubleValue(), false);
        case kString:
          return NetworkTablesJNI.setString(m_handle, time, (String)o, false);
        case kRaw:
          return NetworkTablesJNI.setRaw(m_handle, time, (byte[])o, false);
        case kBooleanArray:
          return NetworkTablesJNI.setBooleanArray(m_handle, time, (boolean[])o, false);
        case kDoubleArray:
          return NetworkTablesJNI.setDoubleArray(m_handle, time, (double[])o, false);
        case kStringArray:
          return NetworkTablesJNI.setStringArray(m_handle, time, (String[])o, false);
        case kRpc:
          // TODO
        default:
          return true;
      }
    } else if (value instanceof Boolean) {
      return setBoolean((Boolean)value);
    } else if (value instanceof Number) {
      return setNumber((Number)value);
    } else if (value instanceof String) {
      return setString((String)value);
    } else if (value instanceof byte[]) {
      return setRaw((byte[])value);
    } else if (value instanceof boolean[]) {
      return setBooleanArray((boolean[])value);
    } else if (value instanceof double[]) {
      return setDoubleArray((double[])value);
    } else if (value instanceof Boolean[]) {
      return setBooleanArray((Boolean[])value);
    } else if (value instanceof Number[]) {
      return setNumberArray((Number[])value);
    } else if (value instanceof String[]) {
      return setStringArray((String[])value);
    } else {
      throw new IllegalArgumentException("Value of type " + value.getClass().getName()
        + " cannot be put into a table");
    }
  }

  /**
   * Sets the entry's value.
   * @param value the value to set
   * @return False if the entry exists with a different type
   */
  public boolean setBoolean(boolean value) {
    return NetworkTablesJNI.setBoolean(m_handle, 0, value, false);
  }

  /**
   * Sets the entry's value.
   * @param value the value to set
   * @return False if the entry exists with a different type
   */
  public boolean setDouble(double value) {
    return NetworkTablesJNI.setDouble(m_handle, 0, value, false);
  }

  /**
   * Sets the entry's value.
   * @param value the value to set
   * @return False if the entry exists with a different type
   */
  public boolean setNumber(Number value) {
    return NetworkTablesJNI.setDouble(m_handle, 0, value.doubleValue(), false);
  }

  /**
   * Sets the entry's value.
   * @param value the value to set
   * @return False if the entry exists with a different type
   */
  public boolean setString(String value) {
    return NetworkTablesJNI.setString(m_handle, 0, value, false);
  }

  /**
   * Sets the entry's value.
   * @param value the value to set
   * @return False if the entry exists with a different type
   */
  public boolean setRaw(byte[] value) {
    return NetworkTablesJNI.setRaw(m_handle, 0, value, false);
  }

  /**
   * Sets the entry's value.
   * @param value the value to set
   * @param len the length of the value
   * @return False if the entry exists with a different type
   */
  public boolean setRaw(ByteBuffer value, int len) {
    if (!value.isDirect())
      throw new IllegalArgumentException("must be a direct buffer");
    if (value.capacity() < len)
      throw new IllegalArgumentException("buffer is too small, must be at least " + len);
    return NetworkTablesJNI.setRaw(m_handle, 0, value, len, false);
  }

  /**
   * Sets the entry's value.
   * @param value the value to set
   * @return False if the entry exists with a different type
   */
  public boolean setBooleanArray(boolean[] value) {
    return NetworkTablesJNI.setBooleanArray(m_handle, 0, value, false);
  }

  /**
   * Sets the entry's value.
   * @param value the value to set
   * @return False if the entry exists with a different type
   */
  public boolean setBooleanArray(Boolean[] value) {
    return NetworkTablesJNI.setBooleanArray(m_handle, 0, NetworkTableValue.toNative(value), false);
  }

  /**
   * Sets the entry's value.
   * @param value the value to set
   * @return False if the entry exists with a different type
   */
  public boolean setDoubleArray(double[] value) {
    return NetworkTablesJNI.setDoubleArray(m_handle, 0, value, false);
  }

  /**
   * Sets the entry's value.
   * @param value the value to set
   * @return False if the entry exists with a different type
   */
  public boolean setNumberArray(Number[] value) {
    return NetworkTablesJNI.setDoubleArray(m_handle, 0, NetworkTableValue.toNative(value), false);
  }

  /**
   * Sets the entry's value.
   * @param value the value to set
   * @return False if the entry exists with a different type
   */
  public boolean setStringArray(String[] value) {
    return NetworkTablesJNI.setStringArray(m_handle, 0, value, false);
  }

  /**
   * Sets the entry's value.  If the value is of different type, the type is
   * changed to match the new value.
   * @param value the value to set
   * @throws IllegalArgumentException if the value is not a known type
   */
  public void forceSetValue(Object value) {
    if (value instanceof NetworkTableValue) {
      long time = ((NetworkTableValue)value).getTime();
      Object o = ((NetworkTableValue)value).getValue();
      switch (((NetworkTableValue)value).getType()) {
        case kBoolean:
          NetworkTablesJNI.setBoolean(m_handle, time, ((Boolean)o).booleanValue(), true);
          return;
        case kDouble:
          NetworkTablesJNI.setDouble(m_handle, time, ((Number)o).doubleValue(), true);
          return;
        case kString:
          NetworkTablesJNI.setString(m_handle, time, (String)o, true);
          return;
        case kRaw:
          NetworkTablesJNI.setRaw(m_handle, time, (byte[])o, true);
          return;
        case kBooleanArray:
          NetworkTablesJNI.setBooleanArray(m_handle, time, (boolean[])o, true);
          return;
        case kDoubleArray:
          NetworkTablesJNI.setDoubleArray(m_handle, time, (double[])o, true);
          return;
        case kStringArray:
          NetworkTablesJNI.setStringArray(m_handle, time, (String[])o, true);
          return;
        case kRpc:
          // TODO
        default:
          return;
      }
    } else if (value instanceof Boolean) {
      forceSetBoolean((Boolean)value);
    } else if (value instanceof Number) {
      forceSetNumber((Number)value);
    } else if (value instanceof String) {
      forceSetString((String)value);
    } else if (value instanceof byte[]) {
      forceSetRaw((byte[])value);
    } else if (value instanceof boolean[]) {
      forceSetBooleanArray((boolean[])value);
    } else if (value instanceof double[]) {
      forceSetDoubleArray((double[])value);
    } else if (value instanceof Boolean[]) {
      forceSetBooleanArray((Boolean[])value);
    } else if (value instanceof Number[]) {
      forceSetNumberArray((Number[])value);
    } else if (value instanceof String[]) {
      forceSetStringArray((String[])value);
    } else {
      throw new IllegalArgumentException("Value of type " + value.getClass().getName()
        + " cannot be put into a table");
    }
  }

  /**
   * Sets the entry's value.  If the value is of different type, the type is
   * changed to match the new value.
   * @param value the value to set
   */
  public void forceSetBoolean(boolean value) {
    NetworkTablesJNI.setBoolean(m_handle, 0, value, true);
  }

  /**
   * Sets the entry's value.  If the value is of different type, the type is
   * changed to match the new value.
   * @param value the value to set
   */
  public void forceSetDouble(double value) {
    NetworkTablesJNI.setDouble(m_handle, 0, value, true);
  }

  /**
   * Sets the entry's value.  If the value is of different type, the type is
   * changed to match the new value.
   * @param value the value to set
   */
  public void forceSetNumber(Number value) {
    NetworkTablesJNI.setDouble(m_handle, 0, value.doubleValue(), true);
  }

  /**
   * Sets the entry's value.  If the value is of different type, the type is
   * changed to match the new value.
   * @param value the value to set
   */
  public void forceSetString(String value) {
    NetworkTablesJNI.setString(m_handle, 0, value, true);
  }

  /**
   * Sets the entry's value.  If the value is of different type, the type is
   * changed to match the new value.
   * @param value the value to set
   */
  public void forceSetRaw(byte[] value) {
    NetworkTablesJNI.setRaw(m_handle, 0, value, true);
  }

  /**
   * Sets the entry's value.  If the value is of different type, the type is
   * changed to match the new value.
   * @param value the value to set
   */
  public void forceSetBooleanArray(boolean[] value) {
    NetworkTablesJNI.setBooleanArray(m_handle, 0, value, true);
  }

  /**
   * Sets the entry's value.  If the value is of different type, the type is
   * changed to match the new value.
   * @param value the value to set
   */
  public void forceSetBooleanArray(Boolean[] value) {
    NetworkTablesJNI.setBooleanArray(m_handle, 0, NetworkTableValue.toNative(value), true);
  }

  /**
   * Sets the entry's value.  If the value is of different type, the type is
   * changed to match the new value.
   * @param value the value to set
   */
  public void forceSetDoubleArray(double[] value) {
    NetworkTablesJNI.setDoubleArray(m_handle, 0, value, true);
  }

  /**
   * Sets the entry's value.  If the value is of different type, the type is
   * changed to match the new value.
   * @param value the value to set
   */
  public void forceSetNumberArray(Number[] value) {
    NetworkTablesJNI.setDoubleArray(m_handle, 0, NetworkTableValue.toNative(value), true);
  }

  /**
   * Sets the entry's value.  If the value is of different type, the type is
   * changed to match the new value.
   * @param value the value to set
   */
  public void forceSetStringArray(String[] value) {
    NetworkTablesJNI.setStringArray(m_handle, 0, value, true);
  }

  /**
   * Sets flags.
   * @param flags the flags to set (bitmask)
   */
  public void setFlags(int flags) {
    NetworkTablesJNI.setEntryFlags(m_handle, getFlags() | flags);
  }

  /**
   * Clears flags.
   * @param flags the flags to clear (bitmask)
   */
  public void clearFlags(int flags) {
    NetworkTablesJNI.setEntryFlags(m_handle, getFlags() & ~flags);
  }

  /**
   * Make value persistent through program restarts.
   */
  public void setPersistent() {
    setFlags(kPersistent);
  }

  /**
   * Stop making value persistent through program restarts.
   */
  public void clearPersistent() {
    clearFlags(kPersistent);
  }

  /**
   * Returns whether the value is persistent through program restarts.
   * @return True if the value is persistent.
   */
  public boolean isPersistent() {
    return (getFlags() & kPersistent) != 0;
  }

  /**
   * Deletes the entry.
   */
  public void delete() {
    NetworkTablesJNI.deleteEntry(m_handle);
  }

  /**
   * Create a callback-based RPC entry point.  Only valid to use on the server.
   * The callback function will be called when the RPC is called.
   * This function creates RPC version 0 definitions (raw data in and out).
   * @param callback  callback function
   */
  void createRpc(Consumer<RpcAnswer> callback) {
    m_inst.createRpc(this, callback);
  }

  /**
   * Call a RPC function.  May be used on either the client or server.
   * This function is non-blocking.  Either {@link RpcCall#GetResult()} or
   * {@link RpcCall#CancelResult()} must be called on the return value to either
   * get or ignore the result of the call.
   * @param params      parameter
   * @return RPC call object.
   */
  RpcCall callRpc(byte[] params) {
    return new RpcCall(this, NetworkTablesJNI.callRpc(m_handle, params));
  }

  /**
   * Add a listener for changes to the entry
   * @param listener the listener to add
   * @param flags bitmask specifying desired notifications
   * @return listener handle
   */
  public int addListener(Consumer<EntryNotification> listener, int flags) {
    return m_inst.addEntryListener(this, listener, flags);
  }

  /**
   * Remove a listener from receiving entry events
   * @param listener the listener to be removed
   */
  public void removeListener(int listener) {
    m_inst.removeEntryListener(listener);
  }

  @Override
  public boolean equals(Object o) {
    if (o == this) {
      return true;
    }
    if (!(o instanceof NetworkTableEntry)) {
      return false;
    }
    NetworkTableEntry other = (NetworkTableEntry) o;
    return m_handle == other.m_handle;
  }

  @Override
  public int hashCode() {
    return m_handle;
  }

  private NetworkTableInstance m_inst;
  private int m_handle;
} public final class NetworkTableInstance {
  /**
   * Client/server mode flag values (as returned by {@link #getNetworkMode()}).
   * This is a bitmask.
   */
  public static final int kNetModeNone = 0x00;
  public static final int kNetModeServer = 0x01;
  public static final int kNetModeClient = 0x02;
  public static final int kNetModeStarting = 0x04;
  public static final int kNetModeFailure = 0x08;

  /**
   * The default port that network tables operates on.
   */
  public static final int kDefaultPort = 1735;

  /**
   * Construct from native handle.
   * @param handle Native handle
   */
  private NetworkTableInstance(int handle) {
    m_owned = false;
    m_handle = handle;
  }

  /**
   * Destroys the instance (if created by {@link #create()}).
   */
  public synchronized void free() {
    if (m_owned && m_handle != 0) {
      NetworkTablesJNI.destroyInstance(m_handle);
    }
  }

  /**
   * Determines if the native handle is valid.
   * @return True if the native handle is valid, false otherwise.
   */
  public boolean isValid() {
    return m_handle != 0;
  }

  /* The default instance. */
  private static NetworkTableInstance s_defaultInstance;

  /**
   * Get global default instance.
   * @return Global default instance
   */
  public synchronized static NetworkTableInstance getDefault() {
    if (s_defaultInstance == null) {
      s_defaultInstance = new NetworkTableInstance(NetworkTablesJNI.getDefaultInstance());
    }
    return s_defaultInstance;
  }

  /**
   * Create an instance.
   * Note: A reference to the returned instance must be retained to ensure the
   * instance is not garbage collected.
   * @return Newly created instance
   */
  public static NetworkTableInstance create() {
    NetworkTableInstance inst = new NetworkTableInstance(NetworkTablesJNI.createInstance());
    inst.m_owned = true;
    return inst;
  }

  /**
   * Gets the native handle for the entry.
   * @return Native handle
   */
  public int getHandle() {
    return m_handle;
  }

  /**
   * Gets the entry for a key.
   * @param name Key
   * @return Network table entry.
   */
  public NetworkTableEntry getEntry(String name) {
    return new NetworkTableEntry(this, NetworkTablesJNI.getEntry(m_handle, name));
  }

  /**
   * Get entries starting with the given prefix.
   * The results are optionally filtered by string prefix and entry type to
   * only return a subset of all entries.
   *
   * @param prefix entry name required prefix; only entries whose name
   * starts with this string are returned
   * @param types bitmask of types; 0 is treated as a "don't care"
   * @return Array of entries.
   */
  public NetworkTableEntry[] getEntries(String prefix, int types) {
    int[] handles = NetworkTablesJNI.getEntries(m_handle, prefix, types);
    NetworkTableEntry[] entries = new NetworkTableEntry[handles.length];
    for (int i = 0; i < handles.length; i++) {
      entries[i] = new NetworkTableEntry(this, handles[i]);
    }
    return entries;
  }

  /**
   * Get information about entries starting with the given prefix.
   * The results are optionally filtered by string prefix and entry type to
   * only return a subset of all entries.
   *
   * @param prefix entry name required prefix; only entries whose name
   * starts with this string are returned
   * @param types bitmask of types; 0 is treated as a "don't care"
   * @return Array of entry information.
   */
  public EntryInfo[] getEntryInfo(String prefix, int types) {
    return NetworkTablesJNI.getEntryInfo(this, m_handle, prefix, types);
  }

  /* Cache of created tables. */
  private final ConcurrentMap<String, NetworkTable> m_tables = new ConcurrentHashMap<>();

  /**
   * Gets the table with the specified key.
   *
   * @param key the key name
   * @return The network table
   */
  public NetworkTable getTable(String key) {
    // prepend leading / if not present
    String theKey;
    if (key.isEmpty() || key.equals("/")) {
      theKey = "";
    } else if (key.charAt(0) == NetworkTable.PATH_SEPARATOR) {
      theKey = key;
    } else {
      theKey = NetworkTable.PATH_SEPARATOR + key;
    }

    // cache created tables
    NetworkTable table = m_tables.get(theKey);
    if (table == null) {
      table = new NetworkTable(this, theKey);
      NetworkTable oldTable = m_tables.putIfAbsent(theKey, table);
      if (oldTable != null) {
        table = oldTable;
      }
    }
    return table;
  }

  /**
   * Deletes ALL keys in ALL subtables (except persistent values).
   * Use with caution!
   */
  public void deleteAllEntries() {
    NetworkTablesJNI.deleteAllEntries(m_handle);
  }

  /*
   * Callback Creation Functions
   */

  private static class EntryConsumer<T> {
    final NetworkTableEntry entry;
    final Consumer<T> consumer;

    EntryConsumer(NetworkTableEntry entry, Consumer<T> consumer) {
      this.entry = entry;
      this.consumer = consumer;
    }
  }

  private final ReentrantLock m_entryListenerLock = new ReentrantLock();
  private final Map<Integer, EntryConsumer<EntryNotification>> m_entryListeners = new HashMap<>();
  private Thread m_entryListenerThread;
  private int m_entryListenerPoller;
  private boolean m_entryListenerWaitQueue;
  private final Condition m_entryListenerWaitQueueCond = m_entryListenerLock.newCondition();

  private void startEntryListenerThread() {
    m_entryListenerThread = new Thread(() -> {
      boolean wasInterrupted = false;
      while (!Thread.interrupted()) {
        EntryNotification[] events;
        try {
          events = NetworkTablesJNI.pollEntryListener(this, m_entryListenerPoller);
        } catch (InterruptedException ex) {
          m_entryListenerLock.lock();
          try {
            if (m_entryListenerWaitQueue) {
              m_entryListenerWaitQueue = false;
              m_entryListenerWaitQueueCond.signalAll();
              continue;
            }
          } finally {
            m_entryListenerLock.unlock();
          }
          Thread.currentThread().interrupt();
          wasInterrupted = true; // don't try to destroy poller, as its handle is likely no longer valid
          break;
        }
        for (EntryNotification event : events) {
          EntryConsumer<EntryNotification> listener;
          m_entryListenerLock.lock();
          try {
            listener = m_entryListeners.get(event.listener);
          } finally {
            m_entryListenerLock.unlock();
          }
          if (listener != null) {
            event.entryObject = listener.entry;
            try {
              listener.consumer.accept(event);
            } catch (Throwable throwable) {
              System.err.println("Unhandled exception during entry listener callback: " + throwable.toString());
              throwable.printStackTrace();
            }
          }
        }
      }
      m_entryListenerLock.lock();
      try {
        if (!wasInterrupted) {
          NetworkTablesJNI.destroyEntryListenerPoller(m_entryListenerPoller);
        }
        m_entryListenerPoller = 0;
      } finally {
        m_entryListenerLock.unlock();
      }
    }, "NTEntryListener");
    m_entryListenerThread.setDaemon(true);
    m_entryListenerThread.start();
  }

  /**
   * Add a listener for all entries starting with a certain prefix.
   *
   * @param prefix            UTF-8 string prefix
   * @param listener          listener to add
   * @param flags             {@link EntryListenerFlags} bitmask
   * @return Listener handle
   */
  public int addEntryListener(String prefix, Consumer<EntryNotification> listener, int flags) {
    m_entryListenerLock.lock();
    try {
      if (m_entryListenerPoller == 0) {
        m_entryListenerPoller = NetworkTablesJNI.createEntryListenerPoller(m_handle);
        startEntryListenerThread();
      }
      int handle = NetworkTablesJNI.addPolledEntryListener(m_entryListenerPoller, prefix, flags);
      m_entryListeners.put(handle, new EntryConsumer<>(null, listener));
      return handle;
    } finally {
      m_entryListenerLock.unlock();
    }
  }

  /**
   * Add a listener for a particular entry.
   *
   * @param entry             the entry
   * @param listener          listener to add
   * @param flags             {@link EntryListenerFlags} bitmask
   * @return Listener handle
   */
  public int addEntryListener(NetworkTableEntry entry, Consumer<EntryNotification> listener, int flags) {
    if (!equals(entry.getInstance())) {
      throw new IllegalArgumentException("entry does not belong to this instance");
    }
    m_entryListenerLock.lock();
    try {
      if (m_entryListenerPoller == 0) {
        m_entryListenerPoller = NetworkTablesJNI.createEntryListenerPoller(m_handle);
        startEntryListenerThread();
      }
      int handle = NetworkTablesJNI.addPolledEntryListener(m_entryListenerPoller, entry.getHandle(), flags);
      m_entryListeners.put(handle, new EntryConsumer<>(entry, listener));
      return handle;
    } finally {
      m_entryListenerLock.unlock();
    }
  }

  /**
   * Remove an entry listener.
   * @param listener Listener handle to remove
   */
  public void removeEntryListener(int listener) {
    NetworkTablesJNI.removeEntryListener(listener);
  }

  /**
   * Wait for the entry listener queue to be empty.  This is primarily useful
   * for deterministic testing.  This blocks until either the entry listener
   * queue is empty (e.g. there are no more events that need to be passed along
   * to callbacks or poll queues) or the timeout expires.
   * @param timeout   timeout, in seconds.  Set to 0 for non-blocking behavior,
   *                  or a negative value to block indefinitely
   * @return False if timed out, otherwise true.
  */
  public boolean waitForEntryListenerQueue(double timeout) {
    if (!NetworkTablesJNI.waitForEntryListenerQueue(m_handle, timeout)) {
      return false;
    }
    m_entryListenerLock.lock();
    try {
      if (m_entryListenerPoller != 0) {
        m_entryListenerWaitQueue = true;
        NetworkTablesJNI.cancelPollEntryListener(m_entryListenerPoller);
        while (m_entryListenerWaitQueue) {
          try {
            if (timeout < 0) {
              m_entryListenerWaitQueueCond.await();
            } else {
              return m_entryListenerWaitQueueCond.await((long) (timeout * 1e9), TimeUnit.NANOSECONDS);
            }
          } catch (InterruptedException ex) {
            Thread.currentThread().interrupt();
            return true;
          }
        }
      }
    } finally {
      m_entryListenerLock.unlock();
    }
    return true;
  }

  private final ReentrantLock m_connectionListenerLock = new ReentrantLock();
  private final Map<Integer, Consumer<ConnectionNotification>> m_connectionListeners = new HashMap<>();
  private Thread m_connectionListenerThread;
  private int m_connectionListenerPoller;
  private boolean m_connectionListenerWaitQueue;
  private final Condition m_connectionListenerWaitQueueCond = m_connectionListenerLock.newCondition();

  private void startConnectionListenerThread() {
    m_connectionListenerThread = new Thread(() -> {
      boolean wasInterrupted = false;
      while (!Thread.interrupted()) {
        ConnectionNotification[] events;
        try {
          events = NetworkTablesJNI.pollConnectionListener(this, m_connectionListenerPoller);
        } catch (InterruptedException ex) {
          m_connectionListenerLock.lock();
          try {
            if (m_connectionListenerWaitQueue) {
              m_connectionListenerWaitQueue = false;
              m_connectionListenerWaitQueueCond.signalAll();
              continue;
            }
          } finally {
            m_connectionListenerLock.unlock();
          }
          Thread.currentThread().interrupt();
          wasInterrupted = true; // don't try to destroy poller, as its handle is likely no longer valid
          break;
        }
        for (ConnectionNotification event : events) {
          Consumer<ConnectionNotification> listener;
          m_connectionListenerLock.lock();
          try {
            listener = m_connectionListeners.get(event.listener);
          } finally {
            m_connectionListenerLock.unlock();
          }
          if (listener != null) {
            try {
              listener.accept(event);
            } catch (Throwable throwable) {
              System.err.println("Unhandled exception during connection listener callback: " + throwable.toString());
              throwable.printStackTrace();
            }
          }
        }
      }
      m_connectionListenerLock.lock();
      try {
        if (!wasInterrupted) {
          NetworkTablesJNI.destroyConnectionListenerPoller(m_connectionListenerPoller);
        }
        m_connectionListenerPoller = 0;
      } finally {
        m_connectionListenerLock.unlock();
      }
    }, "NTConnectionListener");
    m_connectionListenerThread.setDaemon(true);
    m_connectionListenerThread.start();
  }

  /**
   * Add a connection listener.
   *
   * @param listener Listener to add
   * @param immediateNotify Notify listener of all existing connections
   * @return Listener handle
   */
  public int addConnectionListener(Consumer<ConnectionNotification> listener, boolean immediateNotify) {
    m_connectionListenerLock.lock();
    try {
      if (m_connectionListenerPoller == 0) {
        m_connectionListenerPoller = NetworkTablesJNI.createConnectionListenerPoller(m_handle);
        startConnectionListenerThread();
      }
      int handle = NetworkTablesJNI.addPolledConnectionListener(m_connectionListenerPoller, immediateNotify);
      m_connectionListeners.put(handle, listener);
      return handle;
    } finally {
      m_connectionListenerLock.unlock();
    }
  }

  /**
   * Remove a connection listener.
   * @param listener Listener handle to remove
   */
  public void removeConnectionListener(int listener) {
    m_connectionListenerLock.lock();
    try {
      m_connectionListeners.remove(listener);
    } finally {
      m_connectionListenerLock.unlock();
    }
    NetworkTablesJNI.removeConnectionListener(listener);
  }

  /**
   * Wait for the connection listener queue to be empty.  This is primarily useful
   * for deterministic testing.  This blocks until either the connection listener
   * queue is empty (e.g. there are no more events that need to be passed along
   * to callbacks or poll queues) or the timeout expires.
   * @param timeout   timeout, in seconds.  Set to 0 for non-blocking behavior,
   *                  or a negative value to block indefinitely
   * @return False if timed out, otherwise true.
   */
  public boolean waitForConnectionListenerQueue(double timeout) {
    if (!NetworkTablesJNI.waitForConnectionListenerQueue(m_handle, timeout)) {
      return false;
    }
    m_connectionListenerLock.lock();
    try {
      if (m_connectionListenerPoller != 0) {
        m_connectionListenerWaitQueue = true;
        NetworkTablesJNI.cancelPollConnectionListener(m_connectionListenerPoller);
        while (m_connectionListenerWaitQueue) {
          try {
            if (timeout < 0) {
              m_connectionListenerWaitQueueCond.await();
            } else {
              return m_connectionListenerWaitQueueCond.await((long) (timeout * 1e9), TimeUnit.NANOSECONDS);
            }
          } catch (InterruptedException ex) {
            Thread.currentThread().interrupt();
            return true;
          }
        }
      }
    } finally {
      m_connectionListenerLock.unlock();
    }
    return true;
  }

  /*
   * Remote Procedure Call Functions
   */

  private final ReentrantLock m_rpcCallLock = new ReentrantLock();
  private final Map<Integer, EntryConsumer<RpcAnswer>> m_rpcCalls = new HashMap<>();
  private Thread m_rpcCallThread;
  private int m_rpcCallPoller;
  private boolean m_rpcCallWaitQueue;
  private final Condition m_rpcCallWaitQueueCond = m_rpcCallLock.newCondition();

  private void startRpcCallThread() {
    m_rpcCallThread = new Thread(() -> {
      boolean wasInterrupted = false;
      while (!Thread.interrupted()) {
        RpcAnswer[] events;
        try {
          events = NetworkTablesJNI.pollRpc(this, m_rpcCallPoller);
        } catch (InterruptedException ex) {
          m_rpcCallLock.lock();
          try {
            if (m_rpcCallWaitQueue) {
              m_rpcCallWaitQueue = false;
              m_rpcCallWaitQueueCond.signalAll();
              continue;
            }
          } finally {
            m_rpcCallLock.unlock();
          }
          Thread.currentThread().interrupt();
          wasInterrupted = true; // don't try to destroy poller, as its handle is likely no longer valid
          break;
        }
        for (RpcAnswer event : events) {
          EntryConsumer<RpcAnswer> listener;
          m_rpcCallLock.lock();
          try {
            listener = m_rpcCalls.get(event.entry);
          } finally {
            m_rpcCallLock.unlock();
          }
          if (listener != null) {
            event.entryObject = listener.entry;
            try {
              listener.consumer.accept(event);
            } catch (Throwable throwable) {
              System.err.println("Unhandled exception during RPC callback: " + throwable.toString());
              throwable.printStackTrace();
            }
          }
        }
      }
      m_rpcCallLock.lock();
      try {
        if (!wasInterrupted) {
          NetworkTablesJNI.destroyRpcCallPoller(m_rpcCallPoller);
        }
        m_rpcCallPoller = 0;
      } finally {
        m_rpcCallLock.unlock();
      }
    }, "NTRpcCall");
    m_rpcCallThread.setDaemon(true);
    m_rpcCallThread.start();
  }

  private static final byte[] rev0def = new byte[] {0};

  /**
   * Create a callback-based RPC entry point.  Only valid to use on the server.
   * The callback function will be called when the RPC is called.
   * This function creates RPC version 0 definitions (raw data in and out).
   * @param entry     the entry
   * @param callback  callback function
   */
  public void createRpc(NetworkTableEntry entry, Consumer<RpcAnswer> callback) {
    m_rpcCallLock.lock();
    try {
      if (m_rpcCallPoller == 0) {
        m_rpcCallPoller = NetworkTablesJNI.createRpcCallPoller(m_handle);
        startRpcCallThread();
      }
      NetworkTablesJNI.createPolledRpc(entry.getHandle(), rev0def, m_rpcCallPoller);
      m_rpcCalls.put(entry.getHandle(), new EntryConsumer<>(entry, callback));
    } finally {
      m_rpcCallLock.unlock();
    }
  }

  /**
   * Wait for the incoming RPC call queue to be empty.  This is primarily useful
   * for deterministic testing.  This blocks until either the RPC call
   * queue is empty (e.g. there are no more events that need to be passed along
   * to callbacks or poll queues) or the timeout expires.
   * @param timeout   timeout, in seconds.  Set to 0 for non-blocking behavior,
   *                  or a negative value to block indefinitely
   * @return False if timed out, otherwise true.
   */
  public boolean waitForRpcCallQueue(double timeout) {
    if (!NetworkTablesJNI.waitForRpcCallQueue(m_handle, timeout)) {
      return false;
    }
    m_rpcCallLock.lock();
    try {
      if (m_rpcCallPoller != 0) {
        m_rpcCallWaitQueue = true;
        NetworkTablesJNI.cancelPollRpc(m_rpcCallPoller);
        while (m_rpcCallWaitQueue) {
          try {
            if (timeout < 0) {
              m_rpcCallWaitQueueCond.await();
            } else {
              return m_rpcCallWaitQueueCond.await((long) (timeout * 1e9), TimeUnit.NANOSECONDS);
            }
          } catch (InterruptedException ex) {
            Thread.currentThread().interrupt();
            return true;
          }
        }
      }
    } finally {
      m_rpcCallLock.unlock();
    }
    return true;
  }

  /*
   * Client/Server Functions
   */

  /**
   * Set the network identity of this node.
   * This is the name used during the initial connection handshake, and is
   * visible through ConnectionInfo on the remote node.
   * @param name      identity to advertise
   */
  public void setNetworkIdentity(String name) {
    NetworkTablesJNI.setNetworkIdentity(m_handle, name);
  }

  /**
   * Get the current network mode.
   * @return Bitmask of NetworkMode.
   */
  public int getNetworkMode() {
    return NetworkTablesJNI.getNetworkMode(m_handle);
  }

  /**
   * Starts a server using the networktables.ini as the persistent file,
   * using the default listening address and port.
   */
  public void startServer() {
    startServer("networktables.ini");
  }

  /**
   * Starts a server using the specified persistent filename, using the default
   * listening address and port.
   *
   * @param persistFilename  the name of the persist file to use
   */
  public void startServer(String persistFilename) {
    startServer(persistFilename, "");
  }

  /**
   * Starts a server using the specified filename and listening address,
   * using the default port.
   *
   * @param persistFilename  the name of the persist file to use
   * @param listenAddress    the address to listen on, or empty to listen on any
   *                         address
   */
  public void startServer(String persistFilename, String listenAddress) {
    startServer(persistFilename, listenAddress, kDefaultPort);
  }

  /**
   * Starts a server using the specified filename, listening address, and port.
   *
   * @param persistFilename  the name of the persist file to use
   * @param listenAddress    the address to listen on, or empty to listen on any
   *                         address
   * @param port             port to communicate over
   */
  public void startServer(String persistFilename, String listenAddress, int port) {
    NetworkTablesJNI.startServer(m_handle, persistFilename, listenAddress, port);
  }

  /**
   * Stops the server if it is running.
   */
  public void stopServer() {
    NetworkTablesJNI.stopServer(m_handle);
  }

  /**
   * Starts a client.  Use SetServer to set the server name and port.
   */
  public void startClient() {
    NetworkTablesJNI.startClient(m_handle);
  }

  /**
   * Starts a client using the specified server and the default port
   *
   * @param serverName  server name
   */
  public void startClient(String serverName) {
    startClient(serverName, kDefaultPort);
  }

  /**
   * Starts a client using the specified server and port
   *
   * @param serverName  server name
   * @param port        port to communicate over
   */
  public void startClient(String serverName, int port) {
    NetworkTablesJNI.startClient(m_handle, serverName, port);
  }

  /**
   * Starts a client using the specified servers and default port.  The
   * client will attempt to connect to each server in round robin fashion.
   *
   * @param serverNames   array of server names
   */
  public void startClient(String[] serverNames) {
    startClient(serverNames, kDefaultPort);
  }

  /**
   * Starts a client using the specified servers and port number.  The
   * client will attempt to connect to each server in round robin fashion.
   *
   * @param serverNames   array of server names
   * @param port          port to communicate over
   */
  public void startClient(String[] serverNames, int port) {
    int[] ports = new int[serverNames.length];
    for (int i = 0; i < serverNames.length; i++) {
      ports[i] = port;
    }
    startClient(serverNames, ports);
  }

  /**
   * Starts a client using the specified (server, port) combinations.  The
   * client will attempt to connect to each server in round robin fashion.
   *
   * @param serverNames   array of server names
   * @param ports         array of port numbers
   */
  public void startClient(String[] serverNames, int[] ports) {
    NetworkTablesJNI.startClient(m_handle, serverNames, ports);
  }

  /**
   * Starts a client using commonly known robot addresses for the specified
   * team using the default port number.
   *
   * @param team        team number
   */
  public void startClientTeam(int team) {
    startClientTeam(team, kDefaultPort);
  }

  /**
   * Starts a client using commonly known robot addresses for the specified
   * team.
   *
   * @param team        team number
   * @param port        port to communicate over
   */
  public void startClientTeam(int team, int port) {
    NetworkTablesJNI.startClientTeam(m_handle, team, port);
  }

  /**
   * Stops the client if it is running.
   */
  public void stopClient() {
    NetworkTablesJNI.stopClient(m_handle);
  }

  /**
   * Sets server address and port for client (without restarting client).
   * Changes the port to the default port.
   *
   * @param serverName  server name
   */
  public void setServer(String serverName) {
    setServer(serverName, kDefaultPort);
  }

  /**
   * Sets server address and port for client (without restarting client).
   *
   * @param serverName  server name
   * @param port        port to communicate over
   */
  public void setServer(String serverName, int port) {
    NetworkTablesJNI.setServer(m_handle, serverName, port);
  }

  /**
   * Sets server addresses and port for client (without restarting client).
   * Changes the port to the default port.  The client will attempt to connect
   * to each server in round robin fashion.
   *
   * @param serverNames   array of server names
   */
  public void setServer(String[] serverNames) {
    setServer(serverNames, kDefaultPort);
  }

  /**
   * Sets server addresses and port for client (without restarting client).
   * The client will attempt to connect to each server in round robin fashion.
   *
   * @param serverNames   array of server names
   * @param port          port to communicate over
   */
  public void setServer(String[] serverNames, int port) {
    int[] ports = new int[serverNames.length];
    for (int i = 0; i < serverNames.length; i++) {
      ports[i] = port;
    }
    setServer(serverNames, ports);
  }

  /**
   * Sets server addresses and ports for client (without restarting client).
   * The client will attempt to connect to each server in round robin fashion.
   *
   * @param serverNames   array of server names
   * @param ports         array of port numbers
   */
  public void setServer(String[] serverNames, int[] ports) {
    NetworkTablesJNI.setServer(m_handle, serverNames, ports);
  }

  /**
   * Sets server addresses and port for client (without restarting client).
   * Changes the port to the default port.  The client will attempt to connect
   * to each server in round robin fashion.
   *
   * @param team        team number
   */
  public void setServerTeam(int team) {
    setServerTeam(team, kDefaultPort);
  }

  /**
   * Sets server addresses and port for client (without restarting client).
   * Connects using commonly known robot addresses for the specified team.
   *
   * @param team        team number
   * @param port        port to communicate over
   */
  public void setServerTeam(int team, int port) {
    NetworkTablesJNI.setServerTeam(m_handle, team, port);
  }

  /**
   * Starts requesting server address from Driver Station.
   * This connects to the Driver Station running on localhost to obtain the
   * server IP address, and connects with the default port.
   */
  public void startDSClient() {
    startDSClient(kDefaultPort);
  }

  /**
   * Starts requesting server address from Driver Station.
   * This connects to the Driver Station running on localhost to obtain the
   * server IP address.
   *
   * @param port server port to use in combination with IP from DS
   */
  public void startDSClient(int port) {
    NetworkTablesJNI.startDSClient(m_handle, port);
  }

  /**
   * Stops requesting server address from Driver Station.
   */
  public void stopDSClient() {
    NetworkTablesJNI.stopDSClient(m_handle);
  }

  /**
   * Set the periodic update rate.
   * Sets how frequently updates are sent to other nodes over the network.
   *
   * @param interval update interval in seconds (range 0.01 to 1.0)
   */
  public void setUpdateRate(double interval) {
    NetworkTablesJNI.setUpdateRate(m_handle, interval);
  }

  /**
   * Flushes all updated values immediately to the network.
   * Note: This is rate-limited to protect the network from flooding.
   * This is primarily useful for synchronizing network updates with
   * user code.
   */
  public void flush() {
    NetworkTablesJNI.flush(m_handle);
  }

  /**
   * Gets information on the currently established network connections.
   * If operating as a client, this will return either zero or one values.
   * @return array of connection information
   */
  public ConnectionInfo[] getConnections() {
    return NetworkTablesJNI.getConnections(m_handle);
  }

  /**
   * Return whether or not the instance is connected to another node.
   * @return True if connected.
   */
  public boolean isConnected() {
    return NetworkTablesJNI.isConnected(m_handle);
  }

  /**
   * Saves persistent keys to a file.  The server does this automatically.
   *
   * @param filename file name
   * @throws PersistentException if error saving file
   */
  public void savePersistent(String filename) throws PersistentException {
    NetworkTablesJNI.savePersistent(m_handle, filename);
  }

  /**
   * Loads persistent keys from a file.  The server does this automatically.
   *
   * @param filename file name
   * @return List of warnings (errors result in an exception instead)
   * @throws PersistentException if error reading file
   */
  public String[] loadPersistent(String filename) throws PersistentException {
    return NetworkTablesJNI.loadPersistent(m_handle, filename);
  }

  /**
   * Save table values to a file.  The file format used is identical to
   * that used for SavePersistent.
   * @param filename  filename
   * @param prefix    save only keys starting with this prefix
   * @throws PersistentException if error saving file
   */
  public void saveEntries(String filename, String prefix) throws PersistentException {
    NetworkTablesJNI.saveEntries(m_handle, filename, prefix);
  }

  /**
   * Load table values from a file.  The file format used is identical to
   * that used for SavePersistent / LoadPersistent.
   * @param filename  filename
   * @param prefix    load only keys starting with this prefix
   * @return List of warnings (errors result in an exception instead)
   * @throws PersistentException if error saving file
   */
  public String[] loadEntries(String filename, String prefix) throws PersistentException {
    return NetworkTablesJNI.loadEntries(m_handle, filename, prefix);
  }

  private final ReentrantLock m_loggerLock = new ReentrantLock();
  private final Map<Integer, Consumer<LogMessage>> m_loggers = new HashMap<>();
  private Thread m_loggerThread;
  private int m_loggerPoller;
  private boolean m_loggerWaitQueue;
  private final Condition m_loggerWaitQueueCond = m_loggerLock.newCondition();

  private void startLogThread() {
    m_loggerThread = new Thread(() -> {
      boolean wasInterrupted = false;
      while (!Thread.interrupted()) {
        LogMessage[] events;
        try {
          events = NetworkTablesJNI.pollLogger(this, m_loggerPoller);
        } catch (InterruptedException ex) {
          Thread.currentThread().interrupt();
          wasInterrupted = true; // don't try to destroy poller, as its handle is likely no longer valid
          break;
        }
        for (LogMessage event : events) {
          Consumer<LogMessage> logger;
          m_loggerLock.lock();
          try {
            logger = m_loggers.get(event.logger);
          } finally {
            m_loggerLock.unlock();
          }
          if (logger != null) {
            try {
              logger.accept(event);
            } catch (Throwable throwable) {
              System.err.println("Unhandled exception during logger callback: " + throwable.toString());
              throwable.printStackTrace();
            }
          }
        }
      }
      m_loggerLock.lock();
      try {
        if (!wasInterrupted) {
          NetworkTablesJNI.destroyLoggerPoller(m_loggerPoller);
        }
        m_rpcCallPoller = 0;
      } finally {
        m_loggerLock.unlock();
      }
    }, "NTLogger");
    m_loggerThread.setDaemon(true);
    m_loggerThread.start();
  }

  /**
   * Add logger callback function.  By default, log messages are sent to stderr;
   * this function sends log messages with the specified levels to the provided
   * callback function instead.  The callback function will only be called for
   * log messages with level greater than or equal to minLevel and less than or
   * equal to maxLevel; messages outside this range will be silently ignored.
   *
   * @param func        log callback function
   * @param minLevel    minimum log level
   * @param maxLevel    maximum log level
   * @return Logger handle
   */
  public int addLogger(Consumer<LogMessage> func, int minLevel, int maxLevel) {
    m_loggerLock.lock();
    try {
      if (m_loggerPoller == 0) {
        m_loggerPoller = NetworkTablesJNI.createLoggerPoller(m_handle);
        startLogThread();
      }
      int handle = NetworkTablesJNI.addPolledLogger(m_loggerPoller, minLevel, maxLevel);
      m_loggers.put(handle, func);
      return handle;
    } finally {
      m_loggerLock.unlock();
    }
  }

  /**
   * Remove a logger.
   * @param logger Logger handle to remove
   */
  public void removeLogger(int logger) {
    m_loggerLock.lock();
    try {
      m_loggers.remove(logger);
    } finally {
      m_loggerLock.unlock();
    }
    NetworkTablesJNI.removeLogger(logger);
  }

  /**
   * Wait for the incoming log event queue to be empty.  This is primarily useful
   * for deterministic testing.  This blocks until either the log event
   * queue is empty (e.g. there are no more events that need to be passed along
   * to callbacks or poll queues) or the timeout expires.
   * @param timeout   timeout, in seconds.  Set to 0 for non-blocking behavior,
   *                  or a negative value to block indefinitely
   * @return False if timed out, otherwise true.
   */
  public boolean waitForLoggerQueue(double timeout) {
    if (!NetworkTablesJNI.waitForLoggerQueue(m_handle, timeout)) {
      return false;
    }
    m_loggerLock.lock();
    try {
      if (m_loggerPoller != 0) {
        m_loggerWaitQueue = true;
        NetworkTablesJNI.cancelPollLogger(m_loggerPoller);
        while (m_loggerWaitQueue) {
          try {
            if (timeout < 0) {
              m_loggerWaitQueueCond.await();
            } else {
              return m_loggerWaitQueueCond.await((long) (timeout * 1e9), TimeUnit.NANOSECONDS);
            }
          } catch (InterruptedException ex) {
            Thread.currentThread().interrupt();
            return true;
          }
        }
      }
    } finally {
      m_loggerLock.unlock();
    }
    return true;
  }

  @Override
  public boolean equals(Object o) {
    if (o == this) {
      return true;
    }
    if (!(o instanceof NetworkTableInstance)) {
      return false;
    }
    NetworkTableInstance other = (NetworkTableInstance) o;
    return m_handle == other.m_handle;
  }

  @Override
  public int hashCode() {
    return m_handle;
  }

  private boolean m_owned;
  private int m_handle;
} public enum NetworkTableType {
  kUnassigned(0),
  kBoolean(0x01),
  kDouble(0x02),
  kString(0x04),
  kRaw(0x08),
  kBooleanArray(0x10),
  kDoubleArray(0x20),
  kStringArray(0x40),
  kRpc(0x80);

  private final int value;

  private NetworkTableType(int value) {
    this.value = value;
  }

  public int getValue() {
    return value;
  }

  public static NetworkTableType getFromInt(int value) {
    switch (value) {
      case 0x01: return kBoolean;
      case 0x02: return kDouble;
      case 0x04: return kString;
      case 0x08: return kRaw;
      case 0x10: return kBooleanArray;
      case 0x20: return kDoubleArray;
      case 0x40: return kStringArray;
      case 0x80: return kRpc;
      default: return kUnassigned;
    }
  }
} public final class NetworkTableValue {
  NetworkTableValue(NetworkTableType type, Object value, long time) {
    m_type = type;
    m_value = value;
    m_time = time;
  }

  NetworkTableValue(NetworkTableType type, Object value) {
    this(type, value, NetworkTablesJNI.now());
  }

  NetworkTableValue(int type, Object value, long time) {
    this(NetworkTableType.getFromInt(type), value, time);
  }

  /**
   * Get the data type.
   * @return The type.
   */
  public NetworkTableType getType() {
    return m_type;
  }

  /**
   * Get the data value stored.
   * @return The type.
   */
  public Object getValue() {
    return m_value;
  }

  /**
   * Get the creation time of the value.
   * @return The time, in the units returned by NetworkTablesJNI.now().
   */
  public long getTime() {
    return m_time;
  }

  /*
   * Type Checkers
   */

  /**
   * Determine if entry value contains a value or is unassigned.
   * @return True if the entry value contains a value.
   */
  public boolean isValid() {
    return m_type != NetworkTableType.kUnassigned;
  }

  /**
   * Determine if entry value contains a boolean.
   * @return True if the entry value is of boolean type.
   */
  public boolean isBoolean() {
    return m_type == NetworkTableType.kBoolean;
  }

  /**
   * Determine if entry value contains a double.
   * @return True if the entry value is of double type.
   */
  public boolean isDouble() {
    return m_type == NetworkTableType.kDouble;
  }

  /**
   * Determine if entry value contains a string.
   * @return True if the entry value is of string type.
   */
  public boolean isString() {
    return m_type == NetworkTableType.kString;
  }

  /**
   * Determine if entry value contains a raw.
   * @return True if the entry value is of raw type.
   */
  public boolean isRaw() {
    return m_type == NetworkTableType.kRaw;
  }

  /**
   * Determine if entry value contains a rpc definition.
   * @return True if the entry value is of rpc definition type.
   */
  public boolean isRpc() {
    return m_type == NetworkTableType.kRpc;
  }

  /**
   * Determine if entry value contains a boolean array.
   * @return True if the entry value is of boolean array type.
   */
  public boolean isBooleanArray() {
    return m_type == NetworkTableType.kBooleanArray;
  }

  /**
   * Determine if entry value contains a double array.
   * @return True if the entry value is of double array type.
   */
  public boolean isDoubleArray() {
    return m_type == NetworkTableType.kDoubleArray;
  }

  /**
   * Determine if entry value contains a string array.
   * @return True if the entry value is of string array type.
   */
  public boolean isStringArray() {
    return m_type == NetworkTableType.kStringArray;
  }

  /*
   * Type-Safe Getters
   */

  /**
   * Get the entry's boolean value.
   * @throws ClassCastException if the entry value is not of boolean type.
   * @return The boolean value.
   */
  public boolean getBoolean() {
    if (m_type != NetworkTableType.kBoolean) {
      throw new ClassCastException("cannot convert " + m_type + " to boolean");
    }
    return ((Boolean)m_value).booleanValue();
  }

  /**
   * Get the entry's double value.
   * @throws ClassCastException if the entry value is not of double type.
   * @return The double value.
   */
  public double getDouble() {
    if (m_type != NetworkTableType.kDouble) {
      throw new ClassCastException("cannot convert " + m_type + " to double");
    }
    return ((Number)m_value).doubleValue();
  }

  /**
   * Get the entry's string value.
   * @throws ClassCastException if the entry value is not of string type.
   * @return The string value.
   */
  public String getString() {
    if (m_type != NetworkTableType.kString) {
      throw new ClassCastException("cannot convert " + m_type + " to string");
    }
    return (String)m_value;
  }

  /**
   * Get the entry's raw value.
   * @throws ClassCastException if the entry value is not of raw type.
   * @return The raw value.
   */
  public byte[] getRaw() {
    if (m_type != NetworkTableType.kRaw) {
      throw new ClassCastException("cannot convert " + m_type + " to raw");
    }
    return (byte[])m_value;
  }

  /**
   * Get the entry's rpc definition value.
   * @throws ClassCastException if the entry value is not of rpc definition type.
   * @return The rpc definition value.
   */
  public byte[] getRpc() {
    if (m_type != NetworkTableType.kRpc) {
      throw new ClassCastException("cannot convert " + m_type + " to rpc");
    }
    return (byte[])m_value;
  }

  /**
   * Get the entry's boolean array value.
   * @throws ClassCastException if the entry value is not of boolean array type.
   * @return The boolean array value.
   */
  public boolean[] getBooleanArray() {
    if (m_type != NetworkTableType.kBooleanArray) {
      throw new ClassCastException("cannot convert " + m_type + " to boolean array");
    }
    return (boolean[])m_value;
  }

  /**
   * Get the entry's double array value.
   * @throws ClassCastException if the entry value is not of double array type.
   * @return The double array value.
   */
  public double[] getDoubleArray() {
    if (m_type != NetworkTableType.kDoubleArray) {
      throw new ClassCastException("cannot convert " + m_type + " to double array");
    }
    return (double[])m_value;
  }

  /**
   * Get the entry's string array value.
   * @throws ClassCastException if the entry value is not of string array type.
   * @return The string array value.
   */
  public String[] getStringArray() {
    if (m_type != NetworkTableType.kStringArray) {
      throw new ClassCastException("cannot convert " + m_type + " to string array");
    }
    return (String[])m_value;
  }

  /*
   * Factory functions.
   */

  /**
   * Creates a boolean entry value.
   * @param value the value
   * @return The entry value
   */
  public static NetworkTableValue makeBoolean(boolean value) {
    return new NetworkTableValue(NetworkTableType.kBoolean, new Boolean(value));
  }

  /**
   * Creates a boolean entry value.
   * @param value the value
   * @param time the creation time to use (instead of the current time)
   * @return The entry value
   */
  public static NetworkTableValue makeBoolean(boolean value, long time) {
    return new NetworkTableValue(NetworkTableType.kBoolean, new Boolean(value), time);
  }

  /**
   * Creates a double entry value.
   * @param value the value
   * @return The entry value
   */
  public static NetworkTableValue makeDouble(double value) {
    return new NetworkTableValue(NetworkTableType.kDouble, new Double(value));
  }

  /**
   * Creates a double entry value.
   * @param value the value
   * @param time the creation time to use (instead of the current time)
   * @return The entry value
   */
  public static NetworkTableValue makeDouble(double value, long time) {
    return new NetworkTableValue(NetworkTableType.kDouble, new Double(value), time);
  }

  /**
   * Creates a string entry value.
   * @param value the value
   * @return The entry value
   */
  public static NetworkTableValue makeString(String value) {
    return new NetworkTableValue(NetworkTableType.kString, value);
  }

  /**
   * Creates a string entry value.
   * @param value the value
   * @param time the creation time to use (instead of the current time)
   * @return The entry value
   */
  public static NetworkTableValue makeString(String value, long time) {
    return new NetworkTableValue(NetworkTableType.kString, value, time);
  }

  /**
   * Creates a raw entry value.
   * @param value the value
   * @return The entry value
   */
  public static NetworkTableValue makeRaw(byte[] value) {
    return new NetworkTableValue(NetworkTableType.kRaw, value);
  }

  /**
   * Creates a raw entry value.
   * @param value the value
   * @param time the creation time to use (instead of the current time)
   * @return The entry value
   */
  public static NetworkTableValue makeRaw(byte[] value, long time) {
    return new NetworkTableValue(NetworkTableType.kRaw, value, time);
  }

  /**
   * Creates a rpc entry value.
   * @param value the value
   * @return The entry value
   */
  public static NetworkTableValue makeRpc(byte[] value) {
    return new NetworkTableValue(NetworkTableType.kRpc, value);
  }

  /**
   * Creates a rpc entry value.
   * @param value the value
   * @param time the creation time to use (instead of the current time)
   * @return The entry value
   */
  public static NetworkTableValue makeRpc(byte[] value, long time) {
    return new NetworkTableValue(NetworkTableType.kRpc, value, time);
  }

  /**
   * Creates a boolean array entry value.
   * @param value the value
   * @return The entry value
   */
  public static NetworkTableValue makeBooleanArray(boolean[] value) {
    return new NetworkTableValue(NetworkTableType.kBooleanArray, value);
  }

  /**
   * Creates a boolean array entry value.
   * @param value the value
   * @param time the creation time to use (instead of the current time)
   * @return The entry value
   */
  public static NetworkTableValue makeBooleanArray(boolean[] value, long time) {
    return new NetworkTableValue(NetworkTableType.kBooleanArray, value, time);
  }

  /**
   * Creates a boolean array entry value.
   * @param value the value
   * @return The entry value
   */
  public static NetworkTableValue makeBooleanArray(Boolean[] value) {
    return new NetworkTableValue(NetworkTableType.kBooleanArray, toNative(value));
  }

  /**
   * Creates a boolean array entry value.
   * @param value the value
   * @param time the creation time to use (instead of the current time)
   * @return The entry value
   */
  public static NetworkTableValue makeBooleanArray(Boolean[] value, long time) {
    return new NetworkTableValue(NetworkTableType.kBooleanArray, toNative(value), time);
  }

  /**
   * Creates a double array entry value.
   * @param value the value
   * @return The entry value
   */
  public static NetworkTableValue makeDoubleArray(double[] value) {
    return new NetworkTableValue(NetworkTableType.kDoubleArray, value);
  }

  /**
   * Creates a double array entry value.
   * @param value the value
   * @param time the creation time to use (instead of the current time)
   * @return The entry value
   */
  public static NetworkTableValue makeDoubleArray(double[] value, long time) {
    return new NetworkTableValue(NetworkTableType.kDoubleArray, value, time);
  }

  /**
   * Creates a double array entry value.
   * @param value the value
   * @return The entry value
   */
  public static NetworkTableValue makeDoubleArray(Number[] value) {
    return new NetworkTableValue(NetworkTableType.kDoubleArray, toNative(value));
  }

  /**
   * Creates a double array entry value.
   * @param value the value
   * @param time the creation time to use (instead of the current time)
   * @return The entry value
   */
  public static NetworkTableValue makeDoubleArray(Number[] value, long time) {
    return new NetworkTableValue(NetworkTableType.kDoubleArray, toNative(value), time);
  }

  /**
   * Creates a string array entry value.
   * @param value the value
   * @return The entry value
   */
  public static NetworkTableValue makeStringArray(String[] value) {
    return new NetworkTableValue(NetworkTableType.kStringArray, value);
  }

  /**
   * Creates a string array entry value.
   * @param value the value
   * @param time the creation time to use (instead of the current time)
   * @return The entry value
   */
  public static NetworkTableValue makeStringArray(String[] value, long time) {
    return new NetworkTableValue(NetworkTableType.kStringArray, value, time);
  }

  @Override
  public boolean equals(Object o) {
    if (o == this) {
      return true;
    }
    if (!(o instanceof NetworkTableValue)) {
      return false;
    }
    NetworkTableValue other = (NetworkTableValue) o;
    return m_type == other.m_type && m_value.equals(other.m_value);
  }

  @Override
  public int hashCode() {
    return Objects.hash(m_type, m_value);
  }

  static boolean[] toNative(Boolean[] arr) {
    boolean[] out = new boolean[arr.length];
    for (int i = 0; i < arr.length; i++)
      out[i] = arr[i];
    return out;
  }

  static double[] toNative(Number[] arr) {
    double[] out = new double[arr.length];
    for (int i = 0; i < arr.length; i++)
      out[i] = arr[i].doubleValue();
    return out;
  }

  static Boolean[] fromNative(boolean[] arr) {
    Boolean[] out = new Boolean[arr.length];
    for (int i = 0; i < arr.length; i++)
      out[i] = arr[i];
    return out;
  }

  static Double[] fromNative(double[] arr) {
    Double[] out = new Double[arr.length];
    for (int i = 0; i < arr.length; i++)
      out[i] = arr[i];
    return out;
  }

  private NetworkTableType m_type;
  private Object m_value;
  private long m_time;
} public final class NetworkTablesJNI {
  static boolean libraryLoaded = false;
  static File jniLibrary = null;
  static {
    if (!libraryLoaded) {
      try {
        System.loadLibrary("ntcore");
      } catch (UnsatisfiedLinkError e) {
        try {
          String resname = RuntimeDetector.getLibraryResource("ntcore");
          InputStream is = NetworkTablesJNI.class.getResourceAsStream(resname);
          if (is != null) {
            // create temporary file
            if (System.getProperty("os.name").startsWith("Windows"))
              jniLibrary = File.createTempFile("NetworkTablesJNI", ".dll");
            else if (System.getProperty("os.name").startsWith("Mac"))
              jniLibrary = File.createTempFile("libNetworkTablesJNI", ".dylib");
            else
              jniLibrary = File.createTempFile("libNetworkTablesJNI", ".so");
            // flag for delete on exit
            jniLibrary.deleteOnExit();
            OutputStream os = new FileOutputStream(jniLibrary);

            byte[] buffer = new byte[1024];
            int readBytes;
            try {
              while ((readBytes = is.read(buffer)) != -1) {
                os.write(buffer, 0, readBytes);
              }
            } finally {
              os.close();
              is.close();
            }
            System.load(jniLibrary.getAbsolutePath());
          } else {
            System.loadLibrary("ntcore");
          }
        } catch (IOException ex) {
          ex.printStackTrace();
          System.exit(1);
        }
      }
      libraryLoaded = true;
    }
  }

  public static native int getDefaultInstance();
  public static native int createInstance();
  public static native void destroyInstance(int inst);
  public static native int getInstanceFromHandle(int handle);

  public static native int getEntry(int inst, String key);
  public static native int[] getEntries(int inst, String prefix, int types);
  public static native String getEntryName(int entry);
  public static native long getEntryLastChange(int entry);

  public static native int getType(int entry);

  public static native boolean setBoolean(int entry, long time, boolean value, boolean force);
  public static native boolean setDouble(int entry, long time, double value, boolean force);
  public static native boolean setString(int entry, long time, String value, boolean force);
  public static native boolean setRaw(int entry, long time, byte[] value, boolean force);
  public static native boolean setRaw(int entry, long time, ByteBuffer value, int len, boolean force);
  public static native boolean setBooleanArray(int entry, long time, boolean[] value, boolean force);
  public static native boolean setDoubleArray(int entry, long time, double[] value, boolean force);
  public static native boolean setStringArray(int entry, long time, String[] value, boolean force);

  public static native NetworkTableValue getValue(int entry);

  public static native boolean getBoolean(int entry, boolean defaultValue);
  public static native double getDouble(int entry, double defaultValue);
  public static native String getString(int entry, String defaultValue);
  public static native byte[] getRaw(int entry, byte[] defaultValue);
  public static native boolean[] getBooleanArray(int entry, boolean[] defaultValue);
  public static native double[] getDoubleArray(int entry, double[] defaultValue);
  public static native String[] getStringArray(int entry, String[] defaultValue);
  public static native boolean setDefaultBoolean(int entry, long time, boolean defaultValue);

  public static native boolean setDefaultDouble(int entry, long time, double defaultValue);
  public static native boolean setDefaultString(int entry, long time, String defaultValue);
  public static native boolean setDefaultRaw(int entry, long time, byte[] defaultValue);
  public static native boolean setDefaultBooleanArray(int entry, long time, boolean[] defaultValue);
  public static native boolean setDefaultDoubleArray(int entry, long time, double[] defaultValue);
  public static native boolean setDefaultStringArray(int entry, long time, String[] defaultValue);

  public static native void setEntryFlags(int entry, int flags);
  public static native int getEntryFlags(int entry);

  public static native void deleteEntry(int entry);

  public static native void deleteAllEntries(int inst);

  public static native EntryInfo getEntryInfoHandle(NetworkTableInstance inst, int entry);
  public static native EntryInfo[] getEntryInfo(NetworkTableInstance instObject, int inst, String prefix, int types);

  public static native int createEntryListenerPoller(int inst);
  public static native void destroyEntryListenerPoller(int poller);
  public static native int addPolledEntryListener(int poller, String prefix, int flags);
  public static native int addPolledEntryListener(int poller, int entry, int flags);
  public static native EntryNotification[] pollEntryListener(NetworkTableInstance inst, int poller) throws InterruptedException;
  public static native EntryNotification[] pollEntryListenerTimeout(NetworkTableInstance inst, int poller, double timeout) throws InterruptedException;
  public static native void cancelPollEntryListener(int poller);
  public static native void removeEntryListener(int entryListener);
  public static native boolean waitForEntryListenerQueue(int inst, double timeout);

  public static native int createConnectionListenerPoller(int inst);
  public static native void destroyConnectionListenerPoller(int poller);
  public static native int addPolledConnectionListener(int poller, boolean immediateNotify);
  public static native ConnectionNotification[] pollConnectionListener(NetworkTableInstance inst, int poller) throws InterruptedException;
  public static native ConnectionNotification[] pollConnectionListenerTimeout(NetworkTableInstance inst, int poller, double timeout) throws InterruptedException;
  public static native void cancelPollConnectionListener(int poller);
  public static native void removeConnectionListener(int connListener);
  public static native boolean waitForConnectionListenerQueue(int inst, double timeout);

  public static native int createRpcCallPoller(int inst);
  public static native void destroyRpcCallPoller(int poller);
  public static native void createPolledRpc(int entry, byte[] def, int poller);
  public static native RpcAnswer[] pollRpc(NetworkTableInstance inst, int poller) throws InterruptedException;
  public static native RpcAnswer[] pollRpcTimeout(NetworkTableInstance inst, int poller, double timeout) throws InterruptedException;
  public static native void cancelPollRpc(int poller);
  public static native boolean waitForRpcCallQueue(int inst, double timeout);
  public static native void postRpcResponse(int entry, int call, byte[] result);
  public static native int callRpc(int entry, byte[] params);
  public static native byte[] getRpcResult(int entry, int call);
  public static native byte[] getRpcResult(int entry, int call, double timeout);
  public static native void cancelRpcResult(int entry, int call);

  public static native byte[] getRpc(int entry, byte[] defaultValue);

  public static native void setNetworkIdentity(int inst, String name);
  public static native int getNetworkMode(int inst);
  public static native void startServer(int inst, String persistFilename, String listenAddress, int port);
  public static native void stopServer(int inst);
  public static native void startClient(int inst);
  public static native void startClient(int inst, String serverName, int port);
  public static native void startClient(int inst, String[] serverNames, int[] ports);
  public static native void startClientTeam(int inst, int team, int port);
  public static native void stopClient(int inst);
  public static native void setServer(int inst, String serverName, int port);
  public static native void setServer(int inst, String[] serverNames, int[] ports);
  public static native void setServerTeam(int inst, int team, int port);
  public static native void startDSClient(int inst, int port);
  public static native void stopDSClient(int inst);
  public static native void setUpdateRate(int inst, double interval);

  public static native void flush(int inst);

  public static native ConnectionInfo[] getConnections(int inst);

  public static native boolean isConnected(int inst);

  public static native void savePersistent(int inst, String filename) throws PersistentException;
  public static native String[] loadPersistent(int inst, String filename) throws PersistentException;  // returns warnings

  public static native void saveEntries(int inst, String filename, String prefix) throws PersistentException;
  public static native String[] loadEntries(int inst, String filename, String prefix) throws PersistentException;  // returns warnings

  public static native long now();

  public static native int createLoggerPoller(int inst);
  public static native void destroyLoggerPoller(int poller);
  public static native int addPolledLogger(int poller, int minLevel, int maxLevel);
  public static native LogMessage[] pollLogger(NetworkTableInstance inst, int poller) throws InterruptedException;
  public static native LogMessage[] pollLoggerTimeout(NetworkTableInstance inst, int poller, double timeout) throws InterruptedException;
  public static native void cancelPollLogger(int poller);
  public static native void removeLogger(int logger);
  public static native boolean waitForLoggerQueue(int inst, double timeout);
} public final class PersistentException extends IOException {
  public static final long serialVersionUID = 0;

  /**
   * @param message The error message
   */
  public PersistentException(String message) {
    super(message);
  }

} @Deprecated
public interface IRemote {
	/**
	 * Register an object to listen for connection and disconnection events
	 *
	 * @param listener the listener to be register
	 * @param immediateNotify if the listener object should be notified of the current connection state
	 */
	public void addConnectionListener(IRemoteConnectionListener listener, boolean immediateNotify);

	/**
	 * Unregister a listener from connection events
	 *
	 * @param listener the listener to be unregistered
	 */
	public void removeConnectionListener(IRemoteConnectionListener listener);

	/**
	 * Get the current state of the objects connection
	 * @return the current connection state
	 */
    public boolean isConnected();

	/**
	 * If the object is acting as a server
	 * @return if the object is a server
	 */
    public boolean isServer();
} @Deprecated
public interface IRemoteConnectionListener {
	/**
	 * Called when an IRemote is connected
	 * @param remote the object that connected
	 */
	public void connected(IRemote remote);
	/**
	 * Called when an IRemote is disconnected
	 * @param remote the object that disconnected
	 */
	public void disconnected(IRemote remote);
	/**
	 * Extended version of connected called when an IRemote is connected.
 	 * Contains the connection info of the connected remote
	 * @param remote the object that connected
	 * @param info the connection info for the connected remote
	 */
	default public void connectedEx(IRemote remote, ConnectionInfo info) {
		connected(remote);
	}
	/**
	 * Extended version of connected called when an IRemote is disconnected.
	 * Contains the connection info of the disconnected remote
	 * @param remote the object that disconnected
	 * @param info the connection info for the disconnected remote
	 */
	default public void disconnectedEx(IRemote remote, ConnectionInfo info) {
		disconnected(remote);
	}
} @Deprecated
public interface ITable {

  /**
   * Checks the table and tells if it contains the specified key
   *
   * @param key the key to search for
   * @return true if the table as a value assigned to the given key
   */
  public boolean containsKey(String key);

  /**
   * @param key the key to search for
   * @return true if there is a subtable with the key which contains at least
   * one key/subtable of its own
   */
  public boolean containsSubTable(String key);

  /**
   * Returns the table at the specified key. If there is no table at the
   * specified key, it will create a new table
   *
   * @param key the name of the table relative to this one
   * @return a sub table relative to this one
   */
  public ITable getSubTable(String key);

  /**
   * Gets all keys in the table (not including sub-tables).
   * @param types bitmask of types; 0 is treated as a "don't care".
   * @return keys currently in the table
   */
  public Set<String> getKeys(int types);

  /**
   * Gets all keys in the table (not including sub-tables).
   * @return keys currently in the table
   */
  public Set<String> getKeys();

  /**
   * Gets the names of all subtables in the table.
   * @return subtables currently in the table
   */
  public Set<String> getSubTables();

  /**
   * Makes a key's value persistent through program restarts.
   * The key cannot be null.
   *
   * @param key the key name
   */
  public void setPersistent(String key);

  /**
   * Stop making a key's value persistent through program restarts.
   * The key cannot be null.
   *
   * @param key the key name
   */
  public void clearPersistent(String key);

  /**
   * Returns whether the value is persistent through program restarts.
   * The key cannot be null.
   *
   * @param key the key name
   * @return True if the value is persistent.
   */
  public boolean isPersistent(String key);

  /**
   * Sets flags on the specified key in this table. The key can
   * not be null.
   *
   * @param key the key name
   * @param flags the flags to set (bitmask)
   */
  public void setFlags(String key, int flags);

  /**
   * Clears flags on the specified key in this table. The key can
   * not be null.
   *
   * @param key the key name
   * @param flags the flags to clear (bitmask)
   */
  public void clearFlags(String key, int flags);

  /**
   * Returns the flags for the specified key.
   *
   * @param key the key name
   * @return the flags, or 0 if the key is not defined
   */
  public int getFlags(String key);

  /**
   * Deletes the specified key in this table. The key can
   * not be null.
   *
   * @param key the key name
   */
  public void delete(String key);

  /**
   * Gets the value associated with a key as an object.
   * NOTE: If the value is a double, it will return a Double object,
   * not a primitive.  To get the primitive, use
   * {@link #getDouble(String, double)}.
   * @param key the key of the value to look up
   * @param defaultValue the default value if the key is null
   * @return the value associated with the given key
   */
  public Object getValue(String key, Object defaultValue);

  /**
   * Put a value in the table
   * @param key the key to be assigned to
   * @param value the value that will be assigned
   * @return False if the table key already exists with a different type
   * @throws IllegalArgumentException when the value is not supported by the
   * table
   */
  public boolean putValue(String key, Object value)
      throws IllegalArgumentException;

  /**
   * Put a number in the table
   * @param key the key to be assigned to
   * @param value the value that will be assigned
   * @return False if the table key already exists with a different type
   */
  public boolean putNumber(String key, double value);

  /**
   * Gets the current value in the table, setting it if it does not exist.
   * @param key the key
   * @param defaultValue the default value to set if key doens't exist.
   * @return False if the table key exists with a different type
   */
  public boolean setDefaultNumber(String key, double defaultValue);

  /**
   * Returns the number the key maps to. If the key does not exist or is of
   * different type, it will return the default value.
   * @param key the key to look up
   * @param defaultValue the value to be returned if no value is found
   * @return the value associated with the given key or the given default value
   * if there is no value associated with the key
   */
  public double getNumber(String key, double defaultValue);

  /**
   * Put a string in the table
   * @param key the key to be assigned to
   * @param value the value that will be assigned
   * @return False if the table key already exists with a different type
   */
  public boolean putString(String key, String value);

  /**
   * Gets the current value in the table, setting it if it does not exist.
   * @param key the key
   * @param defaultValue the default value to set if key doens't exist.
   * @return False if the table key exists with a different type
   */
  public boolean setDefaultString(String key, String defaultValue);

  /**
   * Returns the string the key maps to. If the key does not exist or is of
   * different type, it will return the default value.
   * @param key the key to look up
   * @param defaultValue the value to be returned if no value is found
   * @return the value associated with the given key or the given default value
   * if there is no value associated with the key
   */
  public String getString(String key, String defaultValue);

  /**
   * Put a boolean in the table
   * @param key the key to be assigned to
   * @param value the value that will be assigned
   * @return False if the table key already exists with a different type
   */
  public boolean putBoolean(String key, boolean value);

  /**
   * Gets the current value in the table, setting it if it does not exist.
   * @param key the key
   * @param defaultValue the default value to set if key doens't exist.
   * @return False if the table key exists with a different type
   */
  public boolean setDefaultBoolean(String key, boolean defaultValue);

  /**
   * Returns the boolean the key maps to. If the key does not exist or is of
   * different type, it will return the default value.
   * @param key the key to look up
   * @param defaultValue the value to be returned if no value is found
   * @return the value associated with the given key or the given default value
   * if there is no value associated with the key
   */
  public boolean getBoolean(String key, boolean defaultValue);

  /**
   * Put a boolean array in the table
   * @param key the key to be assigned to
   * @param value the value that will be assigned
   * @return False if the table key already exists with a different type
   */
  public boolean putBooleanArray(String key, boolean[] value);

  /**
   * Gets the current value in the table, setting it if it does not exist.
   * @param key the key
   * @param defaultValue the default value to set if key doens't exist.
   * @return False if the table key exists with a different type
   */
  public boolean setDefaultBooleanArray(String key, boolean[] defaultValue);

  /**
   * Put a boolean array in the table
   * @param key the key to be assigned to
   * @param value the value that will be assigned
   * @return False if the table key already exists with a different type
   */
  public boolean putBooleanArray(String key, Boolean[] value);

  /**
   * Gets the current value in the table, setting it if it does not exist.
   * @param key the key
   * @param defaultValue the default value to set if key doens't exist.
   * @return False if the table key exists with a different type
   */
  public boolean setDefaultBooleanArray(String key, Boolean[] defaultValue);

  /**
   * Returns the boolean array the key maps to. If the key does not exist or is
   * of different type, it will return the default value.
   * @param key the key to look up
   * @param defaultValue the value to be returned if no value is found
   * @return the value associated with the given key or the given default value
   * if there is no value associated with the key
   */
  public boolean[] getBooleanArray(String key, boolean[] defaultValue);
  /**
   * Returns the boolean array the key maps to. If the key does not exist or is
   * of different type, it will return the default value.
   * @param key the key to look up
   * @param defaultValue the value to be returned if no value is found
   * @return the value associated with the given key or the given default value
   * if there is no value associated with the key
   */
  public Boolean[] getBooleanArray(String key, Boolean[] defaultValue);

  /**
   * Put a number array in the table
   * @param key the key to be assigned to
   * @param value the value that will be assigned
   * @return False if the table key already exists with a different type
   */
  public boolean putNumberArray(String key, double[] value);

  /**
   * Gets the current value in the table, setting it if it does not exist.
   * @param key the key
   * @param defaultValue the default value to set if key doens't exist.
   * @return False if the table key exists with a different type
   */
  public boolean setDefaultNumberArray(String key, double[] defaultValue);

  /**
   * Put a number array in the table
   * @param key the key to be assigned to
   * @param value the value that will be assigned
   * @return False if the table key already exists with a different type
   */
  public boolean putNumberArray(String key, Double[] value);

  /**
   * Gets the current value in the table, setting it if it does not exist.
   * @param key the key
   * @param defaultValue the default value to set if key doens't exist.
   * @return False if the table key exists with a different type
   */
  public boolean setDefaultNumberArray(String key, Double[] defaultValue);

  /**
   * Returns the number array the key maps to. If the key does not exist or is
   * of different type, it will return the default value.
   * @param key the key to look up
   * @param defaultValue the value to be returned if no value is found
   * @return the value associated with the given key or the given default value
   * if there is no value associated with the key
   */
  public double[] getNumberArray(String key, double[] defaultValue);
  /**
   * Returns the number array the key maps to. If the key does not exist or is
   * of different type, it will return the default value.
   * @param key the key to look up
   * @param defaultValue the value to be returned if no value is found
   * @return the value associated with the given key or the given default value
   * if there is no value associated with the key
   */
  public Double[] getNumberArray(String key, Double[] defaultValue);

  /**
   * Put a string array in the table
   * @param key the key to be assigned to
   * @param value the value that will be assigned
   * @return False if the table key already exists with a different type
   */
  public boolean putStringArray(String key, String[] value);

  /**
   * Gets the current value in the table, setting it if it does not exist.
   * @param key the key
   * @param defaultValue the default value to set if key doens't exist.
   * @return False if the table key exists with a different type
   */
  public boolean setDefaultStringArray(String key, String[] defaultValue);

  /**
   * Returns the string array the key maps to. If the key does not exist or is
   * of different type, it will return the default value.
   * @param key the key to look up
   * @param defaultValue the value to be returned if no value is found
   * @return the value associated with the given key or the given default value
   * if there is no value associated with the key
   */
  public String[] getStringArray(String key, String[] defaultValue);

  /**
   * Put a raw value (byte array) in the table
   * @param key the key to be assigned to
   * @param value the value that will be assigned
   * @return False if the table key already exists with a different type
   */
  public boolean putRaw(String key, byte[] value);

  /**
   * Gets the current value in the table, setting it if it does not exist.
   * @param key the key
   * @param defaultValue the default value to set if key doens't exist.
   * @return False if the table key exists with a different type
   */
  public boolean setDefaultRaw(String key, byte[] defaultValue);

  /**
   * Put a raw value (bytes from a byte buffer) in the table
   * @param key the key to be assigned to
   * @param value the value that will be assigned
   * @param len the length of the value
   * @return False if the table key already exists with a different type
   */
  public boolean putRaw(String key, ByteBuffer value, int len);

  /**
   * Returns the raw value (byte array) the key maps to. If the key does not
   * exist or is of different type, it will return the default value.
   * @param key the key to look up
   * @param defaultValue the value to be returned if no value is found
   * @return the value associated with the given key or the given default value
   * if there is no value associated with the key
   */
  public byte[] getRaw(String key, byte[] defaultValue);

  /** Notifier flag values. */
  public static final int NOTIFY_IMMEDIATE = 0x01;
  public static final int NOTIFY_LOCAL = 0x02;
  public static final int NOTIFY_NEW = 0x04;
  public static final int NOTIFY_DELETE = 0x08;
  public static final int NOTIFY_UPDATE = 0x10;
  public static final int NOTIFY_FLAGS = 0x20;

  /**
   * Add a listener for changes to the table
   * @param listener the listener to add
   */
  public void addTableListener(ITableListener listener);
  /**
   * Add a listener for changes to the table
   * @param listener the listener to add
   * @param immediateNotify if true then this listener will be notified of all
   * current entries (marked as new)
   */
  public void addTableListener(ITableListener listener,
                               boolean immediateNotify);
  /**
   * Add a listener for changes to the table
   * @param listener the listener to add
   * @param flags bitmask specifying desired notifications
   */
  public void addTableListenerEx(ITableListener listener, int flags);

  /**
   * Add a listener for changes to a specific key the table
   * @param key the key to listen for
   * @param listener the listener to add
   * @param immediateNotify if true then this listener will be notified of all
   * current entries (marked as new)
   */
  public void addTableListener(String key, ITableListener listener,
                               boolean immediateNotify);
  /**
   * Add a listener for changes to a specific key the table
   * @param key the key to listen for
   * @param listener the listener to add
   * @param flags bitmask specifying desired notifications
   */
  public void addTableListenerEx(String key, ITableListener listener,
                                 int flags);
  /**
   * This will immediately notify the listener of all current sub tables
   * @param listener the listener to notify
   */
  public void addSubTableListener(final ITableListener listener);
  /**
   * This will immediately notify the listener of all current sub tables
   * @param listener the listener to notify
   * @param localNotify if true then this listener will be notified of all
   * local changes in addition to all remote changes
   */
  public void addSubTableListener(final ITableListener listener,
                                  boolean localNotify);
  /**
   * Remove a listener from receiving table events
   * @param listener the listener to be removed
   */
  public void removeTableListener(ITableListener listener);

  /*
   * Deprecated Methods
   */

  /**
   * Maps the specified key to the specified value in this table.
   * The key can not be null.
   * The value can be retrieved by calling the get method with a key that is
   * equal to the original key.
   * @param key the key
   * @param value the value
   * @return False if the table key already exists with a different type
   * @throws IllegalArgumentException if key is null
   * @deprecated Use {@link #putNumber(String, double)} instead.
   */
  @Deprecated
  public boolean putDouble(String key, double value);

  /**
   * Returns the value at the specified key.
   * @param key the key
   * @param defaultValue the value returned if the key is undefined
   * @return the value
   * @throws IllegalArgumentException if the value mapped to by the key is not a
   * double
   * @throws IllegalArgumentException if the key is null
   * @deprecated Use {@link #getNumber(String, double)} instead.
   */
  @Deprecated
  public double getDouble(String key, double defaultValue);

  /**
   * Gets the full path of this table.  Does not include the trailing "/".
   * @return The path to this table (e.g. "", "/foo").
   */
  public String getPath();

} @FunctionalInterface
@Deprecated
public interface ITableListener {
    /**
     * Called when a key-value pair is changed in a {@link ITable}
     * @param source the table the key-value pair exists in
     * @param key the key associated with the value that changed
     * @param value the new value
     * @param isNew true if the key did not previously exist in the table, otherwise it is false
     */
    public void valueChanged(ITable source, String key, Object value, boolean isNew);

    /**
     * Extended version of valueChanged.  Called when a key-value pair is
     * changed in a {@link ITable}.  The default implementation simply calls
     * valueChanged().  If this is overridden, valueChanged() will not be
     * called.
     * @param source the table the key-value pair exists in
     * @param key the key associated with the value that changed
     * @param value the new value
     * @param flags update flags; for example, NOTIFY_NEW if the key did not
     * previously exist in the table
     */
    default public void valueChangedEx(ITable source, String key, Object value, int flags) {
        // NOTIFY_NEW = 0x04
        valueChanged(source, key, value, (flags & 0x04) != 0);
    }
}
import edu.wpi.first.networktables.ConnectionInfo;
import edu.wpi.first.networktables.ConnectionNotification;
import edu.wpi.first.networktables.EntryInfo;
import edu.wpi.first.networktables.EntryNotification;
import edu.wpi.first.networktables.NetworkTableEntry;
import edu.wpi.first.networktables.NetworkTableInstance;
import edu.wpi.first.networktables.NetworkTableType;
import edu.wpi.first.networktables.NetworkTableValue;
import edu.wpi.first.networktables.NetworkTablesJNI;
import edu.wpi.first.networktables.PersistentException;
import edu.wpi.first.wpilibj.tables.IRemote;
import edu.wpi.first.wpilibj.tables.IRemoteConnectionListener;
import edu.wpi.first.wpilibj.tables.ITable;
import edu.wpi.first.wpilibj.tables.ITableListener;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.function.Consumer;
      this.targetListener = targetListener;
    }

    @Override
    public void accept(EntryNotification event) {
      targetListener.valueChangedEx(targetSource, relativeKey, event.value.getValue(), event.flags);
    }
  }

  /**
   * {@inheritDoc}
   * @deprecated Use {@link edu.wpi.first.networktables.NetworkTable#addEntryListener(String, TableEntryListener, int)}
   * or {@link NetworkTableEntry#addListener(Consumer, int)} instead.
   */
  @Override
  @Deprecated
  public synchronized void addTableListenerEx(String key,
                                              ITableListener listener,
                                              int flags) {
    List<ListenerBase> adapters = oldListenerMap.get(listener);
    if (adapters == null) {
      adapters = new ArrayList<ListenerBase>();
      oldListenerMap.put(listener, adapters);
    }
    OldKeyListenerAdapter adapter = new OldKeyListenerAdapter(key, this, listener);
    adapter.uid = inst.addEntryListener(getEntry(key), adapter, flags);
    adapters.add(adapter);
  }

  /**
   * {@inheritDoc}
   * @deprecated Use {@link edu.wpi.first.networktables.NetworkTable#addSubTableListener(TableListener, boolean)}
   * instead.
   */
  @Override
  @Deprecated
  public void addSubTableListener(final ITableListener listener) {
    addSubTableListener(listener, false);
  }

  private class OldSubListenerAdapter extends ListenerBase implements Consumer<EntryNotification> {
    private final int prefixLen;
    private final ITable targetSource;
    private final ITableListener targetListener;
    private final Set<String> notifiedTables = new HashSet<String>();

    public OldSubListenerAdapter(int prefixLen, ITable targetSource, ITableListener targetListener) {
      this.prefixLen = prefixLen;
      this.targetSource = targetSource;
      this.targetListener = targetListener;
    }

    @Override
    public void accept(EntryNotification event) {
      String relativeKey = event.name.substring(prefixLen);
      int endSubTable = relativeKey.indexOf(PATH_SEPARATOR);
      if (endSubTable == -1)
        return;
      String subTableKey = relativeKey.substring(0, endSubTable);
      if (notifiedTables.contains(subTableKey))
        return;
      notifiedTables.add(subTableKey);
      targetListener.valueChangedEx(targetSource, subTableKey, targetSource.getSubTable(subTableKey), event.flags);
    }
  }

  /**
   * {@inheritDoc}
   * @deprecated Use {@link edu.wpi.first.networktables.NetworkTable#addSubTableListener(TableListener, boolean)}
   * instead.
   */
  @Override
  @Deprecated
  public synchronized void addSubTableListener(final ITableListener listener,
                                               boolean localNotify) {
    List<ListenerBase> adapters = oldListenerMap.get(listener);
    if (adapters == null) {
      adapters = new ArrayList<ListenerBase>();
      oldListenerMap.put(listener, adapters);
    }
    OldSubListenerAdapter adapter =
        new OldSubListenerAdapter(path.length() + 1, this, listener);
    int flags = NOTIFY_NEW | NOTIFY_IMMEDIATE;
    if (localNotify)
      flags |= NOTIFY_LOCAL;
    adapter.uid = inst.addEntryListener(pathWithSep, adapter, flags);
    adapters.add(adapter);
  }

  /**
   * {@inheritDoc}
   * @deprecated Use {@link edu.wpi.first.networktables.NetworkTable#removeTableListener(int)} instead.
   */
  @Override
  @Deprecated
  public synchronized void removeTableListener(ITableListener listener) {
    List<ListenerBase> adapters = oldListenerMap.remove(listener);
    if (adapters != null) {
      for (ListenerBase adapter : adapters)
        inst.removeEntryListener(adapter.uid);
    }
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public ITable getSubTable(String key) {
    return new NetworkTable(inst, pathWithSep + key);
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public boolean containsKey(String key) {
    return getEntry(key).exists();
  }

  public boolean containsSubTable(String key) {
    int[] handles = NetworkTablesJNI.getEntries(inst.getHandle(), pathWithSep + key + PATH_SEPARATOR, 0);