aboutsummaryrefslogtreecommitdiffstats
path: root/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/servlet
diff options
context:
space:
mode:
authorLibravatar Kristóf Marussy <marussy@mit.bme.hu>2023-09-14 19:29:36 +0200
committerLibravatar GitHub <noreply@github.com>2023-09-14 19:29:36 +0200
commit98ed3b6db5f4e51961a161050cc31c66015116e8 (patch)
tree8bfd6d9bc8d6ed23b9eb0f889dd40b6c24fe8f92 /subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/servlet
parentMerge pull request #38 from nagilooh/design-space-exploration (diff)
parentMerge remote-tracking branch 'upstream/main' into partial-interpretation (diff)
downloadrefinery-98ed3b6db5f4e51961a161050cc31c66015116e8.tar.gz
refinery-98ed3b6db5f4e51961a161050cc31c66015116e8.tar.zst
refinery-98ed3b6db5f4e51961a161050cc31c66015116e8.zip
Merge pull request #39 from kris7t/partial-interpretation
Implement partial interpretation based model generation
Diffstat (limited to 'subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/servlet')
-rw-r--r--subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/servlet/RuntimeTypeAdapterFactory.java304
-rw-r--r--subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/servlet/XtextWebSocket.java26
2 files changed, 325 insertions, 5 deletions
diff --git a/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/servlet/RuntimeTypeAdapterFactory.java b/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/servlet/RuntimeTypeAdapterFactory.java
new file mode 100644
index 00000000..b16cf7df
--- /dev/null
+++ b/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/servlet/RuntimeTypeAdapterFactory.java
@@ -0,0 +1,304 @@
1/*
2 * Copyright (C) 2011 Google Inc.
3 * Copyright (C) 2023 The Refinery Authors <https://refinery.tools/>
4 *
5 * SPDX-License-Identifier: Apache-2.0
6 *
7 * Licensed under the Apache License, Version 2.0 (the "License");
8 * you may not use this file except in compliance with the License.
9 * You may obtain a copy of the License at
10 *
11 * http://www.apache.org/licenses/LICENSE-2.0
12 *
13 * Unless required by applicable law or agreed to in writing, software
14 * distributed under the License is distributed on an "AS IS" BASIS,
15 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16 * See the License for the specific language governing permissions and
17 * limitations under the License.
18 *
19 * This file was copied into Refinery according to upstream instructions at
20 * https://github.com/google/gson/issues/1104#issuecomment-309582470.
21 * However, we changed the package name below to avoid potential clashes
22 * with other jars on the classpath.
23 */
24package tools.refinery.language.web.xtext.servlet;
25
26import com.google.errorprone.annotations.CanIgnoreReturnValue;
27import com.google.gson.Gson;
28import com.google.gson.JsonElement;
29import com.google.gson.JsonObject;
30import com.google.gson.JsonParseException;
31import com.google.gson.JsonPrimitive;
32import com.google.gson.TypeAdapter;
33import com.google.gson.TypeAdapterFactory;
34import com.google.gson.reflect.TypeToken;
35import com.google.gson.stream.JsonReader;
36import com.google.gson.stream.JsonWriter;
37import java.io.IOException;
38import java.util.LinkedHashMap;
39import java.util.Map;
40
41/**
42 * Adapts values whose runtime type may differ from their declaration type. This
43 * is necessary when a field's type is not the same type that GSON should create
44 * when deserializing that field. For example, consider these types:
45 * <pre> {@code
46 * abstract class Shape {
47 * int x;
48 * int y;
49 * }
50 * class Circle extends Shape {
51 * int radius;
52 * }
53 * class Rectangle extends Shape {
54 * int width;
55 * int height;
56 * }
57 * class Diamond extends Shape {
58 * int width;
59 * int height;
60 * }
61 * class Drawing {
62 * Shape bottomShape;
63 * Shape topShape;
64 * }
65 * }</pre>
66 * <p>Without additional type information, the serialized JSON is ambiguous. Is
67 * the bottom shape in this drawing a rectangle or a diamond? <pre> {@code
68 * {
69 * "bottomShape": {
70 * "width": 10,
71 * "height": 5,
72 * "x": 0,
73 * "y": 0
74 * },
75 * "topShape": {
76 * "radius": 2,
77 * "x": 4,
78 * "y": 1
79 * }
80 * }}</pre>
81 * This class addresses this problem by adding type information to the
82 * serialized JSON and honoring that type information when the JSON is
83 * deserialized: <pre> {@code
84 * {
85 * "bottomShape": {
86 * "type": "Diamond",
87 * "width": 10,
88 * "height": 5,
89 * "x": 0,
90 * "y": 0
91 * },
92 * "topShape": {
93 * "type": "Circle",
94 * "radius": 2,
95 * "x": 4,
96 * "y": 1
97 * }
98 * }}</pre>
99 * Both the type field name ({@code "type"}) and the type labels ({@code
100 * "Rectangle"}) are configurable.
101 *
102 * <h2>Registering Types</h2>
103 * Create a {@code RuntimeTypeAdapterFactory} by passing the base type and type field
104 * name to the {@link #of} factory method. If you don't supply an explicit type
105 * field name, {@code "type"} will be used. <pre> {@code
106 * RuntimeTypeAdapterFactory<Shape> shapeAdapterFactory
107 * = RuntimeTypeAdapterFactory.of(Shape.class, "type");
108 * }</pre>
109 * Next register all of your subtypes. Every subtype must be explicitly
110 * registered. This protects your application from injection attacks. If you
111 * don't supply an explicit type label, the type's simple name will be used.
112 * <pre> {@code
113 * shapeAdapterFactory.registerSubtype(Rectangle.class, "Rectangle");
114 * shapeAdapterFactory.registerSubtype(Circle.class, "Circle");
115 * shapeAdapterFactory.registerSubtype(Diamond.class, "Diamond");
116 * }</pre>
117 * Finally, register the type adapter factory in your application's GSON builder:
118 * <pre> {@code
119 * Gson gson = new GsonBuilder()
120 * .registerTypeAdapterFactory(shapeAdapterFactory)
121 * .create();
122 * }</pre>
123 * Like {@code GsonBuilder}, this API supports chaining: <pre> {@code
124 * RuntimeTypeAdapterFactory<Shape> shapeAdapterFactory = RuntimeTypeAdapterFactory.of(Shape.class)
125 * .registerSubtype(Rectangle.class)
126 * .registerSubtype(Circle.class)
127 * .registerSubtype(Diamond.class);
128 * }</pre>
129 *
130 * <h2>Serialization and deserialization</h2>
131 * In order to serialize and deserialize a polymorphic object,
132 * you must specify the base type explicitly.
133 * <pre> {@code
134 * Diamond diamond = new Diamond();
135 * String json = gson.toJson(diamond, Shape.class);
136 * }</pre>
137 * And then:
138 * <pre> {@code
139 * Shape shape = gson.fromJson(json, Shape.class);
140 * }</pre>
141 */
142public final class RuntimeTypeAdapterFactory<T> implements TypeAdapterFactory {
143 private final Class<?> baseType;
144 private final String typeFieldName;
145 private final Map<String, Class<?>> labelToSubtype = new LinkedHashMap<>();
146 private final Map<Class<?>, String> subtypeToLabel = new LinkedHashMap<>();
147 private final boolean maintainType;
148 private boolean recognizeSubtypes;
149
150 private RuntimeTypeAdapterFactory(
151 Class<?> baseType, String typeFieldName, boolean maintainType) {
152 if (typeFieldName == null || baseType == null) {
153 throw new NullPointerException();
154 }
155 this.baseType = baseType;
156 this.typeFieldName = typeFieldName;
157 this.maintainType = maintainType;
158 }
159
160 /**
161 * Creates a new runtime type adapter using for {@code baseType} using {@code
162 * typeFieldName} as the type field name. Type field names are case sensitive.
163 *
164 * @param maintainType true if the type field should be included in deserialized objects
165 */
166 public static <T> RuntimeTypeAdapterFactory<T> of(Class<T> baseType, String typeFieldName, boolean maintainType) {
167 return new RuntimeTypeAdapterFactory<>(baseType, typeFieldName, maintainType);
168 }
169
170 /**
171 * Creates a new runtime type adapter using for {@code baseType} using {@code
172 * typeFieldName} as the type field name. Type field names are case sensitive.
173 */
174 public static <T> RuntimeTypeAdapterFactory<T> of(Class<T> baseType, String typeFieldName) {
175 return new RuntimeTypeAdapterFactory<>(baseType, typeFieldName, false);
176 }
177
178 /**
179 * Creates a new runtime type adapter for {@code baseType} using {@code "type"} as
180 * the type field name.
181 */
182 public static <T> RuntimeTypeAdapterFactory<T> of(Class<T> baseType) {
183 return new RuntimeTypeAdapterFactory<>(baseType, "type", false);
184 }
185
186 /**
187 * Ensures that this factory will handle not just the given {@code baseType}, but any subtype
188 * of that type.
189 */
190 @CanIgnoreReturnValue
191 public RuntimeTypeAdapterFactory<T> recognizeSubtypes() {
192 this.recognizeSubtypes = true;
193 return this;
194 }
195
196 /**
197 * Registers {@code type} identified by {@code label}. Labels are case
198 * sensitive.
199 *
200 * @throws IllegalArgumentException if either {@code type} or {@code label}
201 * have already been registered on this type adapter.
202 */
203 @CanIgnoreReturnValue
204 public RuntimeTypeAdapterFactory<T> registerSubtype(Class<? extends T> type, String label) {
205 if (type == null || label == null) {
206 throw new NullPointerException();
207 }
208 if (subtypeToLabel.containsKey(type) || labelToSubtype.containsKey(label)) {
209 throw new IllegalArgumentException("types and labels must be unique");
210 }
211 labelToSubtype.put(label, type);
212 subtypeToLabel.put(type, label);
213 return this;
214 }
215
216 /**
217 * Registers {@code type} identified by its {@link Class#getSimpleName simple
218 * name}. Labels are case sensitive.
219 *
220 * @throws IllegalArgumentException if either {@code type} or its simple name
221 * have already been registered on this type adapter.
222 */
223 @CanIgnoreReturnValue
224 public RuntimeTypeAdapterFactory<T> registerSubtype(Class<? extends T> type) {
225 return registerSubtype(type, type.getSimpleName());
226 }
227
228 @Override
229 public <R> TypeAdapter<R> create(Gson gson, TypeToken<R> type) {
230 if (type == null) {
231 return null;
232 }
233 Class<?> rawType = type.getRawType();
234 boolean handle =
235 recognizeSubtypes ? baseType.isAssignableFrom(rawType) : baseType.equals(rawType);
236 if (!handle) {
237 return null;
238 }
239
240 final TypeAdapter<JsonElement> jsonElementAdapter = gson.getAdapter(JsonElement.class);
241 final Map<String, TypeAdapter<?>> labelToDelegate = new LinkedHashMap<>();
242 final Map<Class<?>, TypeAdapter<?>> subtypeToDelegate = new LinkedHashMap<>();
243 for (Map.Entry<String, Class<?>> entry : labelToSubtype.entrySet()) {
244 TypeAdapter<?> delegate = gson.getDelegateAdapter(this, TypeToken.get(entry.getValue()));
245 labelToDelegate.put(entry.getKey(), delegate);
246 subtypeToDelegate.put(entry.getValue(), delegate);
247 }
248
249 return new TypeAdapter<R>() {
250 @Override public R read(JsonReader in) throws IOException {
251 JsonElement jsonElement = jsonElementAdapter.read(in);
252 JsonElement labelJsonElement;
253 if (maintainType) {
254 labelJsonElement = jsonElement.getAsJsonObject().get(typeFieldName);
255 } else {
256 labelJsonElement = jsonElement.getAsJsonObject().remove(typeFieldName);
257 }
258
259 if (labelJsonElement == null) {
260 throw new JsonParseException("cannot deserialize " + baseType
261 + " because it does not define a field named " + typeFieldName);
262 }
263 String label = labelJsonElement.getAsString();
264 @SuppressWarnings("unchecked") // registration requires that subtype extends T
265 TypeAdapter<R> delegate = (TypeAdapter<R>) labelToDelegate.get(label);
266 if (delegate == null) {
267 throw new JsonParseException("cannot deserialize " + baseType + " subtype named "
268 + label + "; did you forget to register a subtype?");
269 }
270 return delegate.fromJsonTree(jsonElement);
271 }
272
273 @Override public void write(JsonWriter out, R value) throws IOException {
274 Class<?> srcType = value.getClass();
275 String label = subtypeToLabel.get(srcType);
276 @SuppressWarnings("unchecked") // registration requires that subtype extends T
277 TypeAdapter<R> delegate = (TypeAdapter<R>) subtypeToDelegate.get(srcType);
278 if (delegate == null) {
279 throw new JsonParseException("cannot serialize " + srcType.getName()
280 + "; did you forget to register a subtype?");
281 }
282 JsonObject jsonObject = delegate.toJsonTree(value).getAsJsonObject();
283
284 if (maintainType) {
285 jsonElementAdapter.write(out, jsonObject);
286 return;
287 }
288
289 JsonObject clone = new JsonObject();
290
291 if (jsonObject.has(typeFieldName)) {
292 throw new JsonParseException("cannot serialize " + srcType.getName()
293 + " because it already defines a field named " + typeFieldName);
294 }
295 clone.add(typeFieldName, new JsonPrimitive(label));
296
297 for (Map.Entry<String, JsonElement> e : jsonObject.entrySet()) {
298 clone.add(e.getKey(), e.getValue());
299 }
300 jsonElementAdapter.write(out, clone);
301 }
302 }.nullSafe();
303 }
304}
diff --git a/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/servlet/XtextWebSocket.java b/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/servlet/XtextWebSocket.java
index 043d318c..1fde1be5 100644
--- a/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/servlet/XtextWebSocket.java
+++ b/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/servlet/XtextWebSocket.java
@@ -6,6 +6,7 @@
6package tools.refinery.language.web.xtext.servlet; 6package tools.refinery.language.web.xtext.servlet;
7 7
8import com.google.gson.Gson; 8import com.google.gson.Gson;
9import com.google.gson.GsonBuilder;
9import com.google.gson.JsonIOException; 10import com.google.gson.JsonIOException;
10import com.google.gson.JsonParseException; 11import com.google.gson.JsonParseException;
11import org.eclipse.jetty.websocket.api.Callback; 12import org.eclipse.jetty.websocket.api.Callback;
@@ -16,6 +17,7 @@ import org.eclipse.xtext.resource.IResourceServiceProvider;
16import org.eclipse.xtext.web.server.ISession; 17import org.eclipse.xtext.web.server.ISession;
17import org.slf4j.Logger; 18import org.slf4j.Logger;
18import org.slf4j.LoggerFactory; 19import org.slf4j.LoggerFactory;
20import tools.refinery.language.semantics.metadata.*;
19import tools.refinery.language.web.xtext.server.ResponseHandler; 21import tools.refinery.language.web.xtext.server.ResponseHandler;
20import tools.refinery.language.web.xtext.server.ResponseHandlerException; 22import tools.refinery.language.web.xtext.server.ResponseHandlerException;
21import tools.refinery.language.web.xtext.server.TransactionExecutor; 23import tools.refinery.language.web.xtext.server.TransactionExecutor;
@@ -28,7 +30,15 @@ import java.io.Reader;
28public class XtextWebSocket implements ResponseHandler { 30public class XtextWebSocket implements ResponseHandler {
29 private static final Logger LOG = LoggerFactory.getLogger(XtextWebSocket.class); 31 private static final Logger LOG = LoggerFactory.getLogger(XtextWebSocket.class);
30 32
31 private final Gson gson = new Gson(); 33 private final Gson gson = new GsonBuilder()
34 .disableJdkUnsafe()
35 .registerTypeAdapterFactory(RuntimeTypeAdapterFactory.of(RelationDetail.class, "type")
36 .registerSubtype(ClassDetail.class, "class")
37 .registerSubtype(ReferenceDetail.class, "reference")
38 .registerSubtype(OppositeReferenceDetail.class, "opposite")
39 .registerSubtype(PredicateDetail.class, "predicate")
40 .registerSubtype(BuiltInDetail.class, "builtin"))
41 .create();
32 42
33 private final TransactionExecutor executor; 43 private final TransactionExecutor executor;
34 44
@@ -70,10 +80,11 @@ public class XtextWebSocket implements ResponseHandler {
70 80
71 @OnWebSocketError 81 @OnWebSocketError
72 public void onError(Throwable error) { 82 public void onError(Throwable error) {
83 executor.dispose();
73 if (webSocketSession == null) { 84 if (webSocketSession == null) {
74 return; 85 return;
75 } 86 }
76 LOG.error("Internal websocket error in connection from" + webSocketSession.getRemoteSocketAddress(), error); 87 LOG.error("Internal websocket error in connection from " + webSocketSession.getRemoteSocketAddress(), error);
77 } 88 }
78 89
79 @OnWebSocketMessage 90 @OnWebSocketMessage
@@ -86,14 +97,18 @@ public class XtextWebSocket implements ResponseHandler {
86 try { 97 try {
87 request = gson.fromJson(reader, XtextWebRequest.class); 98 request = gson.fromJson(reader, XtextWebRequest.class);
88 } catch (JsonIOException e) { 99 } catch (JsonIOException e) {
89 LOG.error("Cannot read from websocket from" + webSocketSession.getRemoteSocketAddress(), e); 100 LOG.error("Cannot read from websocket from " + webSocketSession.getRemoteSocketAddress(), e);
90 if (webSocketSession.isOpen()) { 101 if (webSocketSession.isOpen()) {
102 executor.dispose();
91 webSocketSession.close(StatusCode.SERVER_ERROR, "Cannot read payload", Callback.NOOP); 103 webSocketSession.close(StatusCode.SERVER_ERROR, "Cannot read payload", Callback.NOOP);
92 } 104 }
93 return; 105 return;
94 } catch (JsonParseException e) { 106 } catch (JsonParseException e) {
95 LOG.warn("Malformed websocket request from" + webSocketSession.getRemoteSocketAddress(), e); 107 LOG.warn("Malformed websocket request from " + webSocketSession.getRemoteSocketAddress(), e);
96 webSocketSession.close(XtextStatusCode.INVALID_JSON, "Invalid JSON payload", Callback.NOOP); 108 if (webSocketSession.isOpen()) {
109 executor.dispose();
110 webSocketSession.close(XtextStatusCode.INVALID_JSON, "Invalid JSON payload", Callback.NOOP);
111 }
97 return; 112 return;
98 } 113 }
99 try { 114 try {
@@ -101,6 +116,7 @@ public class XtextWebSocket implements ResponseHandler {
101 } catch (ResponseHandlerException e) { 116 } catch (ResponseHandlerException e) {
102 LOG.warn("Cannot write websocket response", e); 117 LOG.warn("Cannot write websocket response", e);
103 if (webSocketSession.isOpen()) { 118 if (webSocketSession.isOpen()) {
119 executor.dispose();
104 webSocketSession.close(StatusCode.SERVER_ERROR, "Cannot write response", Callback.NOOP); 120 webSocketSession.close(StatusCode.SERVER_ERROR, "Cannot write response", Callback.NOOP);
105 } 121 }
106 } 122 }