diff --git a/rcljava/CMakeLists.txt b/rcljava/CMakeLists.txt index 8f10aeb4..afa2836b 100644 --- a/rcljava/CMakeLists.txt +++ b/rcljava/CMakeLists.txt @@ -143,9 +143,7 @@ set(${PROJECT_NAME}_sources "src/main/java/org/ros2/rcljava/events/PublisherEventStatus.java" "src/main/java/org/ros2/rcljava/events/SubscriptionEventStatus.java" "src/main/java/org/ros2/rcljava/graph/NameAndTypes.java" - "src/main/java/org/ros2/rcljava/publisher/statuses/LivelinessLost.java" - "src/main/java/org/ros2/rcljava/publisher/statuses/OfferedDeadlineMissed.java" - "src/main/java/org/ros2/rcljava/publisher/statuses/OfferedQosIncompatible.java" + "src/main/java/org/ros2/rcljava/graph/NodeNameInfo.java" "src/main/java/org/ros2/rcljava/executors/AnyExecutable.java" "src/main/java/org/ros2/rcljava/executors/BaseExecutor.java" "src/main/java/org/ros2/rcljava/executors/Executor.java" @@ -173,6 +171,9 @@ set(${PROJECT_NAME}_sources "src/main/java/org/ros2/rcljava/parameters/service/ParameterServiceImpl.java" "src/main/java/org/ros2/rcljava/publisher/Publisher.java" "src/main/java/org/ros2/rcljava/publisher/PublisherImpl.java" + "src/main/java/org/ros2/rcljava/publisher/statuses/LivelinessLost.java" + "src/main/java/org/ros2/rcljava/publisher/statuses/OfferedDeadlineMissed.java" + "src/main/java/org/ros2/rcljava/publisher/statuses/OfferedQosIncompatible.java" "src/main/java/org/ros2/rcljava/qos/policies/Durability.java" "src/main/java/org/ros2/rcljava/qos/policies/History.java" "src/main/java/org/ros2/rcljava/qos/policies/Liveliness.java" diff --git a/rcljava/include/org_ros2_rcljava_node_NodeImpl.h b/rcljava/include/org_ros2_rcljava_node_NodeImpl.h index 8f25d1f7..46729339 100644 --- a/rcljava/include/org_ros2_rcljava_node_NodeImpl.h +++ b/rcljava/include/org_ros2_rcljava_node_NodeImpl.h @@ -92,6 +92,15 @@ JNIEXPORT jlong JNICALL Java_org_ros2_rcljava_node_NodeImpl_nativeCreateTimerHandle( JNIEnv *, jclass, jlong, jlong, jlong); +/* + * Class: org_ros2_rcljava_node_NodeImpl + * Method: nativeGetNodeNames + * Signature: (JLjava/util/List;)V + */ +JNIEXPORT void +JNICALL Java_org_ros2_rcljava_node_NodeImpl_nativeGetNodeNames( + JNIEnv *, jclass, jlong, jobject); + /* * Class: org_ros2_rcljava_node_NodeImpl * Method: nativeGetTopicNamesAndTypes diff --git a/rcljava/src/main/cpp/org_ros2_rcljava_node_NodeImpl.cpp b/rcljava/src/main/cpp/org_ros2_rcljava_node_NodeImpl.cpp index 726f48a2..e9b8378b 100644 --- a/rcljava/src/main/cpp/org_ros2_rcljava_node_NodeImpl.cpp +++ b/rcljava/src/main/cpp/org_ros2_rcljava_node_NodeImpl.cpp @@ -254,6 +254,84 @@ Java_org_ros2_rcljava_node_NodeImpl_nativeCreateTimerHandle( return jtimer; } +JNIEXPORT void JNICALL +Java_org_ros2_rcljava_node_NodeImpl_nativeGetNodeNames( + JNIEnv * env, jclass, jlong handle, jobject jnode_names_info) +{ + rcl_node_t * node = reinterpret_cast(handle); + if (!node) { + rcljava_throw_exception(env, "java/lang/IllegalArgumentException", "node handle is NULL"); + return; + } + + jclass list_clazz = env->GetObjectClass(jnode_names_info); + jmethodID list_add_mid = env->GetMethodID(list_clazz, "add", "(Ljava/lang/Object;)Z"); + RCLJAVA_COMMON_CHECK_FOR_EXCEPTION(env); + jclass node_info_clazz = env->FindClass("org/ros2/rcljava/graph/NodeNameInfo"); + RCLJAVA_COMMON_CHECK_FOR_EXCEPTION(env); + jmethodID node_info_init_mid = env->GetMethodID(node_info_clazz, "", "()V"); + RCLJAVA_COMMON_CHECK_FOR_EXCEPTION(env); + jfieldID name_fid = env->GetFieldID(node_info_clazz, "name", "Ljava/lang/String;"); + RCLJAVA_COMMON_CHECK_FOR_EXCEPTION(env); + jfieldID namespace_fid = env->GetFieldID(node_info_clazz, "namespace", "Ljava/lang/String;"); + RCLJAVA_COMMON_CHECK_FOR_EXCEPTION(env); + jfieldID enclave_fid = env->GetFieldID(node_info_clazz, "enclave", "Ljava/lang/String;"); + RCLJAVA_COMMON_CHECK_FOR_EXCEPTION(env); + + rcl_allocator_t allocator = rcl_get_default_allocator(); + rcutils_string_array_t node_names = rcutils_get_zero_initialized_string_array(); + rcutils_string_array_t node_namespaces = rcutils_get_zero_initialized_string_array(); + rcutils_string_array_t enclaves = rcutils_get_zero_initialized_string_array(); + + rcl_ret_t ret = rcl_get_node_names_with_enclaves( + node, + allocator, + &node_names, + &node_namespaces, + &enclaves); + RCLJAVA_COMMON_THROW_FROM_RCL(env, ret, "rcl_get_node_names_with_enclaves failed"); + auto on_scope_exit = rcpputils::make_scope_exit( + [pnames = &node_names, pnamespaces = &node_namespaces, penclaves = &enclaves, env]() { + rcl_ret_t ret = rcutils_string_array_fini(pnames); + if (!env->ExceptionCheck() && RCL_RET_OK != ret) { + rcljava_throw_rclexception(env, ret, "failed to fini node names string array"); + } + ret = rcutils_string_array_fini(pnamespaces); + if (!env->ExceptionCheck() && RCL_RET_OK != ret) { + rcljava_throw_rclexception(env, ret, "failed to fini node namespaces string array"); + } + ret = rcutils_string_array_fini(penclaves); + if (!env->ExceptionCheck() && RCL_RET_OK != ret) { + rcljava_throw_rclexception(env, ret, "failed to fini enclaves string array"); + } + } + ); + + if (node_names.size != node_namespaces.size || node_names.size != enclaves.size) { + rcljava_throw_exception( + env, + "java/lang/IllegalStateException", + "names, namespaces and enclaves array leghts don't match"); + return; + } + + for (size_t i = 0; i < node_names.size; i++) { + jstring jnode_name = env->NewStringUTF(node_names.data[i]); + RCLJAVA_COMMON_CHECK_FOR_EXCEPTION(env); + jstring jnode_namespace = env->NewStringUTF(node_namespaces.data[i]); + RCLJAVA_COMMON_CHECK_FOR_EXCEPTION(env); + jstring jenclave = env->NewStringUTF(enclaves.data[i]); + RCLJAVA_COMMON_CHECK_FOR_EXCEPTION(env); + jobject jitem = env->NewObject(node_info_clazz, node_info_init_mid); + RCLJAVA_COMMON_CHECK_FOR_EXCEPTION(env); + env->SetObjectField(jitem, name_fid, jnode_name); + env->SetObjectField(jitem, namespace_fid, jnode_namespace); + env->SetObjectField(jitem, enclave_fid, jenclave); + env->CallBooleanMethod(jnode_names_info, list_add_mid, jitem); + RCLJAVA_COMMON_CHECK_FOR_EXCEPTION(env); + } +} + JNIEXPORT void JNICALL Java_org_ros2_rcljava_node_NodeImpl_nativeGetTopicNamesAndTypes( JNIEnv * env, jclass, jlong handle, jobject jnames_and_types) diff --git a/rcljava/src/main/java/org/ros2/rcljava/graph/NodeNameInfo.java b/rcljava/src/main/java/org/ros2/rcljava/graph/NodeNameInfo.java new file mode 100644 index 00000000..2cd29806 --- /dev/null +++ b/rcljava/src/main/java/org/ros2/rcljava/graph/NodeNameInfo.java @@ -0,0 +1,68 @@ +/* Copyright 2020 Open Source Robotics Foundation, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.ros2.rcljava.graph; + +import java.util.Objects; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.ros2.rcljava.common.JNIUtils; + +/** + * Class that represents the queried information of a node in the graph. + */ +public class NodeNameInfo { + /// The node name + public String name; + /// The node namespace + public String namespace; + /// The security enclave of the node. + /** + * For further details, see: + * @{link http://design.ros2.org/articles/ros2_security_enclaves.html} + */ + public String enclave; + + /// Constructor + public NodeNameInfo(final String name, final String namespace, final String enclave) { + this.name = name; + this.namespace = namespace; + this.enclave = enclave; + } + + /// Default constructor, used from jni. + private NodeNameInfo() {} + + @Override + public boolean equals(Object o) { + if (o == this) { + return true; + } + if (!(o instanceof NodeNameInfo)) { + return false; + } + NodeNameInfo other = (NodeNameInfo) o; + return Objects.equals(this.name, other.name) && + Objects.equals(this.namespace, other.namespace) && + Objects.equals(this.enclave, other.enclave); + } + + @Override + public int hashCode() { + return Objects.hash(this.name, this.namespace, this.enclave); + } +} diff --git a/rcljava/src/main/java/org/ros2/rcljava/node/Node.java b/rcljava/src/main/java/org/ros2/rcljava/node/Node.java index 42c1281b..e86b3ab5 100644 --- a/rcljava/src/main/java/org/ros2/rcljava/node/Node.java +++ b/rcljava/src/main/java/org/ros2/rcljava/node/Node.java @@ -26,6 +26,7 @@ import org.ros2.rcljava.consumers.TriConsumer; import org.ros2.rcljava.graph.EndpointInfo; import org.ros2.rcljava.graph.NameAndTypes; +import org.ros2.rcljava.graph.NodeNameInfo; import org.ros2.rcljava.interfaces.Disposable; import org.ros2.rcljava.interfaces.MessageDefinition; import org.ros2.rcljava.interfaces.ServiceDefinition; @@ -552,6 +553,12 @@ Client createClient(final Class serviceType, */ rcl_interfaces.msg.ListParametersResult listParameters(List prefixes, long depth); + /** + * Returns a collection of node names that were detected in the ROS graph. + * See @{link NodeNameInfo} for more information about the return value. + */ + Collection getNodeNames(); + /** * Return the topics names and types that were detected in the graph. * See @{link graph#NameAndTypes} for more information about the returned value. diff --git a/rcljava/src/main/java/org/ros2/rcljava/node/NodeImpl.java b/rcljava/src/main/java/org/ros2/rcljava/node/NodeImpl.java index 2736e686..3d5681f0 100644 --- a/rcljava/src/main/java/org/ros2/rcljava/node/NodeImpl.java +++ b/rcljava/src/main/java/org/ros2/rcljava/node/NodeImpl.java @@ -22,6 +22,7 @@ import org.ros2.rcljava.concurrent.Callback; import org.ros2.rcljava.consumers.Consumer; import org.ros2.rcljava.consumers.TriConsumer; +import org.ros2.rcljava.graph.NodeNameInfo; import org.ros2.rcljava.contexts.Context; import org.ros2.rcljava.graph.EndpointInfo; import org.ros2.rcljava.graph.NameAndTypes; @@ -741,6 +742,14 @@ public rcl_interfaces.msg.ListParametersResult listParameters( } } + public final Collection getNodeNames() { + ArrayList nodeNames = new ArrayList(); + nativeGetNodeNames(this.handle, nodeNames); + return nodeNames; + } + + private native static final void nativeGetNodeNames(long handle, ArrayList nodeNames); + public final Collection getTopicNamesAndTypes() { Collection namesAndTypes = new ArrayList(); nativeGetTopicNamesAndTypes(this.handle, namesAndTypes); diff --git a/rcljava/src/test/java/org/ros2/rcljava/node/NodeTest.java b/rcljava/src/test/java/org/ros2/rcljava/node/NodeTest.java index 4ab40171..6ec91f6d 100644 --- a/rcljava/src/test/java/org/ros2/rcljava/node/NodeTest.java +++ b/rcljava/src/test/java/org/ros2/rcljava/node/NodeTest.java @@ -45,6 +45,7 @@ import org.ros2.rcljava.executors.SingleThreadedExecutor; import org.ros2.rcljava.graph.EndpointInfo; import org.ros2.rcljava.graph.NameAndTypes; +import org.ros2.rcljava.graph.NodeNameInfo; import org.ros2.rcljava.node.Node; import org.ros2.rcljava.publisher.Publisher; import org.ros2.rcljava.qos.policies.Reliability; @@ -867,6 +868,64 @@ public Node getNode() { assertEquals(0, subscriptionOne.getHandle()); subscriptionTwo.dispose(); assertEquals(0, subscriptionTwo.getHandle()); + publisherNode.dispose(); + assertEquals(0, publisherNode.getHandle()); + subscriptionNodeOne.dispose(); + assertEquals(0, subscriptionNodeOne.getHandle()); + subscriptionNodeTwo.dispose(); + assertEquals(0, subscriptionNodeTwo.getHandle()); + } + + @Test + public final void testGetNodeNames() throws Exception { + final Node node1 = RCLJava.createNode("test_get_node_names_1"); + final Node node2 = RCLJava.createNode("test_get_node_names_2"); + final Node node3 = RCLJava.createNode("test_get_node_names_3"); + + Consumer> validateNodeNameInfo = + new Consumer>() { + public void accept(final Collection nodeNamesInfo) { + assertEquals(4, nodeNamesInfo.size()); + assertTrue( + "node 'test_node' was not discovered", + nodeNamesInfo.contains(new NodeNameInfo("test_node", "/", "/"))); + assertTrue( + "node 'test_get_node_names_1' was not discovered", + nodeNamesInfo.contains(new NodeNameInfo("test_get_node_names_1", "/", "/"))); + assertTrue( + "node 'test_get_node_names_2' was not discovered", + nodeNamesInfo.contains(new NodeNameInfo("test_get_node_names_1", "/", "/"))); + assertTrue( + "node 'test_get_node_names_3' was not discovered", + nodeNamesInfo.contains(new NodeNameInfo("test_get_node_names_1", "/", "/"))); + } + }; + + long start = System.currentTimeMillis(); + boolean ok = false; + Collection nodeNamesInfo = null; + do { + nodeNamesInfo = this.node.getNodeNames(); + try { + validateNodeNameInfo.accept(nodeNamesInfo); + ok = true; + } catch (AssertionError err) { + // ignore here, it's going to be validated again at the end. + } + // TODO(ivanpauno): We could wait for the graph guard condition to be triggered if that + // would be available. + try { + TimeUnit.MILLISECONDS.sleep(100); + } catch (InterruptedException err) { + // ignore + } + } while (!ok && System.currentTimeMillis() < start + 1000); + assertNotNull(nodeNamesInfo); + validateNodeNameInfo.accept(nodeNamesInfo); + + node1.dispose(); + node2.dispose(); + node3.dispose(); } @Test