diff --git a/src/main/java/io/nats/client/support/ApiConstants.java b/src/main/java/io/nats/client/support/ApiConstants.java index dc3e9b040..b8c10db3b 100644 --- a/src/main/java/io/nats/client/support/ApiConstants.java +++ b/src/main/java/io/nats/client/support/ApiConstants.java @@ -117,6 +117,7 @@ public interface ApiConstants { String MEMORY_MAX_STREAM_BYTES = "memory_max_stream_bytes"; String MESSAGE = "message"; String MESSAGES = "messages"; + String METADATA = "metadata"; String MTIME = "mtime"; String MIRROR = "mirror"; String MSGS = "msgs"; diff --git a/src/main/java/io/nats/client/support/JsonUtils.java b/src/main/java/io/nats/client/support/JsonUtils.java index d239208d0..043aad4b8 100644 --- a/src/main/java/io/nats/client/support/JsonUtils.java +++ b/src/main/java/io/nats/client/support/JsonUtils.java @@ -29,6 +29,7 @@ import static io.nats.client.support.DateTimeUtils.DEFAULT_TIME; import static io.nats.client.support.Encoding.jsonDecode; import static io.nats.client.support.Encoding.jsonEncode; +import static io.nats.client.support.JsonValueUtils.instance; import static io.nats.client.support.NatsConstants.COLON; /** @@ -261,6 +262,12 @@ public static void addField(StringBuilder sb, String fname, JsonSerializable val } } + public static void addField(StringBuilder sb, String fname, Map map) { + if (map != null && map.size() > 0) { + addField(sb, fname, instance(map)); + } + } + interface ListAdder { void append(StringBuilder sb, T t); } @@ -927,4 +934,19 @@ public static void readNanos(String json, Pattern pattern, Consumer c) c.accept(Duration.ofNanos(Long.parseLong(m.group(1)))); } } + + public static boolean mapEquals(Map map1, Map map2) { + if (map1 == null) { + return map2 == null; + } + if (map2 == null || map1.size() != map2.size()) { + return false; + } + for (String key : map1.keySet()) { + if (!Objects.equals(map1.get(key), map2.get(key))) { + return false; + } + } + return true; + } } diff --git a/src/main/java/io/nats/client/support/JsonValueUtils.java b/src/main/java/io/nats/client/support/JsonValueUtils.java index bd6aefbe0..9c626c41d 100644 --- a/src/main/java/io/nats/client/support/JsonValueUtils.java +++ b/src/main/java/io/nats/client/support/JsonValueUtils.java @@ -21,8 +21,7 @@ import java.util.*; import java.util.function.Function; -import static io.nats.client.support.JsonValue.EMPTY_ARRAY; -import static io.nats.client.support.JsonValue.EMPTY_MAP; +import static io.nats.client.support.JsonValue.*; /** * Internal json value helpers. @@ -52,6 +51,21 @@ public static List readArray(JsonValue jsonValue, String key) { return read(jsonValue, key, v -> v == null ? EMPTY_ARRAY.array : v.array); } + public static Map readStringStringMap(JsonValue jv, String key) { + JsonValue o = readObject(jv, key); + if (o.type == Type.MAP && o.map.size() > 0) { + Map temp = new HashMap<>(); + for (String k : o.map.keySet()) { + String value = readString(o, k); + if (value != null) { + temp.put(k, value); + } + } + return temp.isEmpty() ? null : temp; + } + return null; + } + public static String readString(JsonValue jsonValue, String key) { return read(jsonValue, key, v -> v == null ? null : v.string); } @@ -220,7 +234,7 @@ public static JsonValue instance(Map map) { public static JsonValue toJsonValue(Object o) { if (o == null) { - return JsonValue.NULL; + return NULL; } if (o instanceof JsonValue) { return (JsonValue)o; @@ -272,7 +286,7 @@ public static class MapBuilder { public MapBuilder put(String s, Object o) { JsonValue vv = JsonValueUtils.toJsonValue(o); - if (vv.type != JsonValue.Type.NULL) { + if (vv.type != Type.NULL) { jv.map.put(s, vv); } return this; @@ -291,7 +305,7 @@ public static class ArrayBuilder { public JsonValue jv = new JsonValue(new ArrayList<>()); public ArrayBuilder add(Object o) { JsonValue vv = JsonValueUtils.toJsonValue(o); - if (vv.type != JsonValue.Type.NULL) { + if (vv.type != Type.NULL) { jv.array.add(JsonValueUtils.toJsonValue(o)); } return this; diff --git a/src/main/java/io/nats/service/Endpoint.java b/src/main/java/io/nats/service/Endpoint.java index 90de1ce07..ab1eca967 100644 --- a/src/main/java/io/nats/service/Endpoint.java +++ b/src/main/java/io/nats/service/Endpoint.java @@ -17,12 +17,12 @@ import io.nats.client.support.JsonUtils; import io.nats.client.support.JsonValue; +import java.util.Map; import java.util.Objects; import static io.nats.client.support.ApiConstants.*; import static io.nats.client.support.JsonUtils.endJson; -import static io.nats.client.support.JsonValueUtils.readString; -import static io.nats.client.support.JsonValueUtils.readValue; +import static io.nats.client.support.JsonValueUtils.*; import static io.nats.client.support.Validator.validateIsRestrictedTerm; import static io.nats.client.support.Validator.validateSubject; @@ -33,25 +33,26 @@ public class Endpoint implements JsonSerializable { private final String name; private final String subject; private final Schema schema; + private final Map metadata; public Endpoint(String name, String subject, Schema schema) { - this(name, subject, schema, true); + this(name, subject, schema, null, true); } public Endpoint(String name) { - this(name, null, null, true); + this(name, null, null, null, true); } public Endpoint(String name, String subject) { - this(name, subject, null, true); + this(name, subject, null, null, true); } public Endpoint(String name, String subject, String schemaRequest, String schemaResponse) { - this(name, subject, Schema.optionalInstance(schemaRequest, schemaResponse), true); + this(name, subject, Schema.optionalInstance(schemaRequest, schemaResponse), null, true); } // internal use constructors - Endpoint(String name, String subject, Schema schema, boolean validate) { + Endpoint(String name, String subject, Schema schema, Map metadata, boolean validate) { if (validate) { this.name = validateIsRestrictedTerm(name, "Endpoint Name", true); if (subject == null) { @@ -66,16 +67,18 @@ public Endpoint(String name, String subject, String schemaRequest, String schema this.subject = subject; } this.schema = schema; + this.metadata = metadata == null || metadata.size() == 0 ? null : metadata; } Endpoint(JsonValue vEndpoint) { name = readString(vEndpoint, NAME); subject = readString(vEndpoint, SUBJECT); schema = Schema.optionalInstance(readValue(vEndpoint, SCHEMA)); + metadata = readStringStringMap(vEndpoint, METADATA); } Endpoint(Builder b) { - this(b.name, b.subject, Schema.optionalInstance(b.schemaRequest, b.schemaResponse)); + this(b.name, b.subject, Schema.optionalInstance(b.schemaRequest, b.schemaResponse), b.metadata, true); } @Override @@ -84,6 +87,7 @@ public String toJson() { JsonUtils.addField(sb, NAME, name); JsonUtils.addField(sb, SUBJECT, subject); JsonUtils.addField(sb, SCHEMA, schema); + JsonUtils.addField(sb, METADATA, metadata); return endJson(sb).toString(); } @@ -104,6 +108,10 @@ public Schema getSchema() { return schema; } + public Map getMetadata() { + return metadata; + } + public static Builder builder() { return new Builder(); } @@ -113,6 +121,7 @@ public static class Builder { private String subject; private String schemaRequest; private String schemaResponse; + private Map metadata; public Builder endpoint(Endpoint endpoint) { name = endpoint.getName(); @@ -161,6 +170,11 @@ public Builder schema(Schema schema) { return this; } + public Builder metadata(Map metadata) { + this.metadata = metadata; + return this; + } + public Endpoint build() { return new Endpoint(this); } @@ -171,11 +185,12 @@ public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; - Endpoint endpoint = (Endpoint) o; + Endpoint that = (Endpoint) o; - if (!Objects.equals(name, endpoint.name)) return false; - if (!Objects.equals(subject, endpoint.subject)) return false; - return Objects.equals(schema, endpoint.schema); + if (!Objects.equals(name, that.name)) return false; + if (!Objects.equals(subject, that.subject)) return false; + if (!Objects.equals(schema, that.schema)) return false; + return JsonUtils.mapEquals(metadata, that.metadata); } @Override @@ -183,6 +198,7 @@ public int hashCode() { int result = name != null ? name.hashCode() : 0; result = 31 * result + (subject != null ? subject.hashCode() : 0); result = 31 * result + (schema != null ? schema.hashCode() : 0); + result = 31 * result + (metadata != null ? metadata.hashCode() : 0); return result; } } diff --git a/src/main/java/io/nats/service/InfoResponse.java b/src/main/java/io/nats/service/InfoResponse.java index cc83bfe28..3aaf1cf2f 100644 --- a/src/main/java/io/nats/service/InfoResponse.java +++ b/src/main/java/io/nats/service/InfoResponse.java @@ -17,6 +17,7 @@ import io.nats.client.support.JsonValue; import java.util.List; +import java.util.Map; import java.util.Objects; import static io.nats.client.support.ApiConstants.DESCRIPTION; @@ -33,8 +34,8 @@ public class InfoResponse extends ServiceResponse { private final String description; private final List subjects; - public InfoResponse(String id, String name, String version, String description, List subjects) { - super(TYPE, id, name, version); + public InfoResponse(String id, String name, String version, Map metadata, String description, List subjects) { + super(TYPE, id, name, version, metadata); this.description = description; this.subjects = subjects; } diff --git a/src/main/java/io/nats/service/PingResponse.java b/src/main/java/io/nats/service/PingResponse.java index 65aecb3fe..8fa16a666 100644 --- a/src/main/java/io/nats/service/PingResponse.java +++ b/src/main/java/io/nats/service/PingResponse.java @@ -13,14 +13,16 @@ package io.nats.service; +import java.util.Map; + /** * SERVICE IS AN EXPERIMENTAL API SUBJECT TO CHANGE */ public class PingResponse extends ServiceResponse { public static final String TYPE = "io.nats.micro.v1.ping_response"; - public PingResponse(String id, String name, String version) { - super(TYPE, id, name, version); + public PingResponse(String id, String name, String version, Map metadata) { + super(TYPE, id, name, version, metadata); } public PingResponse(byte[] jsonBytes) { diff --git a/src/main/java/io/nats/service/SchemaResponse.java b/src/main/java/io/nats/service/SchemaResponse.java index 08199f5a7..86a3a0b62 100644 --- a/src/main/java/io/nats/service/SchemaResponse.java +++ b/src/main/java/io/nats/service/SchemaResponse.java @@ -17,6 +17,7 @@ import io.nats.client.support.JsonValue; import java.util.List; +import java.util.Map; import java.util.Objects; import static io.nats.client.support.ApiConstants.API_URL; @@ -33,8 +34,8 @@ public class SchemaResponse extends ServiceResponse { private final String apiUrl; private final List endpoints; - public SchemaResponse(String id, String name, String version, String apiUrl, List endpoints) { - super(TYPE, id, name, version); + public SchemaResponse(String id, String name, String version, Map metadata, String apiUrl, List endpoints) { + super(TYPE, id, name, version, metadata); this.apiUrl = apiUrl; this.endpoints = endpoints; } diff --git a/src/main/java/io/nats/service/Service.java b/src/main/java/io/nats/service/Service.java index 345441d97..9911139a7 100644 --- a/src/main/java/io/nats/service/Service.java +++ b/src/main/java/io/nats/service/Service.java @@ -86,9 +86,9 @@ public class Service { } // build static responses - pingResponse = new PingResponse(id, b.name, b.version); - infoResponse = new InfoResponse(id, b.name, b.version, b.description, infoSubjects); - schemaResponse = new SchemaResponse(id, b.name, b.version, b.apiUrl, schemaEndpoints); + pingResponse = new PingResponse(id, b.name, b.version, b.metadata); + infoResponse = new InfoResponse(id, b.name, b.version, b.metadata, b.description, infoSubjects); + schemaResponse = new SchemaResponse(id, b.name, b.version, b.metadata, b.apiUrl, schemaEndpoints); if (b.pingDispatcher == null || b.infoDispatcher == null || b.schemaDispatcher == null || b.statsDispatcher == null) { dTemp = conn.createDispatcher(); @@ -132,7 +132,7 @@ private void addStatsContexts(Dispatcher dUser, Dispatcher dInternal) { private Endpoint internalEndpoint(String discoveryName, String optionalServiceNameSegment, String optionalServiceIdSegment) { String subject = toDiscoverySubject(discoveryName, optionalServiceNameSegment, optionalServiceIdSegment); - return new Endpoint(subject, subject, null, false); + return new Endpoint(subject, subject, null, null, false); } static String toDiscoverySubject(String discoveryName, String optionalServiceNameSegment, String optionalServiceIdSegment) { diff --git a/src/main/java/io/nats/service/ServiceBuilder.java b/src/main/java/io/nats/service/ServiceBuilder.java index a154ab703..101841603 100644 --- a/src/main/java/io/nats/service/ServiceBuilder.java +++ b/src/main/java/io/nats/service/ServiceBuilder.java @@ -16,6 +16,7 @@ public class ServiceBuilder { String name; String description; String version; + Map metadata; String apiUrl; final Map serviceEndpoints = new HashMap<>(); Duration drainTimeout = DEFAULT_DRAIN_TIMEOUT; @@ -44,6 +45,11 @@ public ServiceBuilder version(String version) { return this; } + public ServiceBuilder metadata(Map metadata) { + this.metadata = metadata; + return this; + } + public ServiceBuilder apiUrl(String apiUrl) { this.apiUrl = apiUrl; return this; diff --git a/src/main/java/io/nats/service/ServiceResponse.java b/src/main/java/io/nats/service/ServiceResponse.java index b6d54eb8a..fa36dd037 100644 --- a/src/main/java/io/nats/service/ServiceResponse.java +++ b/src/main/java/io/nats/service/ServiceResponse.java @@ -15,11 +15,13 @@ import io.nats.client.support.*; +import java.util.Map; import java.util.Objects; import static io.nats.client.support.ApiConstants.*; import static io.nats.client.support.JsonUtils.endJson; import static io.nats.client.support.JsonValueUtils.readString; +import static io.nats.client.support.JsonValueUtils.readStringStringMap; /** * SERVICE IS AN EXPERIMENTAL API SUBJECT TO CHANGE @@ -29,12 +31,14 @@ public abstract class ServiceResponse implements JsonSerializable { protected final String name; protected final String id; protected final String version; + protected final Map metadata; - protected ServiceResponse(String type, String id, String name, String version) { + protected ServiceResponse(String type, String id, String name, String version, Map metadata) { this.type = type; this.id = id; this.name = name; this.version = version; + this.metadata = metadata == null || metadata.isEmpty() ? null : metadata; } protected ServiceResponse(String type, ServiceResponse template) { @@ -42,6 +46,7 @@ protected ServiceResponse(String type, ServiceResponse template) { this.id = template.id; this.name = template.name; this.version = template.version; + this.metadata = template.metadata; } protected ServiceResponse(String type, JsonValue jv) { @@ -56,6 +61,7 @@ protected ServiceResponse(String type, JsonValue jv) { id = Validator.required(readString(jv, ID), "Id"); name = Validator.required(readString(jv, NAME), "Name"); version = Validator.required(readString(jv, VERSION), "Version"); + metadata = readStringStringMap(jv, METADATA); } protected static JsonValue parseMessage(byte[] bytes) { @@ -99,6 +105,14 @@ public String getVersion() { return version; } + /** + * Metadata for the service + * @return the metadata or null if there is no metadata + */ + public Map getMetadata() { + return metadata; + } + protected void subToJson(StringBuilder sb) {} @Override @@ -108,7 +122,8 @@ public String toJson() { JsonUtils.addField(sb, NAME, name); JsonUtils.addField(sb, VERSION, version); subToJson(sb); - JsonUtils.addField(sb, ApiConstants.TYPE, type); + JsonUtils.addField(sb, TYPE, type); + JsonUtils.addField(sb, METADATA, metadata); return endJson(sb).toString(); } @@ -128,7 +143,8 @@ public boolean equals(Object o) { if (!Objects.equals(type, that.type)) return false; if (!Objects.equals(name, that.name)) return false; if (!Objects.equals(id, that.id)) return false; - return Objects.equals(version, that.version); + if (!Objects.equals(version, that.version)) return false; + return JsonUtils.mapEquals(metadata, that.metadata); } @Override @@ -137,6 +153,7 @@ public int hashCode() { result = 31 * result + (name != null ? name.hashCode() : 0); result = 31 * result + (id != null ? id.hashCode() : 0); result = 31 * result + (version != null ? version.hashCode() : 0); + result = 31 * result + (metadata != null ? metadata.hashCode() : 0); return result; } } diff --git a/src/test/java/io/nats/client/support/JsonUtilsTests.java b/src/test/java/io/nats/client/support/JsonUtilsTests.java index f6e728577..9924a545e 100644 --- a/src/test/java/io/nats/client/support/JsonUtilsTests.java +++ b/src/test/java/io/nats/client/support/JsonUtilsTests.java @@ -20,10 +20,7 @@ import java.time.Duration; import java.time.ZonedDateTime; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; +import java.util.*; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; import java.util.regex.Pattern; @@ -407,4 +404,62 @@ public void testReadStringMayHaveQuotes() { assertNull(readStringMayHaveQuotes(json, "NotThere", null)); assertEquals("q\"quoted\"q tab\ttab ===", readStringMayHaveQuotes(json, DESCRIPTION, null)); } + + @Test + public void testMapEquals() { + Map map1 = new HashMap<>(); + map1.put("foo", "bar"); + map1.put("bada", "bing"); + + Map map2 = new HashMap<>(); + map2.put("bada", "bing"); + map2.put("foo", "bar"); + + Map map3 = new HashMap<>(); + map3.put("foo", "bar"); + + Map map4 = new HashMap<>(); + map4.put("foo", "baz"); + + Map empty1 = new HashMap<>(); + Map empty2 = new HashMap<>(); + + assertTrue(JsonUtils.mapEquals(null, null)); + assertFalse(JsonUtils.mapEquals(map1, null)); + assertFalse(JsonUtils.mapEquals(null, map1)); + assertFalse(JsonUtils.mapEquals(null, empty1)); + assertFalse(JsonUtils.mapEquals(empty1, null)); + + assertTrue(JsonUtils.mapEquals(map1, map2)); + assertFalse(JsonUtils.mapEquals(map1, map3)); + assertFalse(JsonUtils.mapEquals(map1, map4)); + assertFalse(JsonUtils.mapEquals(map1, empty1)); + + assertTrue(JsonUtils.mapEquals(map2, map1)); + assertFalse(JsonUtils.mapEquals(map2, map3)); + assertFalse(JsonUtils.mapEquals(map2, map4)); + assertFalse(JsonUtils.mapEquals(map2, empty1)); + + assertFalse(JsonUtils.mapEquals(map3, map1)); + assertFalse(JsonUtils.mapEquals(map3, map2)); + assertFalse(JsonUtils.mapEquals(map3, map4)); + assertFalse(JsonUtils.mapEquals(map3, empty1)); + + assertFalse(JsonUtils.mapEquals(map4, map1)); + assertFalse(JsonUtils.mapEquals(map4, map2)); + assertFalse(JsonUtils.mapEquals(map4, map3)); + assertFalse(JsonUtils.mapEquals(map4, empty1)); + + assertFalse(JsonUtils.mapEquals(empty1, map1)); + assertFalse(JsonUtils.mapEquals(empty1, map2)); + assertFalse(JsonUtils.mapEquals(empty1, map3)); + assertFalse(JsonUtils.mapEquals(empty1, map4)); + assertTrue(JsonUtils.mapEquals(empty1, empty2)); + + assertFalse(JsonUtils.mapEquals(empty2, map1)); + assertFalse(JsonUtils.mapEquals(empty2, map2)); + assertFalse(JsonUtils.mapEquals(empty2, map3)); + assertFalse(JsonUtils.mapEquals(empty2, map4)); + assertTrue(JsonUtils.mapEquals(empty2, empty1)); + } } diff --git a/src/test/java/io/nats/service/ServiceTests.java b/src/test/java/io/nats/service/ServiceTests.java index b7f9aa9c0..492fea58e 100644 --- a/src/test/java/io/nats/service/ServiceTests.java +++ b/src/test/java/io/nats/service/ServiceTests.java @@ -743,6 +743,7 @@ public void testEndpointConstruction() { assertEquals(NAME, e.getSubject()); assertNull(e.getSchema()); assertEquals(e, Endpoint.builder().endpoint(e).build()); + assertNull(e.getMetadata()); e = new Endpoint(NAME, SUBJECT); assertEquals(NAME, e.getName()); @@ -800,6 +801,24 @@ public void testEndpointConstruction() { assertEquals(SUBJECT, e.getSubject()); assertNull(e.getSchema()); + Map metadata = new HashMap<>(); + e = Endpoint.builder() + .name(NAME).subject(SUBJECT) + .metadata(metadata) + .build(); + assertEquals(NAME, e.getName()); + assertEquals(SUBJECT, e.getSubject()); + assertNull(e.getMetadata()); + + metadata.put("k", "v"); + e = Endpoint.builder() + .name(NAME).subject(SUBJECT) + .metadata(metadata) + .build(); + assertEquals(NAME, e.getName()); + assertEquals(SUBJECT, e.getSubject()); + assertTrue(JsonUtils.mapEquals(metadata, e.getMetadata())); + // some subject testing e = new Endpoint(NAME, "foo.>"); assertEquals("foo.>", e.getSubject()); @@ -1080,7 +1099,10 @@ public TestServiceResponses(JsonValue jv) { @Test public void testServiceResponsesConstruction() { - PingResponse pr1 = new PingResponse("id", "name", "0.0.0"); + Map metadata = new HashMap<>(); + metadata.put("k", "v"); + + PingResponse pr1 = new PingResponse("id", "name", "0.0.0", metadata); PingResponse pr2 = new PingResponse(pr1.toJson().getBytes()); validateApiInOutPingResponse(pr1); validateApiInOutPingResponse(pr2); @@ -1106,7 +1128,7 @@ public void testServiceResponsesConstruction() { iae = assertThrows(IllegalArgumentException.class, () -> new TestServiceResponses(json4.getBytes())); assertTrue(iae.getMessage().contains("Version cannot be null")); - InfoResponse ir1 = new InfoResponse("id", "name", "0.0.0", "desc", Arrays.asList("subject1", "subject2")); + InfoResponse ir1 = new InfoResponse("id", "name", "0.0.0", metadata, "desc", Arrays.asList("subject1", "subject2")); InfoResponse ir2 = new InfoResponse(ir1.toJson().getBytes()); validateApiInOutInfoResponse(ir1); validateApiInOutInfoResponse(ir2); @@ -1114,7 +1136,7 @@ public void testServiceResponsesConstruction() { List endpoints = new ArrayList<>(); endpoints.add(new EndpointResponse("endName0", "endSubject0", new Schema("endSchemaRequest0", "endSchemaResponse0"))); endpoints.add(new EndpointResponse("endName1", "endSubject1", new Schema("endSchemaRequest1", "endSchemaResponse1"))); - SchemaResponse sch1 = new SchemaResponse("id", "name", "0.0.0", "apiUrl", endpoints); + SchemaResponse sch1 = new SchemaResponse("id", "name", "0.0.0", metadata, "apiUrl", endpoints); SchemaResponse sch2 = new SchemaResponse(sch1.toJson().getBytes()); validateApiInOutSchemaResponse(sch1); validateApiInOutSchemaResponse(sch2); @@ -1197,12 +1219,17 @@ private static void validateApiInOutServiceResponse(ServiceResponse r, String ty assertEquals("id", r.getId()); assertEquals("name", r.getName()); assertEquals("0.0.0", r.getVersion()); + assertNotNull(r.getMetadata()); + assertEquals(1, r.getMetadata().size()); + assertEquals("v", r.getMetadata().get("k")); + assertNull(r.getMetadata().get("x")); String j = r.toJson(); assertTrue(j.startsWith("{")); assertTrue(j.contains("\"type\":\"" + type + "\"")); assertTrue(j.contains("\"name\":\"name\"")); assertTrue(j.contains("\"id\":\"id\"")); assertTrue(j.contains("\"version\":\"0.0.0\"")); + assertTrue(j.contains("\"metadata\":{\"k\":\"v\"}")); assertEquals(toKey(r.getClass()) + j, r.toString()); }