From 24d8ee7756cf35ff75e939f3153371e73f9fbff3 Mon Sep 17 00:00:00 2001 From: Shivansh Arora Date: Tue, 7 May 2024 15:31:37 +0530 Subject: [PATCH 1/3] Add toXContent and fromXContent for DiscoveryNode and DiscoveryNodes Signed-off-by: Shivansh Arora --- .../common/transport/TransportAddress.java | 10 + .../cluster/node/DiscoveryNode.java | 96 +++++++++- .../cluster/node/DiscoveryNodes.java | 69 ++++++- .../cluster/node/DiscoveryNodeTests.java | 74 ++++++++ .../cluster/node/DiscoveryNodesTests.java | 172 ++++++++++++++++++ 5 files changed, 416 insertions(+), 5 deletions(-) diff --git a/libs/core/src/main/java/org/opensearch/core/common/transport/TransportAddress.java b/libs/core/src/main/java/org/opensearch/core/common/transport/TransportAddress.java index 3b5fbb7d76307..b7ab988ce0404 100644 --- a/libs/core/src/main/java/org/opensearch/core/common/transport/TransportAddress.java +++ b/libs/core/src/main/java/org/opensearch/core/common/transport/TransportAddress.java @@ -162,4 +162,14 @@ public String toString() { public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { return builder.value(toString()); } + + public static TransportAddress fromString(String address) throws UnknownHostException { + String[] addressSplit = address.split(":"); + if (addressSplit.length != 2) { + throw new IllegalArgumentException("address must be of the form [hostname/ip]:[port]"); + } + String hostname = addressSplit[0]; + int port = Integer.parseInt(addressSplit[1]); + return new TransportAddress(InetAddress.getByName(hostname), port); + } } diff --git a/server/src/main/java/org/opensearch/cluster/node/DiscoveryNode.java b/server/src/main/java/org/opensearch/cluster/node/DiscoveryNode.java index 5226e9570ac14..644e5f3de9352 100644 --- a/server/src/main/java/org/opensearch/cluster/node/DiscoveryNode.java +++ b/server/src/main/java/org/opensearch/cluster/node/DiscoveryNode.java @@ -33,6 +33,7 @@ package org.opensearch.cluster.node; import org.opensearch.Version; +import org.opensearch.cluster.metadata.Metadata; import org.opensearch.common.UUIDs; import org.opensearch.common.annotation.PublicApi; import org.opensearch.common.settings.Setting; @@ -43,6 +44,7 @@ import org.opensearch.core.common.transport.TransportAddress; import org.opensearch.core.xcontent.ToXContentFragment; import org.opensearch.core.xcontent.XContentBuilder; +import org.opensearch.core.xcontent.XContentParser; import org.opensearch.node.Node; import java.io.IOException; @@ -60,6 +62,8 @@ import java.util.stream.Collectors; import java.util.stream.Stream; +import static org.opensearch.cluster.metadata.Metadata.CONTEXT_MODE_API; +import static org.opensearch.cluster.metadata.Metadata.CONTEXT_MODE_PARAM; import static org.opensearch.node.NodeRoleSettings.NODE_ROLES_SETTING; import static org.opensearch.node.remotestore.RemoteStoreNodeAttribute.REMOTE_STORE_NODE_ATTRIBUTE_KEY_PREFIX; @@ -72,6 +76,14 @@ public class DiscoveryNode implements Writeable, ToXContentFragment { static final String COORDINATING_ONLY = "coordinating_only"; + static final String KEY_NAME = "name"; + static final String KEY_EPHEMERAL_ID = "ephemeral_id"; + static final String KEY_HOST_NAME = "host_name"; + static final String KEY_HOST_ADDRESS = "host_address"; + static final String KEY_TRANSPORT_ADDRESS = "transport_address"; + static final String KEY_ATTRIBUTES = "attributes"; + static final String KEY_VERSION = "version"; + static final String KEY_ROLES = "roles"; public static boolean nodeRequiresLocalStorage(Settings settings) { boolean localStorageEnable = Node.NODE_LOCAL_STORAGE_SETTING.get(settings); @@ -544,21 +556,97 @@ public String toString() { @Override public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + Metadata.XContentContext context = Metadata.XContentContext.valueOf(params.param(CONTEXT_MODE_PARAM, CONTEXT_MODE_API)); builder.startObject(getId()); - builder.field("name", getName()); - builder.field("ephemeral_id", getEphemeralId()); - builder.field("transport_address", getAddress().toString()); + builder.field(KEY_NAME, getName()); + builder.field(KEY_EPHEMERAL_ID, getEphemeralId()); + builder.field(KEY_TRANSPORT_ADDRESS, getAddress().toString()); - builder.startObject("attributes"); + builder.startObject(KEY_ATTRIBUTES); for (Map.Entry entry : attributes.entrySet()) { builder.field(entry.getKey(), entry.getValue()); } builder.endObject(); + if (context == Metadata.XContentContext.GATEWAY) { + builder.field(KEY_HOST_NAME, getHostName()); + builder.field(KEY_HOST_ADDRESS, getHostAddress()); + builder.field(KEY_VERSION, getVersion().toString()); + builder.startArray(KEY_ROLES); + for (DiscoveryNodeRole role : roles) { + builder.value(role.roleName()); + } + builder.endArray(); + } builder.endObject(); return builder; } + public static DiscoveryNode fromXContent(XContentParser parser, String nodeId) throws IOException { + if (parser.currentToken() == null) { + parser.nextToken(); + } + if (parser.currentToken() == XContentParser.Token.START_OBJECT) { + parser.nextToken(); + } + if (parser.currentToken() != XContentParser.Token.FIELD_NAME) { + throw new IllegalArgumentException("expected field name but got a " + parser.currentToken()); + } + String nodeName = null; + String hostName = null; + String hostAddress = null; + String ephemeralId = null; + TransportAddress transportAddress = null; + Map attributes = new HashMap<>(); + Set roles = new HashSet<>(); + Version version = null; + String currentFieldName = parser.currentName(); + // token should be start object at this point + // XContentParser.Token token = parser.nextToken(); + // if (token != XContentParser.Token.START_OBJECT) { + // throw new IllegalArgumentException("expected object but got a " + token); + // } + XContentParser.Token token; + while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { + if (token == XContentParser.Token.FIELD_NAME) { + currentFieldName = parser.currentName(); + } else if (token.isValue()) { + if (KEY_NAME.equals(currentFieldName)) { + nodeName = parser.text(); + } else if (KEY_EPHEMERAL_ID.equals(currentFieldName)) { + ephemeralId = parser.text(); + } else if (KEY_TRANSPORT_ADDRESS.equals(currentFieldName)) { + transportAddress = TransportAddress.fromString(parser.text()); + } else if (KEY_HOST_NAME.equals(currentFieldName)) { + hostName = parser.text(); + } else if (KEY_HOST_ADDRESS.equals(currentFieldName)) { + hostAddress = parser.text(); + } else if (KEY_VERSION.equals(currentFieldName)) { + version = Version.fromString(parser.text()); + } + } else if (token == XContentParser.Token.START_OBJECT) { + if (KEY_ATTRIBUTES.equals(currentFieldName)) { + while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { + if (token == XContentParser.Token.FIELD_NAME) { + currentFieldName = parser.currentName(); + } else if (token.isValue()) { + attributes.put(currentFieldName, parser.text()); + } + } + } + } else if (token == XContentParser.Token.START_ARRAY) { + if (KEY_ROLES.equals(currentFieldName)) { + while ((token = parser.nextToken()) != XContentParser.Token.END_ARRAY) { + roles.add(getRoleFromRoleName(parser.text())); + } + } + } else { + throw new IllegalArgumentException("unexpected token " + token); + } + } + return new DiscoveryNode(nodeName, nodeId, ephemeralId, hostName, hostAddress, transportAddress, attributes, roles, version); + } + private static Map rolesToMap(final Stream roles) { return Collections.unmodifiableMap(roles.collect(Collectors.toMap(DiscoveryNodeRole::roleName, Function.identity()))); } diff --git a/server/src/main/java/org/opensearch/cluster/node/DiscoveryNodes.java b/server/src/main/java/org/opensearch/cluster/node/DiscoveryNodes.java index 2ebcd8096893d..d281021624fd0 100644 --- a/server/src/main/java/org/opensearch/cluster/node/DiscoveryNodes.java +++ b/server/src/main/java/org/opensearch/cluster/node/DiscoveryNodes.java @@ -35,6 +35,7 @@ import org.opensearch.Version; import org.opensearch.cluster.AbstractDiffable; import org.opensearch.cluster.Diff; +import org.opensearch.cluster.metadata.Metadata; import org.opensearch.common.Booleans; import org.opensearch.common.Nullable; import org.opensearch.common.annotation.PublicApi; @@ -44,6 +45,9 @@ import org.opensearch.core.common.io.stream.StreamInput; import org.opensearch.core.common.io.stream.StreamOutput; import org.opensearch.core.common.transport.TransportAddress; +import org.opensearch.core.xcontent.ToXContentFragment; +import org.opensearch.core.xcontent.XContentBuilder; +import org.opensearch.core.xcontent.XContentParser; import java.io.IOException; import java.util.ArrayList; @@ -59,6 +63,9 @@ import java.util.stream.Stream; import java.util.stream.StreamSupport; +import static org.opensearch.cluster.metadata.Metadata.CONTEXT_MODE_API; +import static org.opensearch.cluster.metadata.Metadata.CONTEXT_MODE_PARAM; + /** * This class holds all {@link DiscoveryNode} in the cluster and provides convenience methods to * access, modify merge / diff discovery nodes. @@ -66,7 +73,7 @@ * @opensearch.api */ @PublicApi(since = "1.0.0") -public class DiscoveryNodes extends AbstractDiffable implements Iterable { +public class DiscoveryNodes extends AbstractDiffable implements Iterable, ToXContentFragment { public static final DiscoveryNodes EMPTY_NODES = builder().build(); @@ -566,6 +573,66 @@ public String toString() { return sb.toString(); } + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject("nodes"); + for (DiscoveryNode node : this) { + node.toXContent(builder, params); + } + builder.endObject(); + Metadata.XContentContext context = Metadata.XContentContext.valueOf(params.param(CONTEXT_MODE_PARAM, CONTEXT_MODE_API)); + if (context == Metadata.XContentContext.GATEWAY && clusterManagerNodeId != null) { + builder.field("cluster_manager", clusterManagerNodeId); + } + return builder; + } + + public static DiscoveryNodes fromXContent(XContentParser parser) throws IOException { + Builder builder = new Builder(); + if (parser.currentToken() == null) { + parser.nextToken(); + } + if (parser.currentToken() == XContentParser.Token.START_OBJECT) { + parser.nextToken(); + } + if (parser.currentToken() != XContentParser.Token.FIELD_NAME) { + throw new IllegalArgumentException("expected field name but got a " + parser.currentToken()); + } + XContentParser.Token token; + String currentFieldName = parser.currentName(); + while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { + if (token == XContentParser.Token.FIELD_NAME) { + currentFieldName = parser.currentName(); + } else if (token == XContentParser.Token.START_OBJECT) { + if ("nodes".equals(currentFieldName)) { + while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { + if (token == XContentParser.Token.FIELD_NAME) { + currentFieldName = parser.currentName(); + } else if (token == XContentParser.Token.START_OBJECT) { + String nodeId = currentFieldName; + DiscoveryNode node = DiscoveryNode.fromXContent(parser, nodeId); + builder.add(node); + } + } + } else { + throw new IllegalArgumentException("unexpected object field " + currentFieldName); + } + } else if (token.isValue()) { + if ("cluster_manager".equals(currentFieldName)) { + String clusterManagerNodeId = parser.text(); + if (clusterManagerNodeId != null) { + builder.clusterManagerNodeId(clusterManagerNodeId); + } + } else { + throw new IllegalArgumentException("unexpected value field " + currentFieldName); + } + } else { + throw new IllegalArgumentException("unexpected token " + token); + } + } + return builder.build(); + } + /** * Delta between nodes. * diff --git a/server/src/test/java/org/opensearch/cluster/node/DiscoveryNodeTests.java b/server/src/test/java/org/opensearch/cluster/node/DiscoveryNodeTests.java index c8a6fc76ce820..b3a4c7e34125e 100644 --- a/server/src/test/java/org/opensearch/cluster/node/DiscoveryNodeTests.java +++ b/server/src/test/java/org/opensearch/cluster/node/DiscoveryNodeTests.java @@ -33,15 +33,20 @@ package org.opensearch.cluster.node; import org.opensearch.Version; +import org.opensearch.cluster.metadata.Metadata; import org.opensearch.common.io.stream.BytesStreamOutput; import org.opensearch.common.settings.Setting; import org.opensearch.common.settings.Settings; +import org.opensearch.common.xcontent.json.JsonXContent; import org.opensearch.core.common.io.stream.StreamInput; import org.opensearch.core.common.transport.TransportAddress; +import org.opensearch.core.xcontent.ToXContent; +import org.opensearch.core.xcontent.XContentBuilder; import org.opensearch.node.remotestore.RemoteStoreNodeAttribute; import org.opensearch.test.NodeRoles; import org.opensearch.test.OpenSearchTestCase; +import java.io.IOException; import java.net.InetAddress; import java.util.Collections; import java.util.HashMap; @@ -53,6 +58,9 @@ import static java.util.Collections.emptyMap; import static java.util.Collections.emptySet; +import static java.util.Collections.singletonMap; +import static org.opensearch.cluster.metadata.Metadata.CONTEXT_MODE_API; +import static org.opensearch.cluster.metadata.Metadata.CONTEXT_MODE_GATEWAY; import static org.opensearch.test.NodeRoles.nonRemoteClusterClientNode; import static org.opensearch.test.NodeRoles.nonSearchNode; import static org.opensearch.test.NodeRoles.remoteClusterClientNode; @@ -249,4 +257,70 @@ public void testDiscoveryNodeIsSearchNode() { final DiscoveryNode node = DiscoveryNode.createLocal(settingWithSearchRole, buildNewFakeTransportAddress(), "node"); assertThat(node.isSearchNode(), equalTo(true)); } + + public void testToXContentInAPIMode() throws IOException { + final DiscoveryNode node = DiscoveryNode.createLocal( + Settings.EMPTY, + new TransportAddress(TransportAddress.META_ADDRESS, 9200), + "node_1" + ); + XContentBuilder builder = JsonXContent.contentBuilder().prettyPrint(); + builder.startObject(); + node.toXContent(builder, new ToXContent.MapParams(singletonMap(Metadata.CONTEXT_MODE_PARAM, CONTEXT_MODE_API))); + builder.endObject(); + + String expectedNodeAPUXContent = "{\n" + + " \"node_1\" : {\n" + + " \"name\" : \"" + + node.getName() + + "\",\n" + + " \"ephemeral_id\" : \"" + + node.getEphemeralId() + + "\",\n" + + " \"transport_address\" : \"0.0.0.0:9200\",\n" + + " \"attributes\" : { }\n" + + " }\n" + + "}"; + + assertEquals(expectedNodeAPUXContent, builder.toString()); + } + + public void testToXContentInGatewayMode() throws IOException { + final DiscoveryNode node = DiscoveryNode.createLocal( + Settings.EMPTY, + new TransportAddress(TransportAddress.META_ADDRESS, 9200), + "node_1" + ); + XContentBuilder builder = JsonXContent.contentBuilder().prettyPrint(); + builder.startObject(); + node.toXContent(builder, new ToXContent.MapParams(singletonMap(Metadata.CONTEXT_MODE_PARAM, CONTEXT_MODE_GATEWAY))); + builder.endObject(); + + String expectedNodeAPUXContent = "{\n" + + " \"node_1\" : {\n" + + " \"name\" : \"" + + node.getName() + + "\",\n" + + " \"ephemeral_id\" : \"" + + node.getEphemeralId() + + "\",\n" + + " \"transport_address\" : \"0.0.0.0:9200\",\n" + + " \"attributes\" : { },\n" + + " \"host_name\" : \"0.0.0.0\",\n" + + " \"host_address\" : \"0.0.0.0\",\n" + + " \"version\" : \"" + + node.getVersion() + + "\",\n" + + " \"roles\" : [\n" + + " \"cluster_manager\",\n" + + " \"data\",\n" + + " \"ingest\",\n" + + " \"remote_cluster_client\"\n" + + " ]\n" + + " }\n" + + "}"; + + assertEquals(expectedNodeAPUXContent, builder.toString()); + + } } diff --git a/server/src/test/java/org/opensearch/cluster/node/DiscoveryNodesTests.java b/server/src/test/java/org/opensearch/cluster/node/DiscoveryNodesTests.java index d2450859dfcd4..6a22601fefa93 100644 --- a/server/src/test/java/org/opensearch/cluster/node/DiscoveryNodesTests.java +++ b/server/src/test/java/org/opensearch/cluster/node/DiscoveryNodesTests.java @@ -36,10 +36,20 @@ import org.opensearch.LegacyESVersion; import org.opensearch.Version; +import org.opensearch.cluster.metadata.Metadata; import org.opensearch.common.settings.Setting; +import org.opensearch.common.xcontent.json.JsonXContent; +import org.opensearch.core.common.bytes.BytesReference; import org.opensearch.core.common.transport.TransportAddress; +import org.opensearch.core.xcontent.MediaType; +import org.opensearch.core.xcontent.MediaTypeRegistry; +import org.opensearch.core.xcontent.ToXContent; +import org.opensearch.core.xcontent.XContentBuilder; +import org.opensearch.core.xcontent.XContentParser; import org.opensearch.test.OpenSearchTestCase; +import org.opensearch.test.XContentTestUtils; +import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -47,6 +57,7 @@ import java.util.HashMap; import java.util.HashSet; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.Objects; import java.util.Set; @@ -54,6 +65,9 @@ import java.util.stream.Collectors; import java.util.stream.StreamSupport; +import static java.util.Collections.singletonMap; +import static org.opensearch.cluster.metadata.Metadata.CONTEXT_MODE_API; +import static org.opensearch.cluster.metadata.Metadata.CONTEXT_MODE_GATEWAY; import static org.hamcrest.CoreMatchers.containsString; import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.Matchers.arrayContainingInAnyOrder; @@ -497,6 +511,164 @@ public void testMaxMinNodeVersion() { assertEquals(LegacyESVersion.fromString("5.1.0"), build.getMinNodeVersion()); } + public void testToXContentInAPIMode() throws IOException { + DiscoveryNodes nodes = buildDiscoveryNodes(); + + String expectedNodeAPUXContent = "%1$s\"node_%2$d\" : {\n" + + "%1$s \"name\" : \"name_%2$d\",\n" + + "%1$s \"ephemeral_id\" : \"%3$s\",\n" + + "%1$s \"transport_address\" : \"%4$s\",\n" + + "%1$s \"attributes\" : {%5$s}\n" + + "%1$s}"; + + logger.info(nodes); + + verifyToXContentInContextMode( + CONTEXT_MODE_API, + nodes, + "{\n" + " \"nodes\" : {\n" + nodes.getNodes().entrySet().stream().map(entry -> { + int id = Integer.parseInt(entry.getKey().split("_")[1]); + return String.format( + Locale.ROOT, + expectedNodeAPUXContent, + " ", + id, + entry.getValue().getEphemeralId(), + entry.getValue().getAddress().toString(), + entry.getValue().getAttributes().isEmpty() + ? " " + : "\n" + " \"custom\" : \"" + entry.getValue().getAttributes().get("custom") + "\"\n " + ); + }).collect(Collectors.joining(",\n")) + "\n" + " }\n" + "}" + ); + } + + public void testToXContentInGatewayMode() throws IOException { + DiscoveryNodes nodes = buildDiscoveryNodes(); + String expectedXContent = getExpectedXContentInGatewayMode(nodes); + + verifyToXContentInContextMode(CONTEXT_MODE_GATEWAY, nodes, expectedXContent); + } + + private String getExpectedXContentInGatewayMode(DiscoveryNodes nodes) { + /* + * Following formatting creates a string like following: + * "node_1" : { + * "name" : "name_1", + * "ephemeral_id" : "3Q3xRwYKScWqBgVCrWmNCQ", + * "transport_address" : "0.0.0.0:2", + * "attributes" : { + * "custom" : "PKU" + * }, + * "host_name" : "0.0.0.0", + * "host_address" : "0.0.0.0", + * "version" : "3.0.0", + * "roles" : [ + * "custom_role", + * "ingest", + * "remote_cluster_client", + * "search" + * ] + * } + * */ + String expectedNodeAPUXContent = "%1$s\"node_%2$d\" : {\n" + + "%1$s \"name\" : \"name_%2$d\",\n" + + "%1$s \"ephemeral_id\" : \"%3$s\",\n" + + "%1$s \"transport_address\" : \"%4$s\",\n" + + "%1$s \"attributes\" : {%5$s},\n" + + "%1$s \"host_name\" : \"0.0.0.0\",\n" + + "%1$s \"host_address\" : \"0.0.0.0\",\n" + + "%1$s \"version\" : \"%6$s\",\n" + + "%1$s \"roles\" : [%7$s]\n" + + "%1$s}"; + + return "{\n" + " \"nodes\" : {\n" + nodes.getNodes().entrySet().stream().map(entry -> { + int id = Integer.parseInt(entry.getKey().split("_")[1]); + DiscoveryNode node = entry.getValue(); + String indent = " "; + return String.format( + Locale.ROOT, + expectedNodeAPUXContent, + indent, + id, + node.getEphemeralId(), + entry.getValue().getAddress().toString(), + node.getAttributes().isEmpty() + ? " " + : "\n" + indent + " \"custom\" : \"" + node.getAttributes().get("custom") + "\"\n " + indent, + node.getVersion(), + node.getRoles().isEmpty() + ? " " + : "\n" + + indent + + " \"" + + node.getRoles().stream().map(DiscoveryNodeRole::roleName).collect(Collectors.joining("\",\n" + indent + " \"")) + + "\"\n " + + indent + ); + }).collect(Collectors.joining(",\n")) + + "\n" + + " },\n" + + " \"cluster_manager\" : \"" + + nodes.getClusterManagerNodeId() + + "\"\n" + + "}"; + } + + public void verifyToXContentInContextMode(String context, DiscoveryNodes nodes, String expected) throws IOException { + XContentBuilder builder = JsonXContent.contentBuilder().prettyPrint(); + builder.startObject(); + nodes.toXContent(builder, new ToXContent.MapParams(singletonMap(Metadata.CONTEXT_MODE_PARAM, context))); + builder.endObject(); + + assertEquals(expected, builder.toString()); + } + + public void testFromXContent() throws IOException { + doFromXContentTestWithRandomFields(false); + } + + public void testFromXContentWithRandomFields() throws IOException { + doFromXContentTestWithRandomFields(true); + } + + private void doFromXContentTestWithRandomFields(boolean addRandomFields) throws IOException { + DiscoveryNodes nodes = buildDiscoveryNodes(); + boolean humanReadable = randomBoolean(); + final MediaType mediaType = MediaTypeRegistry.JSON; + BytesReference originalBytes = toShuffledXContent( + nodes, + mediaType, + new ToXContent.MapParams(singletonMap(Metadata.CONTEXT_MODE_PARAM, CONTEXT_MODE_GATEWAY)), + humanReadable + ); + + if (addRandomFields) { + String unsupportedField = "unsupported_field"; + BytesReference mutated = BytesReference.bytes( + XContentTestUtils.insertIntoXContent( + mediaType.xContent(), + originalBytes, + Collections.singletonList(""), + () -> unsupportedField, + () -> randomAlphaOfLengthBetween(3, 10) + ) + ); + IllegalArgumentException iae = expectThrows( + IllegalArgumentException.class, + () -> DiscoveryNodes.fromXContent(createParser(mediaType.xContent(), mutated)) + ); + assertEquals(iae.getMessage(), "unexpected value field " + unsupportedField); + } else { + try (XContentParser parser = createParser(mediaType.xContent(), originalBytes)) { + DiscoveryNodes parsedNodes = DiscoveryNodes.fromXContent(parser); + assertEquals(nodes.getSize(), parsedNodes.getSize()); + nodes.forEach(node -> node.equals(parsedNodes.get(node.getId()))); + assertEquals(nodes.getClusterManagerNodeId(), parsedNodes.getClusterManagerNodeId()); + } + } + } + private DiscoveryNode buildDiscoveryNodeFromExisting(DiscoveryNode existing, Version newVersion) { return new DiscoveryNode( existing.getName(), From 08a10cb802bf0a7566827632d0a825029df2d302 Mon Sep 17 00:00:00 2001 From: Shivansh Arora Date: Tue, 7 May 2024 15:31:37 +0530 Subject: [PATCH 2/3] Address PR comments - Add java docs for fromString method in TransportAddress - Cleaned up toXContent methods in DiscoveryNode, DiscoveryNodes Signed-off-by: Shivansh Arora --- .../common/transport/TransportAddress.java | 5 + .../cluster/node/DiscoveryNode.java | 91 +++++++++++-------- .../cluster/node/DiscoveryNodes.java | 71 ++++++++------- .../cluster/node/DiscoveryNodeTests.java | 9 +- .../cluster/node/DiscoveryNodesTests.java | 36 +++++--- 5 files changed, 119 insertions(+), 93 deletions(-) diff --git a/libs/core/src/main/java/org/opensearch/core/common/transport/TransportAddress.java b/libs/core/src/main/java/org/opensearch/core/common/transport/TransportAddress.java index b7ab988ce0404..42982fefe5952 100644 --- a/libs/core/src/main/java/org/opensearch/core/common/transport/TransportAddress.java +++ b/libs/core/src/main/java/org/opensearch/core/common/transport/TransportAddress.java @@ -163,6 +163,11 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws return builder.value(toString()); } + /** + * Converts a string in the format [hostname/ip]:[port] into a transport address. + * @throws UnknownHostException if the hostname or ip address is invalid + * @throws IllegalArgumentException if invalid string format provided + */ public static TransportAddress fromString(String address) throws UnknownHostException { String[] addressSplit = address.split(":"); if (addressSplit.length != 2) { diff --git a/server/src/main/java/org/opensearch/cluster/node/DiscoveryNode.java b/server/src/main/java/org/opensearch/cluster/node/DiscoveryNode.java index 644e5f3de9352..04112b0efc313 100644 --- a/server/src/main/java/org/opensearch/cluster/node/DiscoveryNode.java +++ b/server/src/main/java/org/opensearch/cluster/node/DiscoveryNode.java @@ -64,6 +64,7 @@ import static org.opensearch.cluster.metadata.Metadata.CONTEXT_MODE_API; import static org.opensearch.cluster.metadata.Metadata.CONTEXT_MODE_PARAM; +import static org.opensearch.core.xcontent.XContentParserUtils.ensureExpectedToken; import static org.opensearch.node.NodeRoleSettings.NODE_ROLES_SETTING; import static org.opensearch.node.remotestore.RemoteStoreNodeAttribute.REMOTE_STORE_NODE_ATTRIBUTE_KEY_PREFIX; @@ -582,16 +583,15 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws return builder; } - public static DiscoveryNode fromXContent(XContentParser parser, String nodeId) throws IOException { - if (parser.currentToken() == null) { + public static DiscoveryNode fromXContent(XContentParser parser) throws IOException { + if (parser.currentToken() == null) { // fresh parser? move to the first token parser.nextToken(); } - if (parser.currentToken() == XContentParser.Token.START_OBJECT) { + if (parser.currentToken() == XContentParser.Token.START_OBJECT) { // on a start object move to next token parser.nextToken(); } - if (parser.currentToken() != XContentParser.Token.FIELD_NAME) { - throw new IllegalArgumentException("expected field name but got a " + parser.currentToken()); - } + ensureExpectedToken(XContentParser.Token.FIELD_NAME, parser.currentToken(), parser); + String nodeId = parser.currentName(); String nodeName = null; String hostName = null; String hostAddress = null; @@ -600,45 +600,56 @@ public static DiscoveryNode fromXContent(XContentParser parser, String nodeId) t Map attributes = new HashMap<>(); Set roles = new HashSet<>(); Version version = null; - String currentFieldName = parser.currentName(); - // token should be start object at this point - // XContentParser.Token token = parser.nextToken(); - // if (token != XContentParser.Token.START_OBJECT) { - // throw new IllegalArgumentException("expected object but got a " + token); - // } - XContentParser.Token token; + XContentParser.Token token = parser.nextToken(); + ensureExpectedToken(XContentParser.Token.START_OBJECT, token, parser); while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { - if (token == XContentParser.Token.FIELD_NAME) { - currentFieldName = parser.currentName(); - } else if (token.isValue()) { - if (KEY_NAME.equals(currentFieldName)) { - nodeName = parser.text(); - } else if (KEY_EPHEMERAL_ID.equals(currentFieldName)) { - ephemeralId = parser.text(); - } else if (KEY_TRANSPORT_ADDRESS.equals(currentFieldName)) { - transportAddress = TransportAddress.fromString(parser.text()); - } else if (KEY_HOST_NAME.equals(currentFieldName)) { - hostName = parser.text(); - } else if (KEY_HOST_ADDRESS.equals(currentFieldName)) { - hostAddress = parser.text(); - } else if (KEY_VERSION.equals(currentFieldName)) { - version = Version.fromString(parser.text()); + ensureExpectedToken(XContentParser.Token.FIELD_NAME, token, parser); + String currentFieldName = parser.currentName(); + token = parser.nextToken(); + if (token.isValue()) { + switch (currentFieldName) { + case KEY_NAME: + nodeName = parser.text(); + break; + case KEY_EPHEMERAL_ID: + ephemeralId = parser.text(); + break; + case KEY_TRANSPORT_ADDRESS: + transportAddress = TransportAddress.fromString(parser.text()); + break; + case KEY_HOST_NAME: + hostName = parser.text(); + break; + case KEY_HOST_ADDRESS: + hostAddress = parser.text(); + break; + case KEY_VERSION: + version = Version.fromString(parser.text()); + break; + default: + throw new IllegalArgumentException("Unexpected field [ " + currentFieldName + " ]"); } } else if (token == XContentParser.Token.START_OBJECT) { - if (KEY_ATTRIBUTES.equals(currentFieldName)) { - while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { - if (token == XContentParser.Token.FIELD_NAME) { - currentFieldName = parser.currentName(); - } else if (token.isValue()) { - attributes.put(currentFieldName, parser.text()); - } - } + assert currentFieldName.equals(KEY_ATTRIBUTES) : "expecting field with name [" + + KEY_ATTRIBUTES + + "] but found [" + + currentFieldName + + "]"; + while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { + ensureExpectedToken(XContentParser.Token.FIELD_NAME, token, parser); + currentFieldName = parser.currentName(); + token = parser.nextToken(); + ensureExpectedToken(XContentParser.Token.VALUE_STRING, token, parser); + attributes.put(currentFieldName, parser.text()); } } else if (token == XContentParser.Token.START_ARRAY) { - if (KEY_ROLES.equals(currentFieldName)) { - while ((token = parser.nextToken()) != XContentParser.Token.END_ARRAY) { - roles.add(getRoleFromRoleName(parser.text())); - } + assert currentFieldName.equals(KEY_ROLES) : "expecting field with name [" + + KEY_ROLES + + "] but found [" + + currentFieldName + + "]"; + while ((token = parser.nextToken()) != XContentParser.Token.END_ARRAY) { + roles.add(getRoleFromRoleName(parser.text())); } } else { throw new IllegalArgumentException("unexpected token " + token); diff --git a/server/src/main/java/org/opensearch/cluster/node/DiscoveryNodes.java b/server/src/main/java/org/opensearch/cluster/node/DiscoveryNodes.java index d281021624fd0..ba7975e126d1f 100644 --- a/server/src/main/java/org/opensearch/cluster/node/DiscoveryNodes.java +++ b/server/src/main/java/org/opensearch/cluster/node/DiscoveryNodes.java @@ -45,7 +45,7 @@ import org.opensearch.core.common.io.stream.StreamInput; import org.opensearch.core.common.io.stream.StreamOutput; import org.opensearch.core.common.transport.TransportAddress; -import org.opensearch.core.xcontent.ToXContentFragment; +import org.opensearch.core.xcontent.ToXContentObject; import org.opensearch.core.xcontent.XContentBuilder; import org.opensearch.core.xcontent.XContentParser; @@ -65,6 +65,7 @@ import static org.opensearch.cluster.metadata.Metadata.CONTEXT_MODE_API; import static org.opensearch.cluster.metadata.Metadata.CONTEXT_MODE_PARAM; +import static org.opensearch.core.xcontent.XContentParserUtils.ensureExpectedToken; /** * This class holds all {@link DiscoveryNode} in the cluster and provides convenience methods to @@ -73,9 +74,11 @@ * @opensearch.api */ @PublicApi(since = "1.0.0") -public class DiscoveryNodes extends AbstractDiffable implements Iterable, ToXContentFragment { +public class DiscoveryNodes extends AbstractDiffable implements Iterable, ToXContentObject { public static final DiscoveryNodes EMPTY_NODES = builder().build(); + public static final String KEY_NODES = "nodes"; + public static final String KEY_CLUSTER_MANAGER = "cluster_manager"; private final Map nodes; private final Map dataNodes; @@ -575,56 +578,54 @@ public String toString() { @Override public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { - builder.startObject("nodes"); + builder.startObject(); + innerToXContent(builder, params); + builder.endObject(); + return builder; + } + + public XContentBuilder innerToXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(KEY_NODES); for (DiscoveryNode node : this) { node.toXContent(builder, params); } builder.endObject(); Metadata.XContentContext context = Metadata.XContentContext.valueOf(params.param(CONTEXT_MODE_PARAM, CONTEXT_MODE_API)); if (context == Metadata.XContentContext.GATEWAY && clusterManagerNodeId != null) { - builder.field("cluster_manager", clusterManagerNodeId); + builder.field(KEY_CLUSTER_MANAGER, clusterManagerNodeId); } return builder; } public static DiscoveryNodes fromXContent(XContentParser parser) throws IOException { Builder builder = new Builder(); - if (parser.currentToken() == null) { + if (parser.currentToken() == null) { // fresh parser? move to the first token parser.nextToken(); } - if (parser.currentToken() == XContentParser.Token.START_OBJECT) { - parser.nextToken(); - } - if (parser.currentToken() != XContentParser.Token.FIELD_NAME) { - throw new IllegalArgumentException("expected field name but got a " + parser.currentToken()); - } - XContentParser.Token token; - String currentFieldName = parser.currentName(); + XContentParser.Token token = parser.currentToken(); + ensureExpectedToken(XContentParser.Token.START_OBJECT, token, parser); while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { - if (token == XContentParser.Token.FIELD_NAME) { - currentFieldName = parser.currentName(); - } else if (token == XContentParser.Token.START_OBJECT) { - if ("nodes".equals(currentFieldName)) { - while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { - if (token == XContentParser.Token.FIELD_NAME) { - currentFieldName = parser.currentName(); - } else if (token == XContentParser.Token.START_OBJECT) { - String nodeId = currentFieldName; - DiscoveryNode node = DiscoveryNode.fromXContent(parser, nodeId); - builder.add(node); - } - } - } else { - throw new IllegalArgumentException("unexpected object field " + currentFieldName); + ensureExpectedToken(XContentParser.Token.FIELD_NAME, token, parser); + String currentFieldName = parser.currentName(); + token = parser.nextToken(); + if (token == XContentParser.Token.START_OBJECT) { + assert currentFieldName.equals(KEY_NODES) : "expecting field with name [" + + KEY_NODES + + "] but found [" + + currentFieldName + + "]"; + while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { + builder.add(DiscoveryNode.fromXContent(parser)); } } else if (token.isValue()) { - if ("cluster_manager".equals(currentFieldName)) { - String clusterManagerNodeId = parser.text(); - if (clusterManagerNodeId != null) { - builder.clusterManagerNodeId(clusterManagerNodeId); - } - } else { - throw new IllegalArgumentException("unexpected value field " + currentFieldName); + assert currentFieldName.equals(KEY_CLUSTER_MANAGER) : "expecting field with name [" + + KEY_CLUSTER_MANAGER + + "] but found [" + + currentFieldName + + "]"; + String clusterManagerNodeId = parser.text(); + if (clusterManagerNodeId != null) { + builder.clusterManagerNodeId(clusterManagerNodeId); } } else { throw new IllegalArgumentException("unexpected token " + token); diff --git a/server/src/test/java/org/opensearch/cluster/node/DiscoveryNodeTests.java b/server/src/test/java/org/opensearch/cluster/node/DiscoveryNodeTests.java index b3a4c7e34125e..f40925f5e653c 100644 --- a/server/src/test/java/org/opensearch/cluster/node/DiscoveryNodeTests.java +++ b/server/src/test/java/org/opensearch/cluster/node/DiscoveryNodeTests.java @@ -269,7 +269,7 @@ public void testToXContentInAPIMode() throws IOException { node.toXContent(builder, new ToXContent.MapParams(singletonMap(Metadata.CONTEXT_MODE_PARAM, CONTEXT_MODE_API))); builder.endObject(); - String expectedNodeAPUXContent = "{\n" + String expectedNodeAPIXContent = "{\n" + " \"node_1\" : {\n" + " \"name\" : \"" + node.getName() @@ -282,7 +282,7 @@ public void testToXContentInAPIMode() throws IOException { + " }\n" + "}"; - assertEquals(expectedNodeAPUXContent, builder.toString()); + assertEquals(expectedNodeAPIXContent, builder.toString()); } public void testToXContentInGatewayMode() throws IOException { @@ -296,7 +296,7 @@ public void testToXContentInGatewayMode() throws IOException { node.toXContent(builder, new ToXContent.MapParams(singletonMap(Metadata.CONTEXT_MODE_PARAM, CONTEXT_MODE_GATEWAY))); builder.endObject(); - String expectedNodeAPUXContent = "{\n" + String expectedNodeAPIXContent = "{\n" + " \"node_1\" : {\n" + " \"name\" : \"" + node.getName() @@ -320,7 +320,6 @@ public void testToXContentInGatewayMode() throws IOException { + " }\n" + "}"; - assertEquals(expectedNodeAPUXContent, builder.toString()); - + assertEquals(expectedNodeAPIXContent, builder.toString()); } } diff --git a/server/src/test/java/org/opensearch/cluster/node/DiscoveryNodesTests.java b/server/src/test/java/org/opensearch/cluster/node/DiscoveryNodesTests.java index 6a22601fefa93..5fed485ad75c9 100644 --- a/server/src/test/java/org/opensearch/cluster/node/DiscoveryNodesTests.java +++ b/server/src/test/java/org/opensearch/cluster/node/DiscoveryNodesTests.java @@ -514,6 +514,18 @@ public void testMaxMinNodeVersion() { public void testToXContentInAPIMode() throws IOException { DiscoveryNodes nodes = buildDiscoveryNodes(); + /* + * Following format is expected to be used by API and looks like following: + * "node_1" : { + * "name" : "name_1", + * "ephemeral_id" : "3Q3xRwYKScWqBgVCrWmNCQ", + * "transport_address" : "0.0.0.0:2", + * "attributes" : { + * "custom" : "PKU" + * } + * } + * + * */ String expectedNodeAPUXContent = "%1$s\"node_%2$d\" : {\n" + "%1$s \"name\" : \"name_%2$d\",\n" + "%1$s \"ephemeral_id\" : \"%3$s\",\n" @@ -521,8 +533,6 @@ public void testToXContentInAPIMode() throws IOException { + "%1$s \"attributes\" : {%5$s}\n" + "%1$s}"; - logger.info(nodes); - verifyToXContentInContextMode( CONTEXT_MODE_API, nodes, @@ -585,26 +595,26 @@ private String getExpectedXContentInGatewayMode(DiscoveryNodes nodes) { return "{\n" + " \"nodes\" : {\n" + nodes.getNodes().entrySet().stream().map(entry -> { int id = Integer.parseInt(entry.getKey().split("_")[1]); DiscoveryNode node = entry.getValue(); - String indent = " "; + String offset = " "; return String.format( Locale.ROOT, expectedNodeAPUXContent, - indent, + offset, id, node.getEphemeralId(), entry.getValue().getAddress().toString(), node.getAttributes().isEmpty() ? " " - : "\n" + indent + " \"custom\" : \"" + node.getAttributes().get("custom") + "\"\n " + indent, + : "\n" + offset + " \"custom\" : \"" + node.getAttributes().get("custom") + "\"\n " + offset, node.getVersion(), node.getRoles().isEmpty() ? " " : "\n" - + indent + + offset + " \"" - + node.getRoles().stream().map(DiscoveryNodeRole::roleName).collect(Collectors.joining("\",\n" + indent + " \"")) + + node.getRoles().stream().map(DiscoveryNodeRole::roleName).collect(Collectors.joining("\",\n" + offset + " \"")) + "\"\n " - + indent + + offset ); }).collect(Collectors.joining(",\n")) + "\n" @@ -617,9 +627,7 @@ private String getExpectedXContentInGatewayMode(DiscoveryNodes nodes) { public void verifyToXContentInContextMode(String context, DiscoveryNodes nodes, String expected) throws IOException { XContentBuilder builder = JsonXContent.contentBuilder().prettyPrint(); - builder.startObject(); nodes.toXContent(builder, new ToXContent.MapParams(singletonMap(Metadata.CONTEXT_MODE_PARAM, context))); - builder.endObject(); assertEquals(expected, builder.toString()); } @@ -654,11 +662,13 @@ private void doFromXContentTestWithRandomFields(boolean addRandomFields) throws () -> randomAlphaOfLengthBetween(3, 10) ) ); - IllegalArgumentException iae = expectThrows( - IllegalArgumentException.class, + AssertionError iae = expectThrows( + AssertionError.class, () -> DiscoveryNodes.fromXContent(createParser(mediaType.xContent(), mutated)) ); - assertEquals(iae.getMessage(), "unexpected value field " + unsupportedField); + assertTrue( + iae.getMessage().matches("expecting field with name \\[([a-z]+(?:_[a-z]+)*)] but found \\[" + unsupportedField + "]") + ); } else { try (XContentParser parser = createParser(mediaType.xContent(), originalBytes)) { DiscoveryNodes parsedNodes = DiscoveryNodes.fromXContent(parser); From 2d2127ec2bf014c3d46d8c3c381cf52e432cef9f Mon Sep 17 00:00:00 2001 From: Shivansh Arora Date: Tue, 4 Jun 2024 20:10:40 +0530 Subject: [PATCH 3/3] Address PR comments Signed-off-by: Shivansh Arora --- .../common/transport/TransportAddress.java | 17 ++++++---- .../cluster/node/DiscoveryNode.java | 9 ++++- .../core/common/TransportAddressTests.java | 34 +++++++++++++++++++ 3 files changed, 53 insertions(+), 7 deletions(-) create mode 100644 server/src/test/java/org/opensearch/core/common/TransportAddressTests.java diff --git a/libs/core/src/main/java/org/opensearch/core/common/transport/TransportAddress.java b/libs/core/src/main/java/org/opensearch/core/common/transport/TransportAddress.java index 42982fefe5952..527b53edd849c 100644 --- a/libs/core/src/main/java/org/opensearch/core/common/transport/TransportAddress.java +++ b/libs/core/src/main/java/org/opensearch/core/common/transport/TransportAddress.java @@ -41,9 +41,11 @@ import org.opensearch.core.xcontent.XContentBuilder; import java.io.IOException; +import java.net.Inet6Address; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.UnknownHostException; +import java.util.Arrays; /** * A transport address used for IP socket address (wraps {@link java.net.InetSocketAddress}). @@ -166,15 +168,18 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws /** * Converts a string in the format [hostname/ip]:[port] into a transport address. * @throws UnknownHostException if the hostname or ip address is invalid - * @throws IllegalArgumentException if invalid string format provided */ public static TransportAddress fromString(String address) throws UnknownHostException { String[] addressSplit = address.split(":"); - if (addressSplit.length != 2) { - throw new IllegalArgumentException("address must be of the form [hostname/ip]:[port]"); + if (addressSplit.length == 2) { + String hostname = addressSplit[0]; + int port = Integer.parseInt(addressSplit[1]); + return new TransportAddress(InetAddress.getByName(hostname), port); + } else { + // this should be an IPv6 ip + int port = Integer.parseInt(addressSplit[addressSplit.length - 1]); + String hostname = String.join(":", Arrays.copyOfRange(addressSplit, 0, addressSplit.length - 1)); + return new TransportAddress(Inet6Address.getByName(hostname), port); } - String hostname = addressSplit[0]; - int port = Integer.parseInt(addressSplit[1]); - return new TransportAddress(InetAddress.getByName(hostname), port); } } diff --git a/server/src/main/java/org/opensearch/cluster/node/DiscoveryNode.java b/server/src/main/java/org/opensearch/cluster/node/DiscoveryNode.java index 04112b0efc313..77f2b0c8504a0 100644 --- a/server/src/main/java/org/opensearch/cluster/node/DiscoveryNode.java +++ b/server/src/main/java/org/opensearch/cluster/node/DiscoveryNode.java @@ -32,6 +32,8 @@ package org.opensearch.cluster.node; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.opensearch.Version; import org.opensearch.cluster.metadata.Metadata; import org.opensearch.common.UUIDs; @@ -85,6 +87,7 @@ public class DiscoveryNode implements Writeable, ToXContentFragment { static final String KEY_ATTRIBUTES = "attributes"; static final String KEY_VERSION = "version"; static final String KEY_ROLES = "roles"; + private static final Logger logger = LogManager.getLogger(DiscoveryNode.class); public static boolean nodeRequiresLocalStorage(Settings settings) { boolean localStorageEnable = Node.NODE_LOCAL_STORAGE_SETTING.get(settings); @@ -583,6 +586,10 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws return builder; } + /** + * This method is able to parse either a complete XContentObject with start and end object + * or only a fragment with the key and value. + */ public static DiscoveryNode fromXContent(XContentParser parser) throws IOException { if (parser.currentToken() == null) { // fresh parser? move to the first token parser.nextToken(); @@ -627,7 +634,7 @@ public static DiscoveryNode fromXContent(XContentParser parser) throws IOExcepti version = Version.fromString(parser.text()); break; default: - throw new IllegalArgumentException("Unexpected field [ " + currentFieldName + " ]"); + logger.warn("unknown field [{}]", currentFieldName); } } else if (token == XContentParser.Token.START_OBJECT) { assert currentFieldName.equals(KEY_ATTRIBUTES) : "expecting field with name [" diff --git a/server/src/test/java/org/opensearch/core/common/TransportAddressTests.java b/server/src/test/java/org/opensearch/core/common/TransportAddressTests.java new file mode 100644 index 0000000000000..b234847ca4885 --- /dev/null +++ b/server/src/test/java/org/opensearch/core/common/TransportAddressTests.java @@ -0,0 +1,34 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.core.common; + +import org.opensearch.core.common.transport.TransportAddress; +import org.opensearch.test.OpenSearchTestCase; + +import java.net.UnknownHostException; + +public class TransportAddressTests extends OpenSearchTestCase { + public void testFromString() throws UnknownHostException { + TransportAddress address = TransportAddress.fromString("127.0.0.1:9300"); + assertEquals("127.0.0.1", address.getAddress()); + assertEquals(9300, address.getPort()); + + address = TransportAddress.fromString("1080:0:0:0:8:800:200C:417A:9300"); + assertEquals("1080::8:800:200c:417a", address.getAddress()); + assertEquals(9300, address.getPort()); + + address = TransportAddress.fromString("FF01:0:0:0:0:0:0:101:9300"); + assertEquals("ff01::101", address.getAddress()); + assertEquals(9300, address.getPort()); + + address = TransportAddress.fromString("FEDC:BA98:7654:3210:FEDC:BA98:7654:3210:9200"); + assertEquals("fedc:ba98:7654:3210:fedc:ba98:7654:3210", address.getAddress()); + assertEquals(9200, address.getPort()); + } +}