001// Copyright (c) FIRST and other WPILib contributors.
002// Open Source Software; you can modify and/or share it under the terms of
003// the WPILib BSD license file in the root directory of this project.
004
005// THIS FILE WAS AUTO-GENERATED BY ./ntcore/generate_topics.py. DO NOT MODIFY
006
007package edu.wpi.first.networktables;
008
009import edu.wpi.first.util.WPIUtilJNI;
010import edu.wpi.first.util.concurrent.Event;
011import edu.wpi.first.util.datalog.DataLog;
012import edu.wpi.first.util.protobuf.Protobuf;
013import edu.wpi.first.util.struct.Struct;
014import java.nio.charset.StandardCharsets;
015import java.util.EnumSet;
016import java.util.HashMap;
017import java.util.HashSet;
018import java.util.Map;
019import java.util.OptionalLong;
020import java.util.Set;
021import java.util.concurrent.ConcurrentHashMap;
022import java.util.concurrent.ConcurrentMap;
023import java.util.concurrent.TimeUnit;
024import java.util.concurrent.locks.Condition;
025import java.util.concurrent.locks.ReentrantLock;
026import java.util.function.Consumer;
027import us.hebi.quickbuf.ProtoMessage;
028
029/**
030 * NetworkTables Instance.
031 *
032 * <p>Instances are completely independent from each other. Table operations on one instance will
033 * not be visible to other instances unless the instances are connected via the network. The main
034 * limitation on instances is that you cannot have two servers on the same network port. The main
035 * utility of instances is for unit testing, but they can also enable one program to connect to two
036 * different NetworkTables networks.
037 *
038 * <p>The global "default" instance (as returned by {@link #getDefault()}) is always available, and
039 * is intended for the common case when there is only a single NetworkTables instance being used in
040 * the program.
041 *
042 * <p>Additional instances can be created with the {@link #create()} function. A reference must be
043 * kept to the NetworkTableInstance returned by this function to keep it from being garbage
044 * collected.
045 */
046public final class NetworkTableInstance implements AutoCloseable {
047  /** Client/server mode flag values (as returned by {@link #getNetworkMode()}). */
048  public enum NetworkMode {
049    /** Running in server mode. */
050    kServer(0x01),
051
052    /** Running in NT3 client mode. */
053    kClient3(0x02),
054
055    /** Running in NT4 client mode. */
056    kClient4(0x04),
057
058    /** Currently starting up (either client or server). */
059    kStarting(0x08),
060
061    /** Running in local-only mode. */
062    kLocal(0x10);
063
064    private final int value;
065
066    NetworkMode(int value) {
067      this.value = value;
068    }
069
070    /**
071     * Returns the network mode value.
072     *
073     * @return The network mode value.
074     */
075    public int getValue() {
076      return value;
077    }
078  }
079
080  /** The default port that network tables operates on for NT3. */
081  public static final int kDefaultPort3 = 1735;
082
083  /** The default port that network tables operates on for NT4. */
084  public static final int kDefaultPort4 = 5810;
085
086  /**
087   * Construct from native handle.
088   *
089   * @param handle Native handle
090   */
091  private NetworkTableInstance(int handle) {
092    m_owned = false;
093    m_handle = handle;
094  }
095
096  /** Destroys the instance (if created by {@link #create()}). */
097  @Override
098  public synchronized void close() {
099    if (m_owned && m_handle != 0) {
100      m_listeners.close();
101      m_schemas.forEach((k, v) -> v.close());
102      NetworkTablesJNI.destroyInstance(m_handle);
103      m_handle = 0;
104    }
105  }
106
107  /**
108   * Determines if the native handle is valid.
109   *
110   * @return True if the native handle is valid, false otherwise.
111   */
112  public boolean isValid() {
113    return m_handle != 0;
114  }
115
116  /* The default instance. */
117  private static NetworkTableInstance s_defaultInstance;
118
119  /**
120   * Get global default instance.
121   *
122   * @return Global default instance
123   */
124  public static synchronized NetworkTableInstance getDefault() {
125    if (s_defaultInstance == null) {
126      s_defaultInstance = new NetworkTableInstance(NetworkTablesJNI.getDefaultInstance());
127    }
128    return s_defaultInstance;
129  }
130
131  /**
132   * Create an instance. Note: A reference to the returned instance must be retained to ensure the
133   * instance is not garbage collected.
134   *
135   * @return Newly created instance
136   */
137  public static NetworkTableInstance create() {
138    NetworkTableInstance inst = new NetworkTableInstance(NetworkTablesJNI.createInstance());
139    inst.m_owned = true;
140    return inst;
141  }
142
143  /**
144   * Gets the native handle for the instance.
145   *
146   * @return Native handle
147   */
148  public int getHandle() {
149    return m_handle;
150  }
151
152  /**
153   * Get (generic) topic.
154   *
155   * @param name topic name
156   * @return Topic
157   */
158  public Topic getTopic(String name) {
159    Topic topic = m_topics.get(name);
160    if (topic == null) {
161      int handle = NetworkTablesJNI.getTopic(m_handle, name);
162      topic = new Topic(this, handle);
163      Topic oldTopic = m_topics.putIfAbsent(name, topic);
164      if (oldTopic != null) {
165        topic = oldTopic;
166      }
167      // also cache by handle
168      m_topicsByHandle.putIfAbsent(handle, topic);
169    }
170    return topic;
171  }
172
173  /**
174   * Get boolean topic.
175   *
176   * @param name topic name
177   * @return BooleanTopic
178   */
179  public BooleanTopic getBooleanTopic(String name) {
180    Topic topic = m_topics.get(name);
181    if (topic instanceof BooleanTopic t) {
182      return t;
183    }
184
185    int handle;
186    if (topic == null) {
187      handle = NetworkTablesJNI.getTopic(m_handle, name);
188    } else {
189      handle = topic.getHandle();
190    }
191
192    BooleanTopic wrapTopic = new BooleanTopic(this, handle);
193    m_topics.put(name, wrapTopic);
194
195    // also cache by handle
196    m_topicsByHandle.put(handle, wrapTopic);
197
198    return wrapTopic;
199  }
200
201  /**
202   * Get long topic.
203   *
204   * @param name topic name
205   * @return IntegerTopic
206   */
207  public IntegerTopic getIntegerTopic(String name) {
208    Topic topic = m_topics.get(name);
209    if (topic instanceof IntegerTopic t) {
210      return t;
211    }
212
213    int handle;
214    if (topic == null) {
215      handle = NetworkTablesJNI.getTopic(m_handle, name);
216    } else {
217      handle = topic.getHandle();
218    }
219
220    IntegerTopic wrapTopic = new IntegerTopic(this, handle);
221    m_topics.put(name, wrapTopic);
222
223    // also cache by handle
224    m_topicsByHandle.put(handle, wrapTopic);
225
226    return wrapTopic;
227  }
228
229  /**
230   * Get float topic.
231   *
232   * @param name topic name
233   * @return FloatTopic
234   */
235  public FloatTopic getFloatTopic(String name) {
236    Topic topic = m_topics.get(name);
237    if (topic instanceof FloatTopic t) {
238      return t;
239    }
240
241    int handle;
242    if (topic == null) {
243      handle = NetworkTablesJNI.getTopic(m_handle, name);
244    } else {
245      handle = topic.getHandle();
246    }
247
248    FloatTopic wrapTopic = new FloatTopic(this, handle);
249    m_topics.put(name, wrapTopic);
250
251    // also cache by handle
252    m_topicsByHandle.put(handle, wrapTopic);
253
254    return wrapTopic;
255  }
256
257  /**
258   * Get double topic.
259   *
260   * @param name topic name
261   * @return DoubleTopic
262   */
263  public DoubleTopic getDoubleTopic(String name) {
264    Topic topic = m_topics.get(name);
265    if (topic instanceof DoubleTopic t) {
266      return t;
267    }
268
269    int handle;
270    if (topic == null) {
271      handle = NetworkTablesJNI.getTopic(m_handle, name);
272    } else {
273      handle = topic.getHandle();
274    }
275
276    DoubleTopic wrapTopic = new DoubleTopic(this, handle);
277    m_topics.put(name, wrapTopic);
278
279    // also cache by handle
280    m_topicsByHandle.put(handle, wrapTopic);
281
282    return wrapTopic;
283  }
284
285  /**
286   * Get String topic.
287   *
288   * @param name topic name
289   * @return StringTopic
290   */
291  public StringTopic getStringTopic(String name) {
292    Topic topic = m_topics.get(name);
293    if (topic instanceof StringTopic t) {
294      return t;
295    }
296
297    int handle;
298    if (topic == null) {
299      handle = NetworkTablesJNI.getTopic(m_handle, name);
300    } else {
301      handle = topic.getHandle();
302    }
303
304    StringTopic wrapTopic = new StringTopic(this, handle);
305    m_topics.put(name, wrapTopic);
306
307    // also cache by handle
308    m_topicsByHandle.put(handle, wrapTopic);
309
310    return wrapTopic;
311  }
312
313  /**
314   * Get byte[] topic.
315   *
316   * @param name topic name
317   * @return RawTopic
318   */
319  public RawTopic getRawTopic(String name) {
320    Topic topic = m_topics.get(name);
321    if (topic instanceof RawTopic t) {
322      return t;
323    }
324
325    int handle;
326    if (topic == null) {
327      handle = NetworkTablesJNI.getTopic(m_handle, name);
328    } else {
329      handle = topic.getHandle();
330    }
331
332    RawTopic wrapTopic = new RawTopic(this, handle);
333    m_topics.put(name, wrapTopic);
334
335    // also cache by handle
336    m_topicsByHandle.put(handle, wrapTopic);
337
338    return wrapTopic;
339  }
340
341  /**
342   * Get boolean[] topic.
343   *
344   * @param name topic name
345   * @return BooleanArrayTopic
346   */
347  public BooleanArrayTopic getBooleanArrayTopic(String name) {
348    Topic topic = m_topics.get(name);
349    if (topic instanceof BooleanArrayTopic t) {
350      return t;
351    }
352
353    int handle;
354    if (topic == null) {
355      handle = NetworkTablesJNI.getTopic(m_handle, name);
356    } else {
357      handle = topic.getHandle();
358    }
359
360    BooleanArrayTopic wrapTopic = new BooleanArrayTopic(this, handle);
361    m_topics.put(name, wrapTopic);
362
363    // also cache by handle
364    m_topicsByHandle.put(handle, wrapTopic);
365
366    return wrapTopic;
367  }
368
369  /**
370   * Get long[] topic.
371   *
372   * @param name topic name
373   * @return IntegerArrayTopic
374   */
375  public IntegerArrayTopic getIntegerArrayTopic(String name) {
376    Topic topic = m_topics.get(name);
377    if (topic instanceof IntegerArrayTopic t) {
378      return t;
379    }
380
381    int handle;
382    if (topic == null) {
383      handle = NetworkTablesJNI.getTopic(m_handle, name);
384    } else {
385      handle = topic.getHandle();
386    }
387
388    IntegerArrayTopic wrapTopic = new IntegerArrayTopic(this, handle);
389    m_topics.put(name, wrapTopic);
390
391    // also cache by handle
392    m_topicsByHandle.put(handle, wrapTopic);
393
394    return wrapTopic;
395  }
396
397  /**
398   * Get float[] topic.
399   *
400   * @param name topic name
401   * @return FloatArrayTopic
402   */
403  public FloatArrayTopic getFloatArrayTopic(String name) {
404    Topic topic = m_topics.get(name);
405    if (topic instanceof FloatArrayTopic t) {
406      return t;
407    }
408
409    int handle;
410    if (topic == null) {
411      handle = NetworkTablesJNI.getTopic(m_handle, name);
412    } else {
413      handle = topic.getHandle();
414    }
415
416    FloatArrayTopic wrapTopic = new FloatArrayTopic(this, handle);
417    m_topics.put(name, wrapTopic);
418
419    // also cache by handle
420    m_topicsByHandle.put(handle, wrapTopic);
421
422    return wrapTopic;
423  }
424
425  /**
426   * Get double[] topic.
427   *
428   * @param name topic name
429   * @return DoubleArrayTopic
430   */
431  public DoubleArrayTopic getDoubleArrayTopic(String name) {
432    Topic topic = m_topics.get(name);
433    if (topic instanceof DoubleArrayTopic t) {
434      return t;
435    }
436
437    int handle;
438    if (topic == null) {
439      handle = NetworkTablesJNI.getTopic(m_handle, name);
440    } else {
441      handle = topic.getHandle();
442    }
443
444    DoubleArrayTopic wrapTopic = new DoubleArrayTopic(this, handle);
445    m_topics.put(name, wrapTopic);
446
447    // also cache by handle
448    m_topicsByHandle.put(handle, wrapTopic);
449
450    return wrapTopic;
451  }
452
453  /**
454   * Get String[] topic.
455   *
456   * @param name topic name
457   * @return StringArrayTopic
458   */
459  public StringArrayTopic getStringArrayTopic(String name) {
460    Topic topic = m_topics.get(name);
461    if (topic instanceof StringArrayTopic t) {
462      return t;
463    }
464
465    int handle;
466    if (topic == null) {
467      handle = NetworkTablesJNI.getTopic(m_handle, name);
468    } else {
469      handle = topic.getHandle();
470    }
471
472    StringArrayTopic wrapTopic = new StringArrayTopic(this, handle);
473    m_topics.put(name, wrapTopic);
474
475    // also cache by handle
476    m_topicsByHandle.put(handle, wrapTopic);
477
478    return wrapTopic;
479  }
480
481
482  /**
483   * Get protobuf-encoded value topic.
484   *
485   * @param <T> value class (inferred from proto)
486   * @param <MessageType> protobuf message type (inferred from proto)
487   * @param name topic name
488   * @param proto protobuf serialization implementation
489   * @return ProtobufTopic
490   */
491  public <T, MessageType extends ProtoMessage<?>>
492      ProtobufTopic<T> getProtobufTopic(String name, Protobuf<T, MessageType> proto) {
493    Topic topic = m_topics.get(name);
494    if (topic instanceof ProtobufTopic<?> t && t.getProto().equals(proto)) {
495      @SuppressWarnings("unchecked")
496      ProtobufTopic<T> wrapTopic = (ProtobufTopic<T>) topic;
497      return wrapTopic;
498    }
499
500    int handle;
501    if (topic == null) {
502      handle = NetworkTablesJNI.getTopic(m_handle, name);
503    } else {
504      handle = topic.getHandle();
505    }
506
507    ProtobufTopic<T> wrapTopic = ProtobufTopic.wrap(this, handle, proto);
508    m_topics.put(name, wrapTopic);
509
510    // also cache by handle
511    m_topicsByHandle.put(handle, wrapTopic);
512
513    return wrapTopic;
514  }
515
516  /**
517   * Get struct-encoded value topic.
518   *
519   * @param <T> value class (inferred from struct)
520   * @param name topic name
521   * @param struct struct serialization implementation
522   * @return StructTopic
523   */
524  public <T>
525      StructTopic<T> getStructTopic(String name, Struct<T> struct) {
526    Topic topic = m_topics.get(name);
527    if (topic instanceof StructTopic<?> t && t.getStruct().equals(struct)) {
528      @SuppressWarnings("unchecked")
529      StructTopic<T> wrapTopic = (StructTopic<T>) topic;
530      return wrapTopic;
531    }
532
533    int handle;
534    if (topic == null) {
535      handle = NetworkTablesJNI.getTopic(m_handle, name);
536    } else {
537      handle = topic.getHandle();
538    }
539
540    StructTopic<T> wrapTopic = StructTopic.wrap(this, handle, struct);
541    m_topics.put(name, wrapTopic);
542
543    // also cache by handle
544    m_topicsByHandle.put(handle, wrapTopic);
545
546    return wrapTopic;
547  }
548
549  /**
550   * Get struct-encoded value array topic.
551   *
552   * @param <T> value class (inferred from struct)
553   * @param name topic name
554   * @param struct struct serialization implementation
555   * @return StructArrayTopic
556   */
557  public <T>
558      StructArrayTopic<T> getStructArrayTopic(String name, Struct<T> struct) {
559    Topic topic = m_topics.get(name);
560    if (topic instanceof StructArrayTopic<?> t && t.getStruct().equals(struct)) {
561      @SuppressWarnings("unchecked")
562      StructArrayTopic<T> wrapTopic = (StructArrayTopic<T>) topic;
563      return wrapTopic;
564    }
565
566    int handle;
567    if (topic == null) {
568      handle = NetworkTablesJNI.getTopic(m_handle, name);
569    } else {
570      handle = topic.getHandle();
571    }
572
573    StructArrayTopic<T> wrapTopic = StructArrayTopic.wrap(this, handle, struct);
574    m_topics.put(name, wrapTopic);
575
576    // also cache by handle
577    m_topicsByHandle.put(handle, wrapTopic);
578
579    return wrapTopic;
580  }
581
582  private Topic[] topicHandlesToTopics(int[] handles) {
583    Topic[] topics = new Topic[handles.length];
584    for (int i = 0; i < handles.length; i++) {
585      topics[i] = getCachedTopic(handles[i]);
586    }
587    return topics;
588  }
589
590  /**
591   * Get all published topics.
592   *
593   * @return Array of topics.
594   */
595  public Topic[] getTopics() {
596    return topicHandlesToTopics(NetworkTablesJNI.getTopics(m_handle, "", 0));
597  }
598
599  /**
600   * Get published topics starting with the given prefix. The results are optionally filtered by
601   * string prefix to only return a subset of all topics.
602   *
603   * @param prefix topic name required prefix; only topics whose name starts with this string are
604   *     returned
605   * @return Array of topic information.
606   */
607  public Topic[] getTopics(String prefix) {
608    return topicHandlesToTopics(NetworkTablesJNI.getTopics(m_handle, prefix, 0));
609  }
610
611  /**
612   * Get published topics starting with the given prefix. The results are optionally filtered by
613   * string prefix and data type to only return a subset of all topics.
614   *
615   * @param prefix topic name required prefix; only topics whose name starts with this string are
616   *     returned
617   * @param types bitmask of data types; 0 is treated as a "don't care"
618   * @return Array of topic information.
619   */
620  public Topic[] getTopics(String prefix, int types) {
621    return topicHandlesToTopics(NetworkTablesJNI.getTopics(m_handle, prefix, types));
622  }
623
624  /**
625   * Get published topics starting with the given prefix. The results are optionally filtered by
626   * string prefix and data type to only return a subset of all topics.
627   *
628   * @param prefix topic name required prefix; only topics whose name starts with this string are
629   *     returned
630   * @param types array of data type strings
631   * @return Array of topic information.
632   */
633  public Topic[] getTopics(String prefix, String[] types) {
634    return topicHandlesToTopics(NetworkTablesJNI.getTopicsStr(m_handle, prefix, types));
635  }
636
637  /**
638   * Get information about all topics.
639   *
640   * @return Array of topic information.
641   */
642  public TopicInfo[] getTopicInfo() {
643    return NetworkTablesJNI.getTopicInfos(this, m_handle, "", 0);
644  }
645
646  /**
647   * Get information about topics starting with the given prefix. The results are optionally
648   * filtered by string prefix to only return a subset of all topics.
649   *
650   * @param prefix topic name required prefix; only topics whose name starts with this string are
651   *     returned
652   * @return Array of topic information.
653   */
654  public TopicInfo[] getTopicInfo(String prefix) {
655    return NetworkTablesJNI.getTopicInfos(this, m_handle, prefix, 0);
656  }
657
658  /**
659   * Get information about topics starting with the given prefix. The results are optionally
660   * filtered by string prefix and data type to only return a subset of all topics.
661   *
662   * @param prefix topic name required prefix; only topics whose name starts with this string are
663   *     returned
664   * @param types bitmask of data types; 0 is treated as a "don't care"
665   * @return Array of topic information.
666   */
667  public TopicInfo[] getTopicInfo(String prefix, int types) {
668    return NetworkTablesJNI.getTopicInfos(this, m_handle, prefix, types);
669  }
670
671  /**
672   * Get information about topics starting with the given prefix. The results are optionally
673   * filtered by string prefix and data type to only return a subset of all topics.
674   *
675   * @param prefix topic name required prefix; only topics whose name starts with this string are
676   *     returned
677   * @param types array of data type strings
678   * @return Array of topic information.
679   */
680  public TopicInfo[] getTopicInfo(String prefix, String[] types) {
681    return NetworkTablesJNI.getTopicInfosStr(this, m_handle, prefix, types);
682  }
683
684  /* Cache of created entries. */
685  private final ConcurrentMap<String, NetworkTableEntry> m_entries = new ConcurrentHashMap<>();
686
687  /**
688   * Gets the entry for a key.
689   *
690   * @param name Key
691   * @return Network table entry.
692   */
693  public NetworkTableEntry getEntry(String name) {
694    NetworkTableEntry entry = m_entries.get(name);
695    if (entry == null) {
696      entry = new NetworkTableEntry(this, NetworkTablesJNI.getEntry(m_handle, name));
697      NetworkTableEntry oldEntry = m_entries.putIfAbsent(name, entry);
698      if (oldEntry != null) {
699        entry = oldEntry;
700      }
701    }
702    return entry;
703  }
704
705  /* Cache of created topics. */
706  private final ConcurrentMap<String, Topic> m_topics = new ConcurrentHashMap<>();
707  private final ConcurrentMap<Integer, Topic> m_topicsByHandle = new ConcurrentHashMap<>();
708
709  Topic getCachedTopic(String name) {
710    Topic topic = m_topics.get(name);
711    if (topic == null) {
712      int handle = NetworkTablesJNI.getTopic(m_handle, name);
713      topic = new Topic(this, handle);
714      Topic oldTopic = m_topics.putIfAbsent(name, topic);
715      if (oldTopic != null) {
716        topic = oldTopic;
717      }
718      // also cache by handle
719      m_topicsByHandle.putIfAbsent(handle, topic);
720    }
721    return topic;
722  }
723
724  Topic getCachedTopic(int handle) {
725    Topic topic = m_topicsByHandle.get(handle);
726    if (topic == null) {
727      topic = new Topic(this, handle);
728      Topic oldTopic = m_topicsByHandle.putIfAbsent(handle, topic);
729      if (oldTopic != null) {
730        topic = oldTopic;
731      }
732    }
733    return topic;
734  }
735
736  /* Cache of created tables. */
737  private final ConcurrentMap<String, NetworkTable> m_tables = new ConcurrentHashMap<>();
738
739  /**
740   * Gets the table with the specified key.
741   *
742   * @param key the key name
743   * @return The network table
744   */
745  public NetworkTable getTable(String key) {
746    // prepend leading / if not present
747    String theKey;
748    if (key.isEmpty() || "/".equals(key)) {
749      theKey = "";
750    } else if (key.charAt(0) == NetworkTable.PATH_SEPARATOR) {
751      theKey = key;
752    } else {
753      theKey = NetworkTable.PATH_SEPARATOR + key;
754    }
755
756    // cache created tables
757    NetworkTable table = m_tables.get(theKey);
758    if (table == null) {
759      table = new NetworkTable(this, theKey);
760      NetworkTable oldTable = m_tables.putIfAbsent(theKey, table);
761      if (oldTable != null) {
762        table = oldTable;
763      }
764    }
765    return table;
766  }
767
768  /*
769   * Callback Creation Functions
770   */
771
772  private static class ListenerStorage implements AutoCloseable {
773    private final ReentrantLock m_lock = new ReentrantLock();
774    private final Map<Integer, Consumer<NetworkTableEvent>> m_listeners = new HashMap<>();
775
776    @SuppressWarnings("PMD.SingularField")
777    private Thread m_thread;
778
779    private int m_poller;
780    private boolean m_waitQueue;
781    private final Event m_waitQueueEvent = new Event();
782    private final Condition m_waitQueueCond = m_lock.newCondition();
783    private final NetworkTableInstance m_inst;
784
785    ListenerStorage(NetworkTableInstance inst) {
786      m_inst = inst;
787    }
788
789    int add(
790        String[] prefixes,
791        EnumSet<NetworkTableEvent.Kind> eventKinds,
792        Consumer<NetworkTableEvent> listener) {
793      m_lock.lock();
794      try {
795        if (m_poller == 0) {
796          m_poller = NetworkTablesJNI.createListenerPoller(m_inst.getHandle());
797          startThread();
798        }
799        int h = NetworkTablesJNI.addListener(m_poller, prefixes, eventKinds);
800        m_listeners.put(h, listener);
801        return h;
802      } finally {
803        m_lock.unlock();
804      }
805    }
806
807    int add(
808        int handle,
809        EnumSet<NetworkTableEvent.Kind> eventKinds,
810        Consumer<NetworkTableEvent> listener) {
811      m_lock.lock();
812      try {
813        if (m_poller == 0) {
814          m_poller = NetworkTablesJNI.createListenerPoller(m_inst.getHandle());
815          startThread();
816        }
817        int h = NetworkTablesJNI.addListener(m_poller, handle, eventKinds);
818        m_listeners.put(h, listener);
819        return h;
820      } finally {
821        m_lock.unlock();
822      }
823    }
824
825    int addLogger(int minLevel, int maxLevel, Consumer<NetworkTableEvent> listener) {
826      m_lock.lock();
827      try {
828        if (m_poller == 0) {
829          m_poller = NetworkTablesJNI.createListenerPoller(m_inst.getHandle());
830          startThread();
831        }
832        int h = NetworkTablesJNI.addLogger(m_poller, minLevel, maxLevel);
833        m_listeners.put(h, listener);
834        return h;
835      } finally {
836        m_lock.unlock();
837      }
838    }
839
840    void remove(int listener) {
841      m_lock.lock();
842      try {
843        m_listeners.remove(listener);
844      } finally {
845        m_lock.unlock();
846      }
847      NetworkTablesJNI.removeListener(listener);
848    }
849
850    @Override
851    public void close() {
852      if (m_poller != 0) {
853        NetworkTablesJNI.destroyListenerPoller(m_poller);
854      }
855      m_poller = 0;
856    }
857
858    private void startThread() {
859      m_thread =
860          new Thread(
861              () -> {
862                boolean wasInterrupted = false;
863                int[] handles = new int[] { m_poller, m_waitQueueEvent.getHandle() };
864                while (!Thread.interrupted()) {
865                  try {
866                    WPIUtilJNI.waitForObjects(handles);
867                  } catch (InterruptedException ex) {
868                    m_lock.lock();
869                    try {
870                      if (m_waitQueue) {
871                        m_waitQueue = false;
872                        m_waitQueueCond.signalAll();
873                      }
874                    } finally {
875                      m_lock.unlock();
876                    }
877                    Thread.currentThread().interrupt();
878                    // don't try to destroy poller, as its handle is likely no longer valid
879                    wasInterrupted = true;
880                    break;
881                  }
882                  for (NetworkTableEvent event :
883                      NetworkTablesJNI.readListenerQueue(m_inst, m_poller)) {
884                    Consumer<NetworkTableEvent> listener;
885                    m_lock.lock();
886                    try {
887                      listener = m_listeners.get(event.listener);
888                    } finally {
889                      m_lock.unlock();
890                    }
891                    if (listener != null) {
892                      try {
893                        listener.accept(event);
894                      } catch (Throwable throwable) {
895                        System.err.println(
896                            "Unhandled exception during listener callback: "
897                            + throwable.toString());
898                        throwable.printStackTrace();
899                      }
900                    }
901                  }
902                  m_lock.lock();
903                  try {
904                    if (m_waitQueue) {
905                      m_waitQueue = false;
906                      m_waitQueueCond.signalAll();
907                    }
908                  } finally {
909                    m_lock.unlock();
910                  }
911                }
912                m_lock.lock();
913                try {
914                  if (!wasInterrupted) {
915                    NetworkTablesJNI.destroyListenerPoller(m_poller);
916                  }
917                  m_poller = 0;
918                } finally {
919                  m_lock.unlock();
920                }
921              },
922              "NTListener");
923      m_thread.setDaemon(true);
924      m_thread.start();
925    }
926
927    boolean waitForQueue(double timeout) {
928      m_lock.lock();
929      try {
930        if (m_poller != 0) {
931          m_waitQueue = true;
932          m_waitQueueEvent.set();
933          while (m_waitQueue) {
934            try {
935              if (timeout < 0) {
936                m_waitQueueCond.await();
937              } else {
938                return m_waitQueueCond.await((long) (timeout * 1e9), TimeUnit.NANOSECONDS);
939              }
940            } catch (InterruptedException ex) {
941              Thread.currentThread().interrupt();
942              return true;
943            }
944          }
945        }
946      } finally {
947        m_lock.unlock();
948      }
949      return true;
950    }
951  }
952
953  private final ListenerStorage m_listeners = new ListenerStorage(this);
954
955  /**
956   * Remove a connection listener.
957   *
958   * @param listener Listener handle to remove
959   */
960  public void removeListener(int listener) {
961    m_listeners.remove(listener);
962  }
963
964  /**
965   * Wait for the listener queue to be empty. This is primarily useful for deterministic
966   * testing. This blocks until either the listener queue is empty (e.g. there are no
967   * more events that need to be passed along to callbacks or poll queues) or the timeout expires.
968   *
969   * @param timeout timeout, in seconds. Set to 0 for non-blocking behavior, or a negative value to
970   *     block indefinitely
971   * @return False if timed out, otherwise true.
972   */
973  public boolean waitForListenerQueue(double timeout) {
974    return m_listeners.waitForQueue(timeout);
975  }
976
977  /**
978   * Add a connection listener. The callback function is called asynchronously on a separate
979   * thread, so it's important to use synchronization or atomics when accessing any shared state
980   * from the callback function.
981   *
982   * @param immediateNotify Notify listener of all existing connections
983   * @param listener Listener to add
984   * @return Listener handle
985   */
986  public int addConnectionListener(
987      boolean immediateNotify, Consumer<NetworkTableEvent> listener) {
988    EnumSet<NetworkTableEvent.Kind> eventKinds = EnumSet.of(NetworkTableEvent.Kind.kConnection);
989    if (immediateNotify) {
990      eventKinds.add(NetworkTableEvent.Kind.kImmediate);
991    }
992    return m_listeners.add(m_handle, eventKinds, listener);
993  }
994
995  /**
996   * Add a time synchronization listener. The callback function is called asynchronously on a
997   * separate thread, so it's important to use synchronization or atomics when accessing any shared
998   * state from the callback function.
999   *
1000   * @param immediateNotify Notify listener of current time synchronization value
1001   * @param listener Listener to add
1002   * @return Listener handle
1003   */
1004  public int addTimeSyncListener(
1005      boolean immediateNotify, Consumer<NetworkTableEvent> listener) {
1006    EnumSet<NetworkTableEvent.Kind> eventKinds = EnumSet.of(NetworkTableEvent.Kind.kTimeSync);
1007    if (immediateNotify) {
1008      eventKinds.add(NetworkTableEvent.Kind.kImmediate);
1009    }
1010    return m_listeners.add(m_handle, eventKinds, listener);
1011  }
1012
1013  /**
1014   * Add a listener for changes on a particular topic. The callback function is called
1015   * asynchronously on a separate thread, so it's important to use synchronization or atomics when
1016   * accessing any shared state from the callback function.
1017   *
1018   * <p>This creates a corresponding internal subscriber with the lifetime of the
1019   * listener.
1020   *
1021   * @param topic Topic
1022   * @param eventKinds set of event kinds to listen to
1023   * @param listener Listener function
1024   * @return Listener handle
1025   */
1026  public int addListener(
1027      Topic topic,
1028      EnumSet<NetworkTableEvent.Kind> eventKinds,
1029      Consumer<NetworkTableEvent> listener) {
1030    if (topic.getInstance().getHandle() != m_handle) {
1031      throw new IllegalArgumentException("topic is not from this instance");
1032    }
1033    return m_listeners.add(topic.getHandle(), eventKinds, listener);
1034  }
1035
1036  /**
1037   * Add a listener for changes on a subscriber. The callback function is called
1038   * asynchronously on a separate thread, so it's important to use synchronization or atomics when
1039   * accessing any shared state from the callback function. This does NOT keep the subscriber
1040   * active.
1041   *
1042   * @param subscriber Subscriber
1043   * @param eventKinds set of event kinds to listen to
1044   * @param listener Listener function
1045   * @return Listener handle
1046   */
1047  public int addListener(
1048      Subscriber subscriber,
1049      EnumSet<NetworkTableEvent.Kind> eventKinds,
1050      Consumer<NetworkTableEvent> listener) {
1051    if (subscriber.getTopic().getInstance().getHandle() != m_handle) {
1052      throw new IllegalArgumentException("subscriber is not from this instance");
1053    }
1054    return m_listeners.add(subscriber.getHandle(), eventKinds, listener);
1055  }
1056
1057  /**
1058   * Add a listener for changes on a subscriber. The callback function is called
1059   * asynchronously on a separate thread, so it's important to use synchronization or atomics when
1060   * accessing any shared state from the callback function. This does NOT keep the subscriber
1061   * active.
1062   *
1063   * @param subscriber Subscriber
1064   * @param eventKinds set of event kinds to listen to
1065   * @param listener Listener function
1066   * @return Listener handle
1067   */
1068  public int addListener(
1069      MultiSubscriber subscriber,
1070      EnumSet<NetworkTableEvent.Kind> eventKinds,
1071      Consumer<NetworkTableEvent> listener) {
1072    if (subscriber.getInstance().getHandle() != m_handle) {
1073      throw new IllegalArgumentException("subscriber is not from this instance");
1074    }
1075    return m_listeners.add(subscriber.getHandle(), eventKinds, listener);
1076  }
1077
1078  /**
1079   * Add a listener for changes on an entry. The callback function is called
1080   * asynchronously on a separate thread, so it's important to use synchronization or atomics when
1081   * accessing any shared state from the callback function.
1082   *
1083   * @param entry Entry
1084   * @param eventKinds set of event kinds to listen to
1085   * @param listener Listener function
1086   * @return Listener handle
1087   */
1088  public int addListener(
1089      NetworkTableEntry entry,
1090      EnumSet<NetworkTableEvent.Kind> eventKinds,
1091      Consumer<NetworkTableEvent> listener) {
1092    if (entry.getTopic().getInstance().getHandle() != m_handle) {
1093      throw new IllegalArgumentException("entry is not from this instance");
1094    }
1095    return m_listeners.add(entry.getHandle(), eventKinds, listener);
1096  }
1097
1098  /**
1099   * Add a listener for changes to topics with names that start with any of the given
1100   * prefixes. The callback function is called asynchronously on a separate thread, so it's
1101   * important to use synchronization or atomics when accessing any shared state from the callback
1102   * function.
1103   *
1104   * <p>This creates a corresponding internal subscriber with the lifetime of the
1105   * listener.
1106   *
1107   * @param prefixes Topic name string prefixes
1108   * @param eventKinds set of event kinds to listen to
1109   * @param listener Listener function
1110   * @return Listener handle
1111   */
1112  public int addListener(
1113      String[] prefixes,
1114      EnumSet<NetworkTableEvent.Kind> eventKinds,
1115      Consumer<NetworkTableEvent> listener) {
1116    return m_listeners.add(prefixes, eventKinds, listener);
1117  }
1118
1119  /*
1120   * Client/Server Functions
1121   */
1122
1123  /**
1124   * Get the current network mode.
1125   *
1126   * @return Enum set of NetworkMode.
1127   */
1128  public EnumSet<NetworkMode> getNetworkMode() {
1129    int flags = NetworkTablesJNI.getNetworkMode(m_handle);
1130    EnumSet<NetworkMode> rv = EnumSet.noneOf(NetworkMode.class);
1131    for (NetworkMode mode : NetworkMode.values()) {
1132      if ((flags & mode.getValue()) != 0) {
1133        rv.add(mode);
1134      }
1135    }
1136    return rv;
1137  }
1138
1139  /**
1140   * Starts local-only operation. Prevents calls to startServer or startClient from taking effect.
1141   * Has no effect if startServer or startClient has already been called.
1142   */
1143  public void startLocal() {
1144    NetworkTablesJNI.startLocal(m_handle);
1145  }
1146
1147  /**
1148   * Stops local-only operation. startServer or startClient can be called after this call to start
1149   * a server or client.
1150   */
1151  public void stopLocal() {
1152    NetworkTablesJNI.stopLocal(m_handle);
1153  }
1154
1155  /**
1156   * Starts a server using the networktables.json as the persistent file, using the default
1157   * listening address and port.
1158   */
1159  public void startServer() {
1160    startServer("networktables.json");
1161  }
1162
1163  /**
1164   * Starts a server using the specified persistent filename, using the default listening address
1165   * and port.
1166   *
1167   * @param persistFilename the name of the persist file to use
1168   */
1169  public void startServer(String persistFilename) {
1170    startServer(persistFilename, "");
1171  }
1172
1173  /**
1174   * Starts a server using the specified filename and listening address, using the default port.
1175   *
1176   * @param persistFilename the name of the persist file to use
1177   * @param listenAddress the address to listen on, or empty to listen on any address
1178   */
1179  public void startServer(String persistFilename, String listenAddress) {
1180    startServer(persistFilename, listenAddress, kDefaultPort3, kDefaultPort4);
1181  }
1182
1183  /**
1184   * Starts a server using the specified filename, listening address, and port.
1185   *
1186   * @param persistFilename the name of the persist file to use
1187   * @param listenAddress the address to listen on, or empty to listen on any address
1188   * @param port3 port to communicate over (NT3)
1189   */
1190  public void startServer(String persistFilename, String listenAddress, int port3) {
1191    startServer(persistFilename, listenAddress, port3, kDefaultPort4);
1192  }
1193
1194  /**
1195   * Starts a server using the specified filename, listening address, and port.
1196   *
1197   * @param persistFilename the name of the persist file to use
1198   * @param listenAddress the address to listen on, or empty to listen on any address
1199   * @param port3 port to communicate over (NT3)
1200   * @param port4 port to communicate over (NT4)
1201   */
1202  public void startServer(String persistFilename, String listenAddress, int port3, int port4) {
1203    NetworkTablesJNI.startServer(m_handle, persistFilename, listenAddress, port3, port4);
1204  }
1205
1206  /** Stops the server if it is running. */
1207  public void stopServer() {
1208    NetworkTablesJNI.stopServer(m_handle);
1209  }
1210
1211  /**
1212   * Starts a NT3 client. Use SetServer or SetServerTeam to set the server name and port.
1213   *
1214   * @param identity network identity to advertise (cannot be empty string)
1215   */
1216  public void startClient3(String identity) {
1217    NetworkTablesJNI.startClient3(m_handle, identity);
1218  }
1219
1220  /**
1221   * Starts a NT4 client. Use SetServer or SetServerTeam to set the server name and port.
1222   *
1223   * @param identity network identity to advertise (cannot be empty string)
1224   */
1225  public void startClient4(String identity) {
1226    NetworkTablesJNI.startClient4(m_handle, identity);
1227  }
1228
1229  /** Stops the client if it is running. */
1230  public void stopClient() {
1231    NetworkTablesJNI.stopClient(m_handle);
1232  }
1233
1234  /**
1235   * Sets server address and port for client (without restarting client). Changes the port to the
1236   * default port.
1237   *
1238   * @param serverName server name
1239   */
1240  public void setServer(String serverName) {
1241    setServer(serverName, 0);
1242  }
1243
1244  /**
1245   * Sets server address and port for client (without restarting client).
1246   *
1247   * @param serverName server name
1248   * @param port port to communicate over (0=default)
1249   */
1250  public void setServer(String serverName, int port) {
1251    NetworkTablesJNI.setServer(m_handle, serverName, port);
1252  }
1253
1254  /**
1255   * Sets server addresses and port for client (without restarting client). Changes the port to the
1256   * default port. The client will attempt to connect to each server in round robin fashion.
1257   *
1258   * @param serverNames array of server names
1259   */
1260  public void setServer(String[] serverNames) {
1261    setServer(serverNames, 0);
1262  }
1263
1264  /**
1265   * Sets server addresses and port for client (without restarting client). The client will attempt
1266   * to connect to each server in round robin fashion.
1267   *
1268   * @param serverNames array of server names
1269   * @param port port to communicate over (0=default)
1270   */
1271  public void setServer(String[] serverNames, int port) {
1272    int[] ports = new int[serverNames.length];
1273    for (int i = 0; i < serverNames.length; i++) {
1274      ports[i] = port;
1275    }
1276    setServer(serverNames, ports);
1277  }
1278
1279  /**
1280   * Sets server addresses and ports for client (without restarting client). The client will
1281   * attempt to connect to each server in round robin fashion.
1282   *
1283   * @param serverNames array of server names
1284   * @param ports array of port numbers (0=default)
1285   */
1286  public void setServer(String[] serverNames, int[] ports) {
1287    NetworkTablesJNI.setServer(m_handle, serverNames, ports);
1288  }
1289
1290  /**
1291   * Sets server addresses and port for client (without restarting client). Changes the port to the
1292   * default port. The client will attempt to connect to each server in round robin fashion.
1293   *
1294   * @param team team number
1295   */
1296  public void setServerTeam(int team) {
1297    setServerTeam(team, 0);
1298  }
1299
1300  /**
1301   * Sets server addresses and port for client (without restarting client). Connects using commonly
1302   * known robot addresses for the specified team.
1303   *
1304   * @param team team number
1305   * @param port port to communicate over (0=default)
1306   */
1307  public void setServerTeam(int team, int port) {
1308    NetworkTablesJNI.setServerTeam(m_handle, team, port);
1309  }
1310
1311  /**
1312   * Disconnects the client if it's running and connected. This will automatically start
1313   * reconnection attempts to the current server list.
1314   */
1315  public void disconnect() {
1316    NetworkTablesJNI.disconnect(m_handle);
1317  }
1318
1319  /**
1320   * Starts requesting server address from Driver Station. This connects to the Driver Station
1321   * running on localhost to obtain the server IP address, and connects with the default port.
1322   */
1323  public void startDSClient() {
1324    startDSClient(0);
1325  }
1326
1327  /**
1328   * Starts requesting server address from Driver Station. This connects to the Driver Station
1329   * running on localhost to obtain the server IP address.
1330   *
1331   * @param port server port to use in combination with IP from DS (0=default)
1332   */
1333  public void startDSClient(int port) {
1334    NetworkTablesJNI.startDSClient(m_handle, port);
1335  }
1336
1337  /** Stops requesting server address from Driver Station. */
1338  public void stopDSClient() {
1339    NetworkTablesJNI.stopDSClient(m_handle);
1340  }
1341
1342  /**
1343   * Flushes all updated values immediately to the local client/server. This does not flush to the
1344   * network.
1345   */
1346  public void flushLocal() {
1347    NetworkTablesJNI.flushLocal(m_handle);
1348  }
1349
1350  /**
1351   * Flushes all updated values immediately to the network. Note: This is rate-limited to protect
1352   * the network from flooding. This is primarily useful for synchronizing network updates with
1353   * user code.
1354   */
1355  public void flush() {
1356    NetworkTablesJNI.flush(m_handle);
1357  }
1358
1359  /**
1360   * Gets information on the currently established network connections. If operating as a client,
1361   * this will return either zero or one values.
1362   *
1363   * @return array of connection information
1364   */
1365  public ConnectionInfo[] getConnections() {
1366    return NetworkTablesJNI.getConnections(m_handle);
1367  }
1368
1369  /**
1370   * Return whether or not the instance is connected to another node.
1371   *
1372   * @return True if connected.
1373   */
1374  public boolean isConnected() {
1375    return NetworkTablesJNI.isConnected(m_handle);
1376  }
1377
1378  /**
1379   * Get the time offset between server time and local time. Add this value to local time to get
1380   * the estimated equivalent server time. In server mode, this always returns 0. In client mode,
1381   * this returns the time offset only if the client and server are connected and have exchanged
1382   * synchronization messages. Note the time offset may change over time as it is periodically
1383   * updated; to receive updates as events, add a listener to the "time sync" event.
1384   *
1385   * @return Time offset in microseconds (optional)
1386   */
1387  public OptionalLong getServerTimeOffset() {
1388    return NetworkTablesJNI.getServerTimeOffset(m_handle);
1389  }
1390
1391  /**
1392   * Starts logging entry changes to a DataLog.
1393   *
1394   * @param log data log object; lifetime must extend until StopEntryDataLog is called or the
1395   *     instance is destroyed
1396   * @param prefix only store entries with names that start with this prefix; the prefix is not
1397   *     included in the data log entry name
1398   * @param logPrefix prefix to add to data log entry names
1399   * @return Data logger handle
1400   */
1401  public int startEntryDataLog(DataLog log, String prefix, String logPrefix) {
1402    return NetworkTablesJNI.startEntryDataLog(m_handle, log, prefix, logPrefix);
1403  }
1404
1405  /**
1406   * Stops logging entry changes to a DataLog.
1407   *
1408   * @param logger data logger handle
1409   */
1410  public static void stopEntryDataLog(int logger) {
1411    NetworkTablesJNI.stopEntryDataLog(logger);
1412  }
1413
1414  /**
1415   * Starts logging connection changes to a DataLog.
1416   *
1417   * @param log data log object; lifetime must extend until StopConnectionDataLog is called or the
1418   *     instance is destroyed
1419   * @param name data log entry name
1420   * @return Data logger handle
1421   */
1422  public int startConnectionDataLog(DataLog log, String name) {
1423    return NetworkTablesJNI.startConnectionDataLog(m_handle, log, name);
1424  }
1425
1426  /**
1427   * Stops logging connection changes to a DataLog.
1428   *
1429   * @param logger data logger handle
1430   */
1431  public static void stopConnectionDataLog(int logger) {
1432    NetworkTablesJNI.stopConnectionDataLog(logger);
1433  }
1434
1435  /**
1436   * Add logger callback function. By default, log messages are sent to stderr; this function sends
1437   * log messages with the specified levels to the provided callback function instead. The callback
1438   * function will only be called for log messages with level greater than or equal to minLevel and
1439   * less than or equal to maxLevel; messages outside this range will be silently ignored.
1440   *
1441   * @param minLevel minimum log level
1442   * @param maxLevel maximum log level
1443   * @param func callback function
1444   * @return Listener handle
1445   */
1446  public int addLogger(int minLevel, int maxLevel, Consumer<NetworkTableEvent> func) {
1447    return m_listeners.addLogger(minLevel, maxLevel, func);
1448  }
1449
1450  /**
1451   * Returns whether there is a data schema already registered with the given name that this
1452   * instance has published. This does NOT perform a check as to whether the schema has already
1453   * been published by another node on the network.
1454   *
1455   * @param name Name (the string passed as the data type for topics using this schema)
1456   * @return True if schema already registered
1457   */
1458  public boolean hasSchema(String name) {
1459    return m_schemas.containsKey("/.schema/" + name);
1460  }
1461
1462  /**
1463   * Registers a data schema. Data schemas provide information for how a certain data type string
1464   * can be decoded. The type string of a data schema indicates the type of the schema itself (e.g.
1465   * "protobuf" for protobuf schemas, "struct" for struct schemas, etc). In NetworkTables, schemas
1466   * are published just like normal topics, with the name being generated from the provided name:
1467   * "/.schema/name". Duplicate calls to this function with the same name are silently ignored.
1468   *
1469   * @param name Name (the string passed as the data type for topics using this schema)
1470   * @param type Type of schema (e.g. "protobuf", "struct", etc)
1471   * @param schema Schema data
1472   */
1473  public void addSchema(String name, String type, byte[] schema) {
1474    m_schemas.computeIfAbsent("/.schema/" + name, k -> {
1475      RawPublisher pub = getRawTopic(k).publishEx(type, "{\"retained\":true}");
1476      pub.setDefault(schema);
1477      return pub;
1478    });
1479  }
1480
1481  /**
1482   * Registers a data schema. Data schemas provide information for how a certain data type string
1483   * can be decoded. The type string of a data schema indicates the type of the schema itself (e.g.
1484   * "protobuf" for protobuf schemas, "struct" for struct schemas, etc). In NetworkTables, schemas
1485   * are published just like normal topics, with the name being generated from the provided name:
1486   * "/.schema/name". Duplicate calls to this function with the same name are silently ignored.
1487   *
1488   * @param name Name (the string passed as the data type for topics using this schema)
1489   * @param type Type of schema (e.g. "protobuf", "struct", etc)
1490   * @param schema Schema data
1491   */
1492  public void addSchema(String name, String type, String schema) {
1493    m_schemas.computeIfAbsent("/.schema/" + name, k -> {
1494      RawPublisher pub = getRawTopic(k).publishEx(type, "{\"retained\":true}");
1495      pub.setDefault(StandardCharsets.UTF_8.encode(schema));
1496      return pub;
1497    });
1498  }
1499
1500  /**
1501   * Registers a protobuf schema. Duplicate calls to this function with the same name are silently
1502   * ignored.
1503   *
1504   * @param proto protobuf serialization object
1505   */
1506  public void addSchema(Protobuf<?, ?> proto) {
1507    proto.forEachDescriptor(
1508        this::hasSchema,
1509        (typeString, schema) -> addSchema(typeString, "proto:FileDescriptorProto", schema));
1510  }
1511
1512  /**
1513   * Registers a struct schema. Duplicate calls to this function with the same name are silently
1514   * ignored.
1515   *
1516   * @param struct struct serialization object
1517   */
1518  public void addSchema(Struct<?> struct) {
1519    addSchemaImpl(struct, new HashSet<>());
1520  }
1521
1522  @Override
1523  public boolean equals(Object other) {
1524    return other == this || other instanceof NetworkTableInstance inst && m_handle == inst.m_handle;
1525  }
1526
1527  @Override
1528  public int hashCode() {
1529    return m_handle;
1530  }
1531
1532  private void addSchemaImpl(Struct<?> struct, Set<String> seen) {
1533    String typeString = struct.getTypeString();
1534    if (hasSchema(typeString)) {
1535      return;
1536    }
1537    if (!seen.add(typeString)) {
1538      throw new UnsupportedOperationException(typeString + ": circular reference with " + seen);
1539    }
1540    addSchema(typeString, "structschema", struct.getSchema());
1541    for (Struct<?> inner : struct.getNested()) {
1542      addSchemaImpl(inner, seen);
1543    }
1544    seen.remove(typeString);
1545  }
1546
1547  private boolean m_owned;
1548  private int m_handle;
1549  private final ConcurrentMap<String, RawPublisher> m_schemas = new ConcurrentHashMap<>();
1550}