Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
124 changes: 98 additions & 26 deletions src/org/rascalmpl/ideservices/GsonUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@
package org.rascalmpl.ideservices;

import java.io.IOException;
import java.io.StringWriter;
import java.util.List;
import java.util.function.Consumer;

import org.checkerframework.checker.nullness.qual.Nullable;
import org.rascalmpl.interpreter.NullRascalMonitor;
Expand Down Expand Up @@ -71,7 +71,6 @@
*/
public class GsonUtils {
private static final JsonValueWriter writer = new JsonValueWriter();
private static final JsonValueReader reader = new JsonValueReader(IRascalValueFactory.getInstance(), new TypeStore(), new NullRascalMonitor(), null);
private static final TypeFactory tf = TypeFactory.getInstance();

private static final List<TypeMapping> typeMappings;
Expand Down Expand Up @@ -100,9 +99,30 @@ public class GsonUtils {
}

public static enum ComplexTypeMode {
/**
* All values are serialized as JSON objects. Automatic deserialization is only supported for primitive types (`bool`,
* `datetime`, `int`, `rat`, `real`, `loc`, `str`, `num`); more complex types cannot be automatically deserialized as
* the type is not available at deserialization time.
*
* Rationals are wrapped in an object with `rat` as key, to avoid an ambiguity between a rational's JSON representation
* (a list) and a list as a sole argument on the JSON-RPC level.
*/
ENCODE_AS_JSON_OBJECT,

/**
* Complex values are serialized as a (binary) Base64-encoded string. An appropriate {@link TypeStore} must be provided for
* deserialization with {@link ComplexTypeMode#base64Decode}.
*/
ENCODE_AS_BASE64_STRING,

/**
* Complex values are serialized as a string.
*/
ENCODE_AS_STRING,

/**
* Only primitive types are supported; more complex types are neither serialized nor deserialized.
*/
NOT_SUPPORTED
}

Expand All @@ -129,15 +149,16 @@ public boolean supports(Class<?> incoming) {
return clazz.isAssignableFrom(incoming);
}

public <T> TypeAdapter<T> createAdapter(ComplexTypeMode complexTypeMode) {
public <T> TypeAdapter<T> createAdapter(ComplexTypeMode complexTypeMode, TypeStore ts) {
JsonValueReader reader = new JsonValueReader(IRascalValueFactory.getInstance(), ts, new NullRascalMonitor(), null);
if (isPrimitive) {
var needsWrapping = complexTypeMode == ComplexTypeMode.ENCODE_AS_JSON_OBJECT && type.isSubtypeOf(tf.rationalType());
return new TypeAdapter<T>() {
@Override
public void write(JsonWriter out, T value) throws IOException {
var needsWrapping = needsWrapping(type, complexTypeMode);
if (needsWrapping) {
out.beginObject();
out.name("val");
out.name("rat");
}
writer.write(out, (IValue) value);
if (needsWrapping) {
Expand All @@ -148,7 +169,6 @@ public void write(JsonWriter out, T value) throws IOException {
@SuppressWarnings("unchecked")
@Override
public T read(JsonReader in) throws IOException {
var needsWrapping = needsWrapping(type, complexTypeMode);
if (needsWrapping) {
in.beginObject();
in.nextName();
Expand All @@ -166,15 +186,7 @@ public T read(JsonReader in) throws IOException {
public void write(JsonWriter out, T value) throws IOException {
switch (complexTypeMode) {
case ENCODE_AS_JSON_OBJECT:
var needsWrapping = needsWrapping(type, complexTypeMode);
if (needsWrapping) {
out.beginObject();
out.name("val");
}
writer.write(out, (IValue) value);
if (needsWrapping) {
out.endObject();
}
break;
case ENCODE_AS_BASE64_STRING:
out.value(base64Encode((IValue) value));
Expand All @@ -189,29 +201,89 @@ public void write(JsonWriter out, T value) throws IOException {
}
}

@SuppressWarnings("unchecked")
@Override
public T read(JsonReader in) throws IOException {
throw new IOException("Cannot handle complex type");
switch (complexTypeMode) {
case ENCODE_AS_BASE64_STRING:
return base64Decode(in.nextString(), ts);
case ENCODE_AS_STRING:
return (T) reader.read(in, type);
default:
throw new IOException("Cannot handle complex type");
}
}
};
}
}


/**
* Configure Gson to encode complex (non-primitive) values as JSON objects.
*
* See {@link ComplexTypeMode.ENCODE_AS_JSON_OBJECT}.
*
* @param builder The {@link GsonBuilder} to be configured.
*/
public static Consumer<GsonBuilder> complexAsJsonObject() {
return builder -> configureGson(builder, ComplexTypeMode.ENCODE_AS_JSON_OBJECT, new TypeStore());
}

/**
* IValues that are encoded as a (JSON) list need to be wrapped in an object to avoid Gson accidentally unpacking the list
* @param type
* @param complexTypeMode
* @return whether or not wrapping is required
* Configure Gson to encode complex (non-primitive) values as Base64-encoded strings.
*
* This configurtion should only be used for serialization; deserialization requires a {@link TypeStore).
*
* @param builder The {@link GsonBuilder} to be configured.
*/
private static boolean needsWrapping(Type type, ComplexTypeMode complexTypeMode) {
return complexTypeMode == ComplexTypeMode.ENCODE_AS_JSON_OBJECT && type == null || type.isSubtypeOf(tf.rationalType());
public static Consumer<GsonBuilder> complexAsBase64String() {
return builder -> complexAsBase64String(new TypeStore());
}

public static void configureGson(GsonBuilder builder) {
configureGson(builder, ComplexTypeMode.ENCODE_AS_JSON_OBJECT);
/**
* Configure Gson to encode complex (non-primitive) values as Base64-encoded strings.
*
* This configuration can be used for both serialization and deserialization.
*
* @param builder The {@link GsonBuilder} to be configured.
* @param ts The {@link TypeStore} to be used during deserialization.
*/
public static Consumer<GsonBuilder> complexAsBase64String(TypeStore ts) {
return builder -> configureGson(builder, ComplexTypeMode.ENCODE_AS_BASE64_STRING, ts);
}

/**
* Configure Gson to encode complex (non-primitive) values as plain strings.
*
* This configurtion should only be used for serialization; deserialization requires a {@link TypeStore).
*
* @param builder The {@link GsonBuilder} to be configured.
*/
public static Consumer<GsonBuilder> complexAsString() {
return builder -> complexAsString(new TypeStore());
}

/**
* Configure Gson to encode complex (non-primitive) values as plain strings.
*
* This configuration can be used for both serialization and deserialization.
*
* @param builder The {@link GsonBuilder} to be configured.
* @param ts The {@link TypeStore} to be used during deserialization.
*/
public static Consumer<GsonBuilder> complexAsString(TypeStore ts) {
return builder -> configureGson(builder, ComplexTypeMode.ENCODE_AS_STRING, ts);
}

/**
* Configure Gson to encode encode primitive values only. Complex values raise an exception.
*
* @param builder The {@link GsonBuilder} to be configured.
*/
public static Consumer<GsonBuilder> noComplexTypes() {
return builder -> configureGson(builder, ComplexTypeMode.NOT_SUPPORTED, new TypeStore());
}

public static void configureGson(GsonBuilder builder, ComplexTypeMode complexTypeMode) {
public static void configureGson(GsonBuilder builder, ComplexTypeMode complexTypeMode, TypeStore ts) {
builder.registerTypeAdapterFactory(new TypeAdapterFactory() {
@Override
public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
Expand All @@ -222,7 +294,7 @@ public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
return typeMappings.stream()
.filter(m -> m.supports(rawType))
.findFirst()
.map(m -> m.<T>createAdapter(complexTypeMode))
.map(m -> m.<T>createAdapter(complexTypeMode, ts))
.orElse(null);
}
});
Expand Down
2 changes: 1 addition & 1 deletion src/org/rascalmpl/ideservices/RemoteIDEServices.java
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ public RemoteIDEServices(int ideServicesPort, PrintWriter stderr, IRascalMonitor
.setLocalService(this)
.setInput(socket.getInputStream())
.setOutput(socket.getOutputStream())
.configureGson(GsonUtils::configureGson)
.configureGson(GsonUtils.complexAsJsonObject())
.setExecutorService(DaemonThreadPool.buildConstrainedCached("rascal-ide-services", Math.max(2, Math.min(6, Runtime.getRuntime().availableProcessors() - 2))))
.create();

Expand Down
110 changes: 110 additions & 0 deletions test/org/rascalmpl/test/rpc/ComplexAsBase64String.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
package org.rascalmpl.test.rpc;

import java.io.IOException;

import org.junit.BeforeClass;
import org.rascalmpl.ideservices.GsonUtils;

import io.usethesource.vallang.IBool;
import io.usethesource.vallang.IDateTime;
import io.usethesource.vallang.IInteger;
import io.usethesource.vallang.IMapWriter;
import io.usethesource.vallang.IReal;
import io.usethesource.vallang.type.Type;
import io.usethesource.vallang.type.TypeFactory;
import io.usethesource.vallang.type.TypeStore;

public class ComplexAsBase64String extends IValueOverJsonTests {
private static TypeFactory tf = TypeFactory.getInstance();
private static TypeStore ts = new TypeStore();
private static final Type TestAdt = tf.abstractDataType(ts, "TestAdt");
private static final Type TestAdt_testCons = tf.constructor(ts, TestAdt, "testCons", tf.stringType(), "id", tf.integerType(), "nr");

@BeforeClass
public static void setup() throws IOException {
startTestServerAndClient(GsonUtils.complexAsBase64String(ts));
}

@Override
public void testSendBool() {
expectSuccessful("IBool", () -> (IBool) prelude.arbBool(), testServer::sendBool);
}

@Override
public void testSendConstructor() {
expectSuccessful("IConstructor", () -> vf.constructor(TestAdt_testCons, vf.string("hi"), vf.integer(38)), testServer::sendConstructor);
}

@Override
public void testSendDateTime() {
expectSuccessful("IDateTime", () -> (IDateTime) prelude.arbDateTime(), testServer::sendDateTime);
}

@Override
public void testSendInteger() {
expectSuccessful("IInteger", () -> (IInteger) math.arbInt(), testServer::sendInteger);
}

@Override
public void testSendNode() {
expectSuccessful("INode", () -> prelude.arbNode(), testServer::sendNode);
}

@Override
public void testSendRational() {
// expectSuccessful("IRational", () -> arbRational(), testServer::sendRational);
}

@Override
public void testSendReal() {
expectSuccessful("IReal", () -> (IReal) math.arbReal(), testServer::sendReal);
}

@Override
public void testSendLocation() {
expectSuccessful("ISourceLocation", () -> prelude.arbLoc(), testServer::sendLocation);
}

@Override
public void testSendString() {
expectSuccessful("IString", () -> prelude.arbString(vf.integer(1024)), testServer::sendString);
}

@Override
public void testSendIntAsNumber() {
expectSuccessful("INumber", () -> (IInteger) math.arbInt(), testServer::sendNumber);
}

@Override
public void testSendRealAsNumber() {
expectSuccessful("INumber", () -> (IReal) math.arbReal(), testServer::sendNumber);
}

@Override
public void testSendRealAsValue() {
expectSuccessful("IValue", () -> (IReal) math.arbReal(), testServer::sendReal);
}

@Override
public void testSendList() {
expectSuccessful("IList", () -> vf.list(vf.string(""), vf.integer(0)), testServer::sendList);
}

@Override
public void testSendMap() {
IMapWriter writer = vf.mapWriter();
writer.put(vf.integer(0), vf.string("zero"));
writer.put(vf.integer(1), vf.string("one"));
expectSuccessful("IMap", () -> writer.done(), testServer::sendMap);
}

@Override
public void testSendSet() {
expectSuccessful("ISet", () -> vf.set(vf.integer(0), vf.integer(1), vf.integer(2)), testServer::sendSet);
}

@Override
public void testSendTuple() {
expectSuccessful("ITuple", () -> vf.tuple(vf.integer(0), vf.integer(1)), testServer::sendTuple);
}
}
Loading
Loading