diff --git a/e2e-tests/docker-compose-test.yml b/e2e-tests/docker-compose-test.yml index 200aa619c2..a05bcf3132 100644 --- a/e2e-tests/docker-compose-test.yml +++ b/e2e-tests/docker-compose-test.yml @@ -62,6 +62,7 @@ services: MERLIN_LOCAL_STORE: /usr/src/app/merlin_file_store MERLIN_PORT: 27183 JAVA_OPTS: > + -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005 -Dorg.slf4j.simpleLogger.defaultLogLevel=DEBUG -Dorg.slf4j.simpleLogger.log.com.zaxxer.hikari=INFO -Dorg.slf4j.simpleLogger.logFile=System.err @@ -90,6 +91,7 @@ services: SCHEDULER_PORT: 27185 SCHEDULER_RULES_JAR: /usr/src/app/merlin_file_store/scheduler_rules.jar JAVA_OPTS: > + -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005 -Dorg.slf4j.simpleLogger.defaultLogLevel=DEBUG -Dorg.slf4j.simpleLogger.log.com.zaxxer.hikari=INFO -Dorg.slf4j.simpleLogger.logFile=System.err @@ -126,6 +128,7 @@ services: MERLIN_WORKER_DB_USER: 'aerie' MERLIN_WORKER_LOCAL_STORE: /usr/src/app/merlin_file_store JAVA_OPTS: > + -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005 -Dorg.slf4j.simpleLogger.defaultLogLevel=DEBUG -Dorg.slf4j.simpleLogger.log.com.zaxxer.hikari=INFO -Dorg.slf4j.simpleLogger.logFile=System.err @@ -147,6 +150,7 @@ services: MERLIN_WORKER_DB_USER: 'aerie' MERLIN_WORKER_LOCAL_STORE: /usr/src/app/merlin_file_store JAVA_OPTS: > + -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005 -Dorg.slf4j.simpleLogger.defaultLogLevel=DEBUG -Dorg.slf4j.simpleLogger.log.com.zaxxer.hikari=INFO -Dorg.slf4j.simpleLogger.logFile=System.err diff --git a/merlin-sdk/src/main/java/gov/nasa/jpl/aerie/merlin/protocol/types/SerializedValue.java b/merlin-sdk/src/main/java/gov/nasa/jpl/aerie/merlin/protocol/types/SerializedValue.java index 86505773db..6d9df651bc 100644 --- a/merlin-sdk/src/main/java/gov/nasa/jpl/aerie/merlin/protocol/types/SerializedValue.java +++ b/merlin-sdk/src/main/java/gov/nasa/jpl/aerie/merlin/protocol/types/SerializedValue.java @@ -6,15 +6,15 @@ import java.util.Optional; /** - * A serializable representation of an mission model-specific activity parameter domain object. + * A serializable representation of a mission model-specific activity parameter domain object. * - * A {@link SerializedValue} is an mission model-agnostic representation of the data in such an + * A {@link SerializedValue} is a mission model-agnostic representation of the data in such an * activity parameter, structured as serializable primitives composed using sequences and maps. * * This class is implemented using the Visitor pattern, following the approach considered at * http://blog.higher-order.com/blog/2009/08/21/structural-pattern-matching-in-java/. * Because a (de)serialization format (such as JSON) may have a fixed set of primitives types - * from which data may be composed. SerializedParameter ensures that all data boils down to + * from which data may be composed. SerializedValue ensures that all data boils down to * this fixed set of primitives. * * Note that, if the disk representation of a {@link SerializedValue} could have multiple parses @@ -24,13 +24,8 @@ * code would need to know about all possible subclasses for deserialization). The Visitor * pattern on a class closed to extension allows us to guarantee that no ambiguity occurs. */ -public abstract class SerializedValue { - static public SerializedValue NULL = SerializedValue.ofNull(); - - - // Closed type family -- the only legal subclasses are those defined within the body of - // this class definition. - private SerializedValue() {} +public sealed interface SerializedValue { + SerializedValue NULL = SerializedValue.ofNull(); /** * Calls the appropriate method of the passed {@link Visitor} depending on the kind of data @@ -41,7 +36,7 @@ private SerializedValue() {} * @return The result of calling {@code visitor.onX()}, where {@code X} depends on the * kind of data contained in this object. */ - public abstract T match(Visitor visitor); + T match(Visitor visitor); /** * An operation to be performed on the data contained in a {@link SerializedValue}. @@ -55,7 +50,7 @@ private SerializedValue() {} * * @param The return type of the operation represented by this {@link Visitor }. */ - public interface Visitor { + interface Visitor { T onNull(); T onReal(double value); T onInt(long value); @@ -65,18 +60,62 @@ public interface Visitor { T onList(List value); } + record NullValue() implements SerializedValue { + @Override + public T match(final Visitor visitor) { + return visitor.onNull(); + } + } + + record RealValue(double value) implements SerializedValue { + @Override + public T match(final Visitor visitor) { + return visitor.onReal(value); + } + } + + record IntValue(long value) implements SerializedValue { + @Override + public T match(final Visitor visitor) { + return visitor.onInt(value); + } + } + + record BooleanValue(boolean value) implements SerializedValue { + @Override + public T match(final Visitor visitor) { + return visitor.onBoolean(value); + } + } + + record StringValue(String value) implements SerializedValue { + @Override + public T match(final Visitor visitor) { + return visitor.onString(value); + } + } + + record MapValue(Map map) implements SerializedValue { + @Override + public T match(final Visitor visitor) { + return visitor.onMap(map); + } + } + + record ListValue(List list) implements SerializedValue { + @Override + public T match(final Visitor visitor) { + return visitor.onList(list); + } + } + /** * Creates a {@link SerializedValue} containing a null value. * * @return A new {@link SerializedValue} containing a null value. */ private static SerializedValue ofNull() { - return new SerializedValue() { - @Override - public T match(final Visitor visitor) { - return visitor.onNull(); - } - }; + return new NullValue(); } /** @@ -85,13 +124,8 @@ public T match(final Visitor visitor) { * @param value Any {@link double} value. * @return A new {@link SerializedValue} containing a real number. */ - public static SerializedValue of(final double value) { - return new SerializedValue() { - @Override - public T match(final Visitor visitor) { - return visitor.onReal(value); - } - }; + static SerializedValue of(final double value) { + return new RealValue(value); } /** @@ -100,13 +134,8 @@ public T match(final Visitor visitor) { * @param value Any {@link long} value. * @return A new {@link SerializedValue} containing an integral number. */ - public static SerializedValue of(final long value) { - return new SerializedValue() { - @Override - public T match(final Visitor visitor) { - return visitor.onInt(value); - } - }; + static SerializedValue of(final long value) { + return new IntValue(value); } /** @@ -115,13 +144,8 @@ public T match(final Visitor visitor) { * @param value Any {@link boolean} value. * @return A new {@link SerializedValue} containing a {@link boolean}. */ - public static SerializedValue of(final boolean value) { - return new SerializedValue() { - @Override - public T match(final Visitor visitor) { - return visitor.onBoolean(value); - } - }; + static SerializedValue of(final boolean value) { + return new BooleanValue(value); } /** @@ -130,14 +154,9 @@ public T match(final Visitor visitor) { * @param value Any {@link String} value. * @return A new {@link SerializedValue} containing a {@link String}. */ - public static SerializedValue of(final String value) { + static SerializedValue of(final String value) { Objects.requireNonNull(value); - return new SerializedValue() { - @Override - public T match(final Visitor visitor) { - return visitor.onString(value); - } - }; + return new StringValue(value); } /** @@ -146,32 +165,22 @@ public T match(final Visitor visitor) { * @param map Any set of named {@link SerializedValue}s. * @return A new {@link SerializedValue} containing a set of named {@link SerializedValue}s. */ - public static SerializedValue of(final Map map) { + static SerializedValue of(final Map map) { for (final var v : Objects.requireNonNull(map).values()) Objects.requireNonNull(v); final var value = Map.copyOf(map); - return new SerializedValue() { - @Override - public T match(final Visitor visitor) { - return visitor.onMap(value); - } - }; + return new MapValue(value); } /** * Creates a {@link SerializedValue} containing a list of {@link SerializedValue}s. * * @param list Any list of {@link SerializedValue}s. - * @return A new SerializedParameter containing a list of {@link SerializedValue}s. + * @return A new SerializedValue containing a list of {@link SerializedValue}s. */ - public static SerializedValue of(final List list) { + static SerializedValue of(final List list) { for (final var v : Objects.requireNonNull(list)) Objects.requireNonNull(v); final var value = List.copyOf(list); - return new SerializedValue() { - @Override - public T match(final Visitor visitor) { - return visitor.onList(value); - } - }; + return new ListValue(value); } @@ -183,7 +192,7 @@ public T match(final Visitor visitor) { * * @param The return type of the operation represented by this {@link Visitor}. */ - public static abstract class DefaultVisitor implements Visitor { + abstract class DefaultVisitor implements Visitor { protected abstract T onDefault(); @Override @@ -227,7 +236,7 @@ public T onList(final List value) { * * By default, all variants return {@code Optional.empty}. */ - public static abstract class OptionalVisitor extends DefaultVisitor> { + abstract class OptionalVisitor extends DefaultVisitor> { @Override protected Optional onDefault() { return Optional.empty(); @@ -239,7 +248,7 @@ protected Optional onDefault() { * * @return True if this object represents a null value, and false otherwise. */ - public boolean isNull() { + default boolean isNull() { return this.match(new DefaultVisitor<>() { @Override public Boolean onNull() { @@ -259,7 +268,7 @@ protected Boolean onDefault() { * @return An {@link Optional} containing a {@link double} if this object contains a real number. * Otherwise, returns an empty {@link Optional}. */ - public Optional asReal() { + default Optional asReal() { return this.match(new OptionalVisitor<>() { @Override public Optional onReal(final double value) { @@ -279,7 +288,7 @@ public Optional onInt(final long value) { * @return An {@link Optional} containing a {@link long} if this object contains an integral number. * Otherwise, returns an empty {@link Optional}. */ - public Optional asInt() { + default Optional asInt() { return this.match(new OptionalVisitor<>() { @Override public Optional onInt(final long value) { @@ -301,7 +310,7 @@ public Optional onReal(final double value) { * @return An {@link Optional} containing a {@link boolean} if this object contains a {@link boolean}. * Otherwise, returns an empty {@link Optional}. */ - public Optional asBoolean() { + default Optional asBoolean() { return this.match(new OptionalVisitor<>() { @Override public Optional onBoolean(final boolean value) { @@ -316,7 +325,7 @@ public Optional onBoolean(final boolean value) { * @return An {@link Optional} containing a {@link String} if this object contains a {@link String}. * Otherwise, returns an empty {@link Optional}. */ - public Optional asString() { + default Optional asString() { return this.match(new OptionalVisitor<>() { @Override public Optional onString(final String value) { @@ -326,12 +335,12 @@ public Optional onString(final String value) { } /** - * Attempts to access the data in this object as a map of named {@code SerializedParameter}s. + * Attempts to access the data in this object as a map of named {@code SerializedValue}s. * * @return An {@link Optional} containing a map if this object contains a map. * Otherwise, returns an empty {@link Optional}. */ - public Optional> asMap() { + default Optional> asMap() { return this.match(new OptionalVisitor<>() { @Override public Optional> onMap(final Map value) { @@ -341,12 +350,12 @@ public Optional> onMap(final Map> asList() { + default Optional> asList() { return this.match(new OptionalVisitor<>() { @Override public Optional> onList(final List value) { @@ -354,87 +363,4 @@ public Optional> onList(final List value) } }); } - - @Override - public String toString() { - return this.match(new Visitor<>() { - @Override - public String onNull() { - return "null"; - } - - @Override - public String onReal(final double value) { - return String.valueOf(value); - } - - @Override - public String onInt(final long value) { - return String.valueOf(value); - } - - @Override - public String onBoolean(final boolean value) { - return String.valueOf(value); - } - - @Override - public String onString(final String value) { - return value; - } - - @Override - public String onMap(final Map value) { - return String.valueOf(value); - } - - @Override - public String onList(final List value) { - return String.valueOf(value); - } - }); - } - - @Override - public boolean equals(final Object o) { - if (!(o instanceof SerializedValue)) return false; - final var other = (SerializedValue) o; - - return this.match(new Visitor<>() { - @Override - public Boolean onNull() { - return other.isNull(); - } - - @Override - public Boolean onReal(final double value) { - return other.asReal().map(x -> x == value).orElse(false); - } - - @Override - public Boolean onInt(final long value) { - return other.asInt().map(x -> x == value).orElse(false); - } - - @Override - public Boolean onBoolean(final boolean value) { - return other.asBoolean().map(x -> x == value).orElse(false); - } - - @Override - public Boolean onString(final String value) { - return other.asString().map(x -> Objects.equals(x, value)).orElse(false); - } - - @Override - public Boolean onMap(final Map value) { - return other.asMap().map(x -> Objects.equals(x, value)).orElse(false); - } - - @Override - public Boolean onList(final List value) { - return other.asList().map(x -> Objects.equals(x, value)).orElse(false); - } - }); - } } diff --git a/merlin-server/src/test/java/gov/nasa/jpl/aerie/merlin/server/services/ConstraintsDSLCompilationServiceTests.java b/merlin-server/src/test/java/gov/nasa/jpl/aerie/merlin/server/services/ConstraintsDSLCompilationServiceTests.java index 5a75e11748..e8a254538d 100644 --- a/merlin-server/src/test/java/gov/nasa/jpl/aerie/merlin/server/services/ConstraintsDSLCompilationServiceTests.java +++ b/merlin-server/src/test/java/gov/nasa/jpl/aerie/merlin/server/services/ConstraintsDSLCompilationServiceTests.java @@ -195,7 +195,7 @@ export default () => { return Discrete.Resource("an integer").notEqual(4.0); } """, - new ViolationsOf(new NotEqual<>(new DiscreteResource("an integer"), new DiscreteValue(SerializedValue.of(4.0)))) + new ViolationsOf(new NotEqual<>(new DiscreteResource("an integer"), new DiscreteValue(SerializedValue.of(4)))) ); } diff --git a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/services/GraphQLMerlinService.java b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/services/GraphQLMerlinService.java index d98729dc5b..2979c595af 100644 --- a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/services/GraphQLMerlinService.java +++ b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/services/GraphQLMerlinService.java @@ -46,6 +46,7 @@ import java.util.Map; import java.util.Optional; import java.util.function.Supplier; +import java.util.stream.Collectors; import static gov.nasa.jpl.aerie.scheduler.server.graphql.GraphQLParsers.parseGraphQLInterval; import static gov.nasa.jpl.aerie.scheduler.server.graphql.GraphQLParsers.parseGraphQLTimestamp; @@ -268,7 +269,7 @@ public PlanId createEmptyPlan(final String name, final long modelId, final Insta //NB: the duration format for creating plans is different than that for activity instances (microseconds) final var durStr = "\"" + duration.in(Duration.SECOND) + "\""; final var request = requestFormat.formatted( - getGraphQLValueString(name), modelId, getGraphQLValueString(startTime), durStr); + serializeForGql(name), modelId, serializeForGql(startTime.toString()), durStr); final var response = postRequest(request).orElseThrow(() -> new NoSuchPlanException(null)); try { @@ -373,7 +374,7 @@ public void updateActivity(final PlanId planId, final MerlinActivityInstance act final var argumentsSb = new StringBuilder(); for (final var arg : activity.arguments().entrySet()) { final var name = arg.getKey(); - var value = getGraphQLValueString(arg.getValue()); + var value = serializeForGql(arg.getValue()); argumentsSb.append(argFormat.formatted(name, value)); } final var updateReq = """ @@ -473,13 +474,14 @@ private Map createActivities(final PlanId //add duration to parameters if controllable if(act.getType().getDurationType() instanceof DurationType.Controllable durationType){ if(!act.getArguments().containsKey(durationType.parameterName())){ - requestSB.append(argFormat.formatted(durationType.parameterName(), getGraphQLValueString(act.getDuration()))); + requestSB.append(argFormat.formatted(durationType.parameterName(), serializeForGql(act.getDuration()))); } } for (final var arg : act.getArguments().entrySet()) { final var name = arg.getKey(); - var value = getGraphQLValueString(arg.getValue()); - requestSB.append(argFormat.formatted(name, value)); + final var value = arg.getValue(); + final var gqlValue = serializeForGql(value); + requestSB.append(argFormat.formatted(name, gqlValue)); } requestSB.append(actPost); } @@ -628,24 +630,66 @@ private Collection getResourceTypes(final MissionModelId missionMo } /** - * serialize the given java object in a manner that can be used as a graphql argument value - * - * eg wraps strings or enums in quotes - * - * @param obj the object to serialize + * serialize the given string in a manner that can be used as a graphql argument value + * @param s the string to serialize * @return a serialization of the object suitable for use as a graphql value */ - public String getGraphQLValueString(Object obj) { + public String serializeForGql(final String s) { //TODO: can probably leverage some serializers from aerie - if (obj instanceof String || obj instanceof Enum || obj instanceof Instant) { - //TODO: (defensive) should escape contents of bare strings, eg internal quotes - //NB: Time::toString will format correctly as HH:MM:SS.sss, just need to quote it here - return "\"" + obj + "\""; - } else if (obj instanceof Duration dur) { - //NB: merlin uses durations in microseconds! (inconsistent with start_offset as a HH:MM:SS.sss string) - return Long.toString(dur.in(Duration.MICROSECOND)); - } else { - return obj.toString(); - } + //TODO: (defensive) should escape contents of bare strings, eg internal quotes + //NB: Time::toString will format correctly as HH:MM:SS.sss, just need to quote it here + return "\"" + s + "\""; + } + + /** + * serialize the given duration in a manner that can be used as a graphql argument value + * @param d the duration to serialize + * @return a serialization of the object suitable for use as a graphql value + */ + public String serializeForGql(final Duration d) { + //TODO: can probably leverage some serializers from aerie + //NB: merlin uses durations in microseconds! (inconsistent with start_offset as a HH:MM:SS.sss string) + return Long.toString(d.in(Duration.MICROSECOND)); + } + + public String serializeForGql(final SerializedValue value) { + return value.match(new SerializedValue.Visitor<>() { + @Override + public String onNull() { + return "null"; + } + + @Override + public String onReal(final double value) { + return String.valueOf(value); + } + + @Override + public String onInt(final long value) { + return String.valueOf(value); + } + + @Override + public String onBoolean(final boolean value) { + return value ? "true" : "false"; + } + + @Override + public String onString(final String value) { + return serializeForGql(value); + } + + @Override + public String onMap(final Map value) { + return "{%s}".formatted(value.entrySet().stream() + .map(e -> "\"%s\": %s".formatted( //TODO: (defensive) should escape contents of bare strings, eg internal quotes + e.getKey(), serializeForGql(e.getValue()))).collect(Collectors.joining(","))); + } + + @Override + public String onList(final List value) { + return "[%s]".formatted(value.stream().map(v -> serializeForGql(v)).collect(Collectors.joining(","))); + } + }); } } diff --git a/scheduler/src/test/resources/gov/nasa/jpl/aerie/scheduler/aerielander.jar b/scheduler/src/test/resources/gov/nasa/jpl/aerie/scheduler/aerielander.jar index ae2c5e5397..af54b271a6 100644 Binary files a/scheduler/src/test/resources/gov/nasa/jpl/aerie/scheduler/aerielander.jar and b/scheduler/src/test/resources/gov/nasa/jpl/aerie/scheduler/aerielander.jar differ