diff options
author | 2023-09-14 19:29:36 +0200 | |
---|---|---|
committer | 2023-09-14 19:29:36 +0200 | |
commit | 98ed3b6db5f4e51961a161050cc31c66015116e8 (patch) | |
tree | 8bfd6d9bc8d6ed23b9eb0f889dd40b6c24fe8f92 /subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/servlet | |
parent | Merge pull request #38 from nagilooh/design-space-exploration (diff) | |
parent | Merge remote-tracking branch 'upstream/main' into partial-interpretation (diff) | |
download | refinery-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')
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 | */ | ||
24 | package tools.refinery.language.web.xtext.servlet; | ||
25 | |||
26 | import com.google.errorprone.annotations.CanIgnoreReturnValue; | ||
27 | import com.google.gson.Gson; | ||
28 | import com.google.gson.JsonElement; | ||
29 | import com.google.gson.JsonObject; | ||
30 | import com.google.gson.JsonParseException; | ||
31 | import com.google.gson.JsonPrimitive; | ||
32 | import com.google.gson.TypeAdapter; | ||
33 | import com.google.gson.TypeAdapterFactory; | ||
34 | import com.google.gson.reflect.TypeToken; | ||
35 | import com.google.gson.stream.JsonReader; | ||
36 | import com.google.gson.stream.JsonWriter; | ||
37 | import java.io.IOException; | ||
38 | import java.util.LinkedHashMap; | ||
39 | import 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 | */ | ||
142 | public 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 @@ | |||
6 | package tools.refinery.language.web.xtext.servlet; | 6 | package tools.refinery.language.web.xtext.servlet; |
7 | 7 | ||
8 | import com.google.gson.Gson; | 8 | import com.google.gson.Gson; |
9 | import com.google.gson.GsonBuilder; | ||
9 | import com.google.gson.JsonIOException; | 10 | import com.google.gson.JsonIOException; |
10 | import com.google.gson.JsonParseException; | 11 | import com.google.gson.JsonParseException; |
11 | import org.eclipse.jetty.websocket.api.Callback; | 12 | import org.eclipse.jetty.websocket.api.Callback; |
@@ -16,6 +17,7 @@ import org.eclipse.xtext.resource.IResourceServiceProvider; | |||
16 | import org.eclipse.xtext.web.server.ISession; | 17 | import org.eclipse.xtext.web.server.ISession; |
17 | import org.slf4j.Logger; | 18 | import org.slf4j.Logger; |
18 | import org.slf4j.LoggerFactory; | 19 | import org.slf4j.LoggerFactory; |
20 | import tools.refinery.language.semantics.metadata.*; | ||
19 | import tools.refinery.language.web.xtext.server.ResponseHandler; | 21 | import tools.refinery.language.web.xtext.server.ResponseHandler; |
20 | import tools.refinery.language.web.xtext.server.ResponseHandlerException; | 22 | import tools.refinery.language.web.xtext.server.ResponseHandlerException; |
21 | import tools.refinery.language.web.xtext.server.TransactionExecutor; | 23 | import tools.refinery.language.web.xtext.server.TransactionExecutor; |
@@ -28,7 +30,15 @@ import java.io.Reader; | |||
28 | public class XtextWebSocket implements ResponseHandler { | 30 | public 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 | } |