aboutsummaryrefslogtreecommitdiffstats
path: root/subprojects/language-web
diff options
context:
space:
mode:
Diffstat (limited to 'subprojects/language-web')
-rw-r--r--subprojects/language-web/build.gradle.kts19
-rw-r--r--subprojects/language-web/src/main/java/tools/refinery/language/web/ProblemWebModule.java6
-rw-r--r--subprojects/language-web/src/main/java/tools/refinery/language/web/ProblemWebSocketServlet.java4
-rw-r--r--subprojects/language-web/src/main/java/tools/refinery/language/web/SecurityHeadersFilter.java4
-rw-r--r--subprojects/language-web/src/main/java/tools/refinery/language/web/ServerLauncher.java33
-rw-r--r--subprojects/language-web/src/main/java/tools/refinery/language/web/config/BackendConfigServlet.java3
-rw-r--r--subprojects/language-web/src/main/java/tools/refinery/language/web/semantics/CancellableSeed.java99
-rw-r--r--subprojects/language-web/src/main/java/tools/refinery/language/web/semantics/SemanticsInternalErrorResult.java9
-rw-r--r--subprojects/language-web/src/main/java/tools/refinery/language/web/semantics/SemanticsIssuesResult.java13
-rw-r--r--subprojects/language-web/src/main/java/tools/refinery/language/web/semantics/SemanticsResult.java12
-rw-r--r--subprojects/language-web/src/main/java/tools/refinery/language/web/semantics/SemanticsService.java142
-rw-r--r--subprojects/language-web/src/main/java/tools/refinery/language/web/semantics/SemanticsSuccessResult.java16
-rw-r--r--subprojects/language-web/src/main/java/tools/refinery/language/web/semantics/SemanticsWorker.java175
-rw-r--r--subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/server/ThreadPoolExecutorServiceProvider.java110
-rw-r--r--subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/server/TransactionExecutor.java48
-rw-r--r--subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/server/message/XtextWebOkResponse.java6
-rw-r--r--subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/server/message/XtextWebRequest.java9
-rw-r--r--subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/server/message/XtextWebResponse.java2
-rw-r--r--subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/server/push/PushServiceDispatcher.java12
-rw-r--r--subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/server/push/PushWebDocument.java50
-rw-r--r--subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/server/push/PushWebDocumentAccess.java8
-rw-r--r--subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/server/push/PushWebDocumentProvider.java9
-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
-rw-r--r--subprojects/language-web/src/test/java/tools/refinery/language/web/ProblemWebSocketServletIntegrationTest.java19
-rw-r--r--subprojects/language-web/src/test/java/tools/refinery/language/web/tests/ProblemWebInjectorProvider.java1
-rw-r--r--subprojects/language-web/src/test/java/tools/refinery/language/web/tests/RestartableCachedThreadPool.java2
-rw-r--r--subprojects/language-web/src/test/java/tools/refinery/language/web/xtext/servlet/TransactionExecutorTest.java10
28 files changed, 1056 insertions, 95 deletions
diff --git a/subprojects/language-web/build.gradle.kts b/subprojects/language-web/build.gradle.kts
index 562a1bd9..88dccdf3 100644
--- a/subprojects/language-web/build.gradle.kts
+++ b/subprojects/language-web/build.gradle.kts
@@ -17,6 +17,10 @@ val webapp: Configuration by configurations.creating {
17dependencies { 17dependencies {
18 implementation(project(":refinery-language")) 18 implementation(project(":refinery-language"))
19 implementation(project(":refinery-language-ide")) 19 implementation(project(":refinery-language-ide"))
20 implementation(project(":refinery-language-semantics"))
21 implementation(project(":refinery-store-query-viatra"))
22 implementation(project(":refinery-store-reasoning-scope"))
23 implementation(libs.gson)
20 implementation(libs.jetty.server) 24 implementation(libs.jetty.server)
21 implementation(libs.jetty.servlet) 25 implementation(libs.jetty.servlet)
22 implementation(libs.jetty.websocket.api) 26 implementation(libs.jetty.websocket.api)
@@ -60,9 +64,18 @@ tasks {
60 classpath(mainRuntimeClasspath) 64 classpath(mainRuntimeClasspath)
61 mainClass.set(application.mainClass) 65 mainClass.set(application.mainClass)
62 standardInput = System.`in` 66 standardInput = System.`in`
63 val baseResource = webapp.incoming.artifacts.artifactFiles.first() 67 environment("REFINERY_BASE_RESOURCE", webapp.singleFile)
64 environment("BASE_RESOURCE", baseResource)
65 group = "run" 68 group = "run"
66 description = "Start a Jetty web server serving the Xtex API and assets." 69 description = "Start a Jetty web server serving the Xtext API and assets."
70 }
71
72 register<JavaExec>("serveBackendOnly") {
73 val mainRuntimeClasspath = sourceSets.main.map { it.runtimeClasspath }
74 dependsOn(mainRuntimeClasspath)
75 classpath(mainRuntimeClasspath)
76 mainClass.set(application.mainClass)
77 standardInput = System.`in`
78 group = "run"
79 description = "Start a Jetty web server serving the Xtext API without assets."
67 } 80 }
68} 81}
diff --git a/subprojects/language-web/src/main/java/tools/refinery/language/web/ProblemWebModule.java b/subprojects/language-web/src/main/java/tools/refinery/language/web/ProblemWebModule.java
index b0197c01..6a6e0107 100644
--- a/subprojects/language-web/src/main/java/tools/refinery/language/web/ProblemWebModule.java
+++ b/subprojects/language-web/src/main/java/tools/refinery/language/web/ProblemWebModule.java
@@ -9,11 +9,13 @@
9 */ 9 */
10package tools.refinery.language.web; 10package tools.refinery.language.web;
11 11
12import org.eclipse.xtext.ide.ExecutorServiceProvider;
12import org.eclipse.xtext.web.server.XtextServiceDispatcher; 13import org.eclipse.xtext.web.server.XtextServiceDispatcher;
13import org.eclipse.xtext.web.server.model.IWebDocumentProvider; 14import org.eclipse.xtext.web.server.model.IWebDocumentProvider;
14import org.eclipse.xtext.web.server.model.XtextWebDocumentAccess; 15import org.eclipse.xtext.web.server.model.XtextWebDocumentAccess;
15import org.eclipse.xtext.web.server.occurrences.OccurrencesService; 16import org.eclipse.xtext.web.server.occurrences.OccurrencesService;
16import tools.refinery.language.web.occurrences.ProblemOccurrencesService; 17import tools.refinery.language.web.occurrences.ProblemOccurrencesService;
18import tools.refinery.language.web.xtext.server.ThreadPoolExecutorServiceProvider;
17import tools.refinery.language.web.xtext.server.push.PushServiceDispatcher; 19import tools.refinery.language.web.xtext.server.push.PushServiceDispatcher;
18import tools.refinery.language.web.xtext.server.push.PushWebDocumentAccess; 20import tools.refinery.language.web.xtext.server.push.PushWebDocumentAccess;
19import tools.refinery.language.web.xtext.server.push.PushWebDocumentProvider; 21import tools.refinery.language.web.xtext.server.push.PushWebDocumentProvider;
@@ -37,4 +39,8 @@ public class ProblemWebModule extends AbstractProblemWebModule {
37 public Class<? extends OccurrencesService> bindOccurrencesService() { 39 public Class<? extends OccurrencesService> bindOccurrencesService() {
38 return ProblemOccurrencesService.class; 40 return ProblemOccurrencesService.class;
39 } 41 }
42
43 public Class<? extends ExecutorServiceProvider> bindExecutorServiceProvider() {
44 return ThreadPoolExecutorServiceProvider.class;
45 }
40} 46}
diff --git a/subprojects/language-web/src/main/java/tools/refinery/language/web/ProblemWebSocketServlet.java b/subprojects/language-web/src/main/java/tools/refinery/language/web/ProblemWebSocketServlet.java
index 7b48cde8..e98d115e 100644
--- a/subprojects/language-web/src/main/java/tools/refinery/language/web/ProblemWebSocketServlet.java
+++ b/subprojects/language-web/src/main/java/tools/refinery/language/web/ProblemWebSocketServlet.java
@@ -10,8 +10,10 @@ import org.eclipse.xtext.util.DisposableRegistry;
10import jakarta.servlet.ServletException; 10import jakarta.servlet.ServletException;
11import tools.refinery.language.web.xtext.servlet.XtextWebSocketServlet; 11import tools.refinery.language.web.xtext.servlet.XtextWebSocketServlet;
12 12
13public class ProblemWebSocketServlet extends XtextWebSocketServlet { 13import java.io.Serial;
14 14
15public class ProblemWebSocketServlet extends XtextWebSocketServlet {
16 @Serial
15 private static final long serialVersionUID = -7040955470384797008L; 17 private static final long serialVersionUID = -7040955470384797008L;
16 18
17 private transient DisposableRegistry disposableRegistry; 19 private transient DisposableRegistry disposableRegistry;
diff --git a/subprojects/language-web/src/main/java/tools/refinery/language/web/SecurityHeadersFilter.java b/subprojects/language-web/src/main/java/tools/refinery/language/web/SecurityHeadersFilter.java
index 7b094fde..fab94689 100644
--- a/subprojects/language-web/src/main/java/tools/refinery/language/web/SecurityHeadersFilter.java
+++ b/subprojects/language-web/src/main/java/tools/refinery/language/web/SecurityHeadersFilter.java
@@ -16,7 +16,7 @@ public class SecurityHeadersFilter implements Filter {
16 ServletException { 16 ServletException {
17 if (response instanceof HttpServletResponse httpResponse) { 17 if (response instanceof HttpServletResponse httpResponse) {
18 httpResponse.setHeader("Content-Security-Policy", "default-src 'none'; " + 18 httpResponse.setHeader("Content-Security-Policy", "default-src 'none'; " +
19 "script-src 'self'; " + 19 "script-src 'self' 'wasm-unsafe-eval'; " +
20 // CodeMirror needs inline styles, see e.g., 20 // CodeMirror needs inline styles, see e.g.,
21 // https://discuss.codemirror.net/t/inline-styles-and-content-security-policy/1311/2 21 // https://discuss.codemirror.net/t/inline-styles-and-content-security-policy/1311/2
22 "style-src 'self' 'unsafe-inline'; " + 22 "style-src 'self' 'unsafe-inline'; " +
@@ -25,7 +25,7 @@ public class SecurityHeadersFilter implements Filter {
25 "font-src 'self'; " + 25 "font-src 'self'; " +
26 "connect-src 'self'; " + 26 "connect-src 'self'; " +
27 "manifest-src 'self'; " + 27 "manifest-src 'self'; " +
28 "worker-src 'self';"); 28 "worker-src 'self' blob:;");
29 httpResponse.setHeader("X-Content-Type-Options", "nosniff"); 29 httpResponse.setHeader("X-Content-Type-Options", "nosniff");
30 httpResponse.setHeader("X-Frame-Options", "DENY"); 30 httpResponse.setHeader("X-Frame-Options", "DENY");
31 httpResponse.setHeader("Referrer-Policy", "strict-origin"); 31 httpResponse.setHeader("Referrer-Policy", "strict-origin");
diff --git a/subprojects/language-web/src/main/java/tools/refinery/language/web/ServerLauncher.java b/subprojects/language-web/src/main/java/tools/refinery/language/web/ServerLauncher.java
index ad19e77d..155efc6f 100644
--- a/subprojects/language-web/src/main/java/tools/refinery/language/web/ServerLauncher.java
+++ b/subprojects/language-web/src/main/java/tools/refinery/language/web/ServerLauncher.java
@@ -33,7 +33,7 @@ import java.util.EnumSet;
33import java.util.Set; 33import java.util.Set;
34 34
35public class ServerLauncher { 35public class ServerLauncher {
36 public static final String DEFAULT_LISTEN_ADDRESS = "localhost"; 36 public static final String DEFAULT_LISTEN_HOST = "localhost";
37 37
38 public static final int DEFAULT_LISTEN_PORT = 1312; 38 public static final int DEFAULT_LISTEN_PORT = 1312;
39 39
@@ -105,7 +105,7 @@ public class ServerLauncher {
105 105
106 private Resource getBaseResource() { 106 private Resource getBaseResource() {
107 var factory = ResourceFactory.of(server); 107 var factory = ResourceFactory.of(server);
108 var baseResourceOverride = System.getenv("BASE_RESOURCE"); 108 var baseResourceOverride = System.getenv("REFINERY_BASE_RESOURCE");
109 if (baseResourceOverride != null) { 109 if (baseResourceOverride != null) {
110 // If a user override is provided, use it. 110 // If a user override is provided, use it.
111 return factory.newResource(baseResourceOverride); 111 return factory.newResource(baseResourceOverride);
@@ -115,7 +115,10 @@ public class ServerLauncher {
115 // If the app is packaged in the jar, serve it. 115 // If the app is packaged in the jar, serve it.
116 URI webRootUri; 116 URI webRootUri;
117 try { 117 try {
118 webRootUri = URI.create(indexUrlInJar.toURI().toASCIIString().replaceFirst("/index.html$", "/")); 118 webRootUri = URI.create(indexUrlInJar.toURI().toASCIIString()
119 .replaceFirst("/index.html$", "/")
120 // Enable running without warnings from a jar.
121 .replaceFirst("^jar:file:", "jar:file://"));
119 } catch (URISyntaxException e) { 122 } catch (URISyntaxException e) {
120 throw new IllegalStateException("Jar has invalid base resource URI", e); 123 throw new IllegalStateException("Jar has invalid base resource URI", e);
121 } 124 }
@@ -152,17 +155,17 @@ public class ServerLauncher {
152 } 155 }
153 156
154 private static String getListenAddress() { 157 private static String getListenAddress() {
155 var listenAddress = System.getenv("LISTEN_ADDRESS"); 158 var listenAddress = System.getenv("REFINERY_LISTEN_HOST");
156 if (listenAddress == null) { 159 if (listenAddress == null) {
157 return DEFAULT_LISTEN_ADDRESS; 160 return DEFAULT_LISTEN_HOST;
158 } 161 }
159 return listenAddress; 162 return listenAddress;
160 } 163 }
161 164
162 private static int getListenPort() { 165 private static int getListenPort() {
163 var portStr = System.getenv("LISTEN_PORT"); 166 var portStr = System.getenv("REFINERY_LISTEN_PORT");
164 if (portStr != null) { 167 if (portStr != null) {
165 return Integer.parseInt(portStr); 168 return Integer.parseUnsignedInt(portStr);
166 } 169 }
167 return DEFAULT_LISTEN_PORT; 170 return DEFAULT_LISTEN_PORT;
168 } 171 }
@@ -174,7 +177,7 @@ public class ServerLauncher {
174 } 177 }
175 178
176 private static String getPublicHost() { 179 private static String getPublicHost() {
177 var publicHost = System.getenv("PUBLIC_HOST"); 180 var publicHost = System.getenv("REFINERY_PUBLIC_HOST");
178 if (publicHost != null) { 181 if (publicHost != null) {
179 return publicHost.toLowerCase(); 182 return publicHost.toLowerCase();
180 } 183 }
@@ -182,15 +185,15 @@ public class ServerLauncher {
182 } 185 }
183 186
184 private static int getPublicPort() { 187 private static int getPublicPort() {
185 var portStr = System.getenv("PUBLIC_PORT"); 188 var portStr = System.getenv("REFINERY_PUBLIC_PORT");
186 if (portStr != null) { 189 if (portStr != null) {
187 return Integer.parseInt(portStr); 190 return Integer.parseUnsignedInt(portStr);
188 } 191 }
189 return DEFAULT_PUBLIC_PORT; 192 return DEFAULT_PUBLIC_PORT;
190 } 193 }
191 194
192 private static String[] getAllowedOrigins() { 195 private static String[] getAllowedOrigins() {
193 var allowedOrigins = System.getenv("ALLOWED_ORIGINS"); 196 var allowedOrigins = System.getenv("REFINERY_ALLOWED_ORIGINS");
194 if (allowedOrigins != null) { 197 if (allowedOrigins != null) {
195 return allowedOrigins.split(ALLOWED_ORIGINS_SEPARATOR); 198 return allowedOrigins.split(ALLOWED_ORIGINS_SEPARATOR);
196 } 199 }
@@ -219,12 +222,10 @@ public class ServerLauncher {
219 int port; 222 int port;
220 var publicHost = getPublicHost(); 223 var publicHost = getPublicHost();
221 if (publicHost == null) { 224 if (publicHost == null) {
222 host = getListenAddress(); 225 return null;
223 port = getListenPort();
224 } else {
225 host = publicHost;
226 port = getPublicPort();
227 } 226 }
227 host = publicHost;
228 port = getPublicPort();
228 var scheme = port == HTTPS_DEFAULT_PORT ? "wss" : "ws"; 229 var scheme = port == HTTPS_DEFAULT_PORT ? "wss" : "ws";
229 return String.format("%s://%s:%d/xtext-service", scheme, host, port); 230 return String.format("%s://%s:%d/xtext-service", scheme, host, port);
230 } 231 }
diff --git a/subprojects/language-web/src/main/java/tools/refinery/language/web/config/BackendConfigServlet.java b/subprojects/language-web/src/main/java/tools/refinery/language/web/config/BackendConfigServlet.java
index a2f04e34..7d0a5122 100644
--- a/subprojects/language-web/src/main/java/tools/refinery/language/web/config/BackendConfigServlet.java
+++ b/subprojects/language-web/src/main/java/tools/refinery/language/web/config/BackendConfigServlet.java
@@ -25,9 +25,6 @@ public class BackendConfigServlet extends HttpServlet {
25 public void init(ServletConfig config) throws ServletException { 25 public void init(ServletConfig config) throws ServletException {
26 super.init(config); 26 super.init(config);
27 var webSocketUrl = config.getInitParameter(WEBSOCKET_URL_INIT_PARAM); 27 var webSocketUrl = config.getInitParameter(WEBSOCKET_URL_INIT_PARAM);
28 if (webSocketUrl == null) {
29 throw new IllegalArgumentException("Init parameter " + WEBSOCKET_URL_INIT_PARAM + " is mandatory");
30 }
31 var backendConfig = new BackendConfig(webSocketUrl); 28 var backendConfig = new BackendConfig(webSocketUrl);
32 var gson = new Gson(); 29 var gson = new Gson();
33 serializedConfig = gson.toJson(backendConfig); 30 serializedConfig = gson.toJson(backendConfig);
diff --git a/subprojects/language-web/src/main/java/tools/refinery/language/web/semantics/CancellableSeed.java b/subprojects/language-web/src/main/java/tools/refinery/language/web/semantics/CancellableSeed.java
new file mode 100644
index 00000000..aa14f39d
--- /dev/null
+++ b/subprojects/language-web/src/main/java/tools/refinery/language/web/semantics/CancellableSeed.java
@@ -0,0 +1,99 @@
1/*
2 * SPDX-FileCopyrightText: 2023 The Refinery Authors <https://refinery.tools/>
3 *
4 * SPDX-License-Identifier: EPL-2.0
5 */
6package tools.refinery.language.web.semantics;
7
8import tools.refinery.store.map.AnyVersionedMap;
9import tools.refinery.store.map.Cursor;
10import tools.refinery.store.reasoning.representation.PartialSymbol;
11import tools.refinery.store.reasoning.seed.ModelSeed;
12import tools.refinery.store.reasoning.seed.Seed;
13import tools.refinery.store.tuple.Tuple;
14import tools.refinery.viatra.runtime.CancellationToken;
15
16import java.util.Set;
17
18class CancellableSeed<T> implements Seed<T> {
19 private final CancellationToken cancellationToken;
20 private final Seed<T> seed;
21
22 private CancellableSeed(CancellationToken cancellationToken, Seed<T> seed) {
23 this.cancellationToken = cancellationToken;
24 this.seed = seed;
25 }
26
27 @Override
28 public int arity() {
29 return seed.arity();
30 }
31
32 @Override
33 public Class<T> valueType() {
34 return seed.valueType();
35 }
36
37 @Override
38 public T reducedValue() {
39 return seed.reducedValue();
40 }
41
42 @Override
43 public T get(Tuple key) {
44 return seed.get(key);
45 }
46
47 @Override
48 public Cursor<Tuple, T> getCursor(T defaultValue, int nodeCount) {
49 return new CancellableCursor<>(cancellationToken, seed.getCursor(defaultValue, nodeCount));
50 }
51
52 public static ModelSeed wrap(CancellationToken cancellationToken, ModelSeed modelSeed) {
53 var builder = ModelSeed.builder(modelSeed.getNodeCount());
54 for (var partialSymbol : modelSeed.getSeededSymbols()) {
55 wrap(cancellationToken, (PartialSymbol<?, ?>) partialSymbol, modelSeed, builder);
56 }
57 return builder.build();
58 }
59
60 private static <A, C> void wrap(CancellationToken cancellationToken, PartialSymbol<A, C> partialSymbol,
61 ModelSeed originalModelSeed, ModelSeed.Builder builder) {
62 var originalSeed = originalModelSeed.getSeed(partialSymbol);
63 builder.seed(partialSymbol, new CancellableSeed<>(cancellationToken, originalSeed));
64 }
65
66 private record CancellableCursor<T>(CancellationToken cancellationToken, Cursor<Tuple, T> cursor)
67 implements Cursor<Tuple, T> {
68 @Override
69 public Tuple getKey() {
70 return cursor.getKey();
71 }
72
73 @Override
74 public T getValue() {
75 return cursor.getValue();
76 }
77
78 @Override
79 public boolean isTerminated() {
80 return cursor.isTerminated();
81 }
82
83 @Override
84 public boolean move() {
85 cancellationToken.checkCancelled();
86 return cursor.move();
87 }
88
89 @Override
90 public boolean isDirty() {
91 return cursor.isDirty();
92 }
93
94 @Override
95 public Set<AnyVersionedMap> getDependingMaps() {
96 return cursor.getDependingMaps();
97 }
98 }
99}
diff --git a/subprojects/language-web/src/main/java/tools/refinery/language/web/semantics/SemanticsInternalErrorResult.java b/subprojects/language-web/src/main/java/tools/refinery/language/web/semantics/SemanticsInternalErrorResult.java
new file mode 100644
index 00000000..ff592e93
--- /dev/null
+++ b/subprojects/language-web/src/main/java/tools/refinery/language/web/semantics/SemanticsInternalErrorResult.java
@@ -0,0 +1,9 @@
1/*
2 * SPDX-FileCopyrightText: 2023 The Refinery Authors <https://refinery.tools/>
3 *
4 * SPDX-License-Identifier: EPL-2.0
5 */
6package tools.refinery.language.web.semantics;
7
8public record SemanticsInternalErrorResult(String error) implements SemanticsResult {
9}
diff --git a/subprojects/language-web/src/main/java/tools/refinery/language/web/semantics/SemanticsIssuesResult.java b/subprojects/language-web/src/main/java/tools/refinery/language/web/semantics/SemanticsIssuesResult.java
new file mode 100644
index 00000000..644bd179
--- /dev/null
+++ b/subprojects/language-web/src/main/java/tools/refinery/language/web/semantics/SemanticsIssuesResult.java
@@ -0,0 +1,13 @@
1/*
2 * SPDX-FileCopyrightText: 2023 The Refinery Authors <https://refinery.tools/>
3 *
4 * SPDX-License-Identifier: EPL-2.0
5 */
6package tools.refinery.language.web.semantics;
7
8import org.eclipse.xtext.web.server.validation.ValidationResult;
9
10import java.util.List;
11
12public record SemanticsIssuesResult(List<ValidationResult.Issue> issues) implements SemanticsResult {
13}
diff --git a/subprojects/language-web/src/main/java/tools/refinery/language/web/semantics/SemanticsResult.java b/subprojects/language-web/src/main/java/tools/refinery/language/web/semantics/SemanticsResult.java
new file mode 100644
index 00000000..a2e19a2f
--- /dev/null
+++ b/subprojects/language-web/src/main/java/tools/refinery/language/web/semantics/SemanticsResult.java
@@ -0,0 +1,12 @@
1/*
2 * SPDX-FileCopyrightText: 2023 The Refinery Authors <https://refinery.tools/>
3 *
4 * SPDX-License-Identifier: EPL-2.0
5 */
6package tools.refinery.language.web.semantics;
7
8import org.eclipse.xtext.web.server.IServiceResult;
9
10public sealed interface SemanticsResult extends IServiceResult permits SemanticsSuccessResult,
11 SemanticsInternalErrorResult, SemanticsIssuesResult {
12}
diff --git a/subprojects/language-web/src/main/java/tools/refinery/language/web/semantics/SemanticsService.java b/subprojects/language-web/src/main/java/tools/refinery/language/web/semantics/SemanticsService.java
new file mode 100644
index 00000000..26924f0a
--- /dev/null
+++ b/subprojects/language-web/src/main/java/tools/refinery/language/web/semantics/SemanticsService.java
@@ -0,0 +1,142 @@
1/*
2 * SPDX-FileCopyrightText: 2023 The Refinery Authors <https://refinery.tools/>
3 *
4 * SPDX-License-Identifier: EPL-2.0
5 */
6package tools.refinery.language.web.semantics;
7
8import com.google.gson.JsonObject;
9import com.google.inject.Inject;
10import com.google.inject.Provider;
11import com.google.inject.Singleton;
12import org.eclipse.xtext.ide.ExecutorServiceProvider;
13import org.eclipse.xtext.service.OperationCanceledManager;
14import org.eclipse.xtext.util.CancelIndicator;
15import org.eclipse.xtext.web.server.model.AbstractCachedService;
16import org.eclipse.xtext.web.server.model.IXtextWebDocument;
17import org.eclipse.xtext.web.server.validation.ValidationService;
18import org.jetbrains.annotations.Nullable;
19import org.slf4j.Logger;
20import org.slf4j.LoggerFactory;
21import tools.refinery.language.model.problem.Problem;
22import tools.refinery.language.web.xtext.server.push.PushWebDocument;
23
24import java.util.List;
25import java.util.Optional;
26import java.util.concurrent.*;
27import java.util.concurrent.atomic.AtomicBoolean;
28
29@Singleton
30public class SemanticsService extends AbstractCachedService<SemanticsResult> {
31 public static final String SEMANTICS_EXECUTOR = "semantics";
32
33 private static final Logger LOG = LoggerFactory.getLogger(SemanticsService.class);
34
35 @Inject
36 private Provider<SemanticsWorker> workerProvider;
37
38 @Inject
39 private OperationCanceledManager operationCanceledManager;
40
41 @Inject
42 private ValidationService validationService;
43
44 private ExecutorService executorService;
45
46 private final long timeoutMs;
47
48 private final long warmupTimeoutMs;
49
50 private final AtomicBoolean warmedUp = new AtomicBoolean(false);
51
52 public SemanticsService() {
53 timeoutMs = getTimeout("REFINERY_SEMANTICS_TIMEOUT_MS").orElse(1000L);
54 warmupTimeoutMs = getTimeout("REFINERY_SEMANTICS_WARMUP_TIMEOUT_MS").orElse(timeoutMs * 2);
55 }
56
57 private static Optional<Long> getTimeout(String name) {
58 return Optional.ofNullable(System.getenv(name)).map(Long::parseUnsignedLong);
59 }
60
61 @Inject
62 public void setExecutorServiceProvider(ExecutorServiceProvider provider) {
63 executorService = provider.get(SEMANTICS_EXECUTOR);
64 }
65
66 @Override
67 public SemanticsResult compute(IXtextWebDocument doc, CancelIndicator cancelIndicator) {
68 long start = 0;
69 if (LOG.isTraceEnabled()) {
70 start = System.currentTimeMillis();
71 }
72 if (hasError(doc, cancelIndicator)) {
73 return null;
74 }
75 var problem = getProblem(doc);
76 if (problem == null) {
77 return new SemanticsSuccessResult(List.of(), List.of(), new JsonObject());
78 }
79 var worker = workerProvider.get();
80 worker.setProblem(problem, cancelIndicator);
81 var future = executorService.submit(worker);
82 boolean warmedUpCurrently = warmedUp.get();
83 long timeout = warmedUpCurrently ? timeoutMs : warmupTimeoutMs;
84 SemanticsResult result = null;
85 try {
86 result = future.get(timeout, TimeUnit.MILLISECONDS);
87 if (!warmedUpCurrently) {
88 warmedUp.set(true);
89 }
90 } catch (InterruptedException e) {
91 future.cancel(true);
92 LOG.error("Semantics service interrupted", e);
93 Thread.currentThread().interrupt();
94 } catch (ExecutionException e) {
95 operationCanceledManager.propagateAsErrorIfCancelException(e.getCause());
96 LOG.debug("Error while computing semantics", e);
97 if (e.getCause() instanceof Error error) {
98 throw error;
99 }
100 String message = e.getMessage();
101 if (message == null) {
102 message = "Partial interpretation error";
103 }
104 return new SemanticsInternalErrorResult(message);
105 } catch (TimeoutException e) {
106 future.cancel(true);
107 if (!warmedUpCurrently) {
108 warmedUp.set(true);
109 }
110 LOG.trace("Semantics service timeout", e);
111 return new SemanticsInternalErrorResult("Partial interpretation timed out");
112 }
113 if (LOG.isTraceEnabled()) {
114 long end = System.currentTimeMillis();
115 LOG.trace("Computed semantics for {} ({}) in {}ms", doc.getResourceId(), doc.getStateId(),
116 end - start);
117 }
118 return result;
119 }
120
121 private boolean hasError(IXtextWebDocument doc, CancelIndicator cancelIndicator) {
122 if (!(doc instanceof PushWebDocument pushDoc)) {
123 throw new IllegalArgumentException("Unexpected IXtextWebDocument: " + doc);
124 }
125 var validationResult = pushDoc.getCachedServiceResult(validationService, cancelIndicator, true);
126 return validationResult.getIssues().stream()
127 .anyMatch(issue -> "error".equals(issue.getSeverity()));
128 }
129
130 @Nullable
131 private Problem getProblem(IXtextWebDocument doc) {
132 var contents = doc.getResource().getContents();
133 if (contents.isEmpty()) {
134 return null;
135 }
136 var model = contents.get(0);
137 if (!(model instanceof Problem problem)) {
138 return null;
139 }
140 return problem;
141 }
142}
diff --git a/subprojects/language-web/src/main/java/tools/refinery/language/web/semantics/SemanticsSuccessResult.java b/subprojects/language-web/src/main/java/tools/refinery/language/web/semantics/SemanticsSuccessResult.java
new file mode 100644
index 00000000..350b0b2b
--- /dev/null
+++ b/subprojects/language-web/src/main/java/tools/refinery/language/web/semantics/SemanticsSuccessResult.java
@@ -0,0 +1,16 @@
1/*
2 * SPDX-FileCopyrightText: 2023 The Refinery Authors <https://refinery.tools/>
3 *
4 * SPDX-License-Identifier: EPL-2.0
5 */
6package tools.refinery.language.web.semantics;
7
8import com.google.gson.JsonObject;
9import tools.refinery.language.semantics.metadata.NodeMetadata;
10import tools.refinery.language.semantics.metadata.RelationMetadata;
11
12import java.util.List;
13
14public record SemanticsSuccessResult(List<NodeMetadata> nodes, List<RelationMetadata> relations,
15 JsonObject partialInterpretation) implements SemanticsResult {
16}
diff --git a/subprojects/language-web/src/main/java/tools/refinery/language/web/semantics/SemanticsWorker.java b/subprojects/language-web/src/main/java/tools/refinery/language/web/semantics/SemanticsWorker.java
new file mode 100644
index 00000000..8470bb99
--- /dev/null
+++ b/subprojects/language-web/src/main/java/tools/refinery/language/web/semantics/SemanticsWorker.java
@@ -0,0 +1,175 @@
1/*
2 * SPDX-FileCopyrightText: 2023 The Refinery Authors <https://refinery.tools/>
3 *
4 * SPDX-License-Identifier: EPL-2.0
5 */
6package tools.refinery.language.web.semantics;
7
8import com.google.gson.JsonArray;
9import com.google.gson.JsonObject;
10import com.google.inject.Inject;
11import org.eclipse.emf.common.util.Diagnostic;
12import org.eclipse.emf.ecore.EObject;
13import org.eclipse.xtext.service.OperationCanceledManager;
14import org.eclipse.xtext.util.CancelIndicator;
15import org.eclipse.xtext.validation.CheckType;
16import org.eclipse.xtext.validation.FeatureBasedDiagnostic;
17import org.eclipse.xtext.validation.IDiagnosticConverter;
18import org.eclipse.xtext.validation.Issue;
19import org.eclipse.xtext.web.server.validation.ValidationResult;
20import tools.refinery.language.model.problem.Problem;
21import tools.refinery.language.semantics.metadata.MetadataCreator;
22import tools.refinery.language.semantics.model.ModelInitializer;
23import tools.refinery.language.semantics.model.SemanticsUtils;
24import tools.refinery.language.semantics.model.TracedException;
25import tools.refinery.store.map.Cursor;
26import tools.refinery.store.model.Model;
27import tools.refinery.store.model.ModelStore;
28import tools.refinery.store.query.viatra.ViatraModelQueryAdapter;
29import tools.refinery.store.reasoning.ReasoningAdapter;
30import tools.refinery.store.reasoning.ReasoningStoreAdapter;
31import tools.refinery.store.reasoning.literal.Concreteness;
32import tools.refinery.store.reasoning.refinement.RefinementResult;
33import tools.refinery.store.reasoning.representation.PartialRelation;
34import tools.refinery.store.reasoning.scope.ScopePropagatorAdapter;
35import tools.refinery.store.reasoning.translator.TranslationException;
36import tools.refinery.store.reasoning.translator.multiobject.MultiObjectTranslator;
37import tools.refinery.store.tuple.Tuple;
38import tools.refinery.viatra.runtime.CancellationToken;
39
40import java.util.ArrayList;
41import java.util.TreeMap;
42import java.util.concurrent.Callable;
43
44class SemanticsWorker implements Callable<SemanticsResult> {
45 private static final String DIAGNOSTIC_ID = "tools.refinery.language.semantics.SemanticError";
46
47 @Inject
48 private SemanticsUtils semanticsUtils;
49
50 @Inject
51 private OperationCanceledManager operationCanceledManager;
52
53 @Inject
54 private IDiagnosticConverter diagnosticConverter;
55
56 @Inject
57 private ModelInitializer initializer;
58
59 @Inject
60 private MetadataCreator metadataCreator;
61
62 private Problem problem;
63
64 private CancellationToken cancellationToken;
65
66 public void setProblem(Problem problem, CancelIndicator parentIndicator) {
67 this.problem = problem;
68 cancellationToken = () -> {
69 if (Thread.interrupted() || parentIndicator.isCanceled()) {
70 operationCanceledManager.throwOperationCanceledException();
71 }
72 };
73 }
74
75 @Override
76 public SemanticsResult call() {
77 var builder = ModelStore.builder()
78 .with(ViatraModelQueryAdapter.builder()
79 .cancellationToken(cancellationToken))
80 .with(ReasoningAdapter.builder()
81 .requiredInterpretations(Concreteness.PARTIAL))
82 .with(ScopePropagatorAdapter.builder());
83 cancellationToken.checkCancelled();
84 try {
85 var modelSeed = initializer.createModel(problem, builder);
86 cancellationToken.checkCancelled();
87 metadataCreator.setInitializer(initializer);
88 cancellationToken.checkCancelled();
89 var nodesMetadata = metadataCreator.getNodesMetadata();
90 cancellationToken.checkCancelled();
91 var relationsMetadata = metadataCreator.getRelationsMetadata();
92 cancellationToken.checkCancelled();
93 var store = builder.build();
94 cancellationToken.checkCancelled();
95 var cancellableModelSeed = CancellableSeed.wrap(cancellationToken, modelSeed);
96 var model = store.getAdapter(ReasoningStoreAdapter.class).createInitialModel(cancellableModelSeed);
97 if (model.getAdapter(ScopePropagatorAdapter.class).propagate() == RefinementResult.REJECTED) {
98 return new SemanticsInternalErrorResult("Scopes are unsatisfiable");
99 }
100 cancellationToken.checkCancelled();
101 var partialInterpretation = getPartialInterpretation(initializer, model);
102
103 return new SemanticsSuccessResult(nodesMetadata, relationsMetadata, partialInterpretation);
104 } catch (TracedException e) {
105 return getTracedErrorResult(e.getSourceElement(), e.getMessage());
106 } catch (TranslationException e) {
107 var sourceElement = initializer.getInverseTrace(e.getPartialSymbol());
108 return getTracedErrorResult(sourceElement, e.getMessage());
109 }
110 }
111
112 private JsonObject getPartialInterpretation(ModelInitializer initializer, Model model) {
113 var adapter = model.getAdapter(ReasoningAdapter.class);
114 var json = new JsonObject();
115 for (var entry : initializer.getRelationTrace().entrySet()) {
116 var relation = entry.getKey();
117 var partialSymbol = entry.getValue();
118 var tuples = getTuplesJson(adapter, partialSymbol);
119 var name = semanticsUtils.getName(relation).orElse(partialSymbol.name());
120 json.add(name, tuples);
121 cancellationToken.checkCancelled();
122 }
123 json.add("builtin::count", getCountJson(model));
124 return json;
125 }
126
127 private static JsonArray getTuplesJson(ReasoningAdapter adapter, PartialRelation partialSymbol) {
128 var interpretation = adapter.getPartialInterpretation(Concreteness.PARTIAL, partialSymbol);
129 var cursor = interpretation.getAll();
130 return getTuplesJson(cursor);
131 }
132
133 private static JsonArray getTuplesJson(Cursor<Tuple, ?> cursor) {
134 var map = new TreeMap<Tuple, Object>();
135 while (cursor.move()) {
136 map.put(cursor.getKey(), cursor.getValue());
137 }
138 var tuples = new JsonArray();
139 for (var entry : map.entrySet()) {
140 tuples.add(toArray(entry.getKey(), entry.getValue()));
141 }
142 return tuples;
143 }
144
145 private static JsonArray toArray(Tuple tuple, Object value) {
146 int arity = tuple.getSize();
147 var json = new JsonArray(arity + 1);
148 for (int i = 0; i < arity; i++) {
149 json.add(tuple.get(i));
150 }
151 json.add(value.toString());
152 return json;
153 }
154
155 private static JsonArray getCountJson(Model model) {
156 var interpretation = model.getInterpretation(MultiObjectTranslator.COUNT_STORAGE);
157 var cursor = interpretation.getAll();
158 return getTuplesJson(cursor);
159 }
160
161 private SemanticsResult getTracedErrorResult(EObject sourceElement, String message) {
162 if (sourceElement == null || !problem.eResource().equals(sourceElement.eResource())) {
163 return new SemanticsInternalErrorResult(message);
164 }
165 var diagnostic = new FeatureBasedDiagnostic(Diagnostic.ERROR, message, sourceElement, null, 0,
166 CheckType.EXPENSIVE, DIAGNOSTIC_ID);
167 var xtextIssues = new ArrayList<Issue>();
168 diagnosticConverter.convertValidatorDiagnostic(diagnostic, xtextIssues::add);
169 var issues = xtextIssues.stream()
170 .map(issue -> new ValidationResult.Issue(issue.getMessage(), "error", issue.getLineNumber(),
171 issue.getColumn(), issue.getOffset(), issue.getLength()))
172 .toList();
173 return new SemanticsIssuesResult(issues);
174 }
175}
diff --git a/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/server/ThreadPoolExecutorServiceProvider.java b/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/server/ThreadPoolExecutorServiceProvider.java
new file mode 100644
index 00000000..ba26ff58
--- /dev/null
+++ b/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/server/ThreadPoolExecutorServiceProvider.java
@@ -0,0 +1,110 @@
1/*
2 * SPDX-FileCopyrightText: 2023 The Refinery Authors <https://refinery.tools/>
3 *
4 * SPDX-License-Identifier: EPL-2.0
5 */
6package tools.refinery.language.web.xtext.server;
7
8import com.google.inject.Singleton;
9import org.eclipse.xtext.ide.ExecutorServiceProvider;
10import org.eclipse.xtext.web.server.model.XtextWebDocumentAccess;
11import org.jetbrains.annotations.NotNull;
12import tools.refinery.language.web.semantics.SemanticsService;
13
14import java.lang.invoke.MethodHandle;
15import java.lang.invoke.MethodHandles;
16import java.util.Optional;
17import java.util.concurrent.ExecutorService;
18import java.util.concurrent.Executors;
19import java.util.concurrent.ThreadFactory;
20import java.util.concurrent.atomic.AtomicInteger;
21
22@Singleton
23public class ThreadPoolExecutorServiceProvider extends ExecutorServiceProvider {
24 private static final String DOCUMENT_LOCK_EXECUTOR;
25 private static final AtomicInteger POOL_ID = new AtomicInteger(1);
26
27 private final int executorThreadCount;
28 private final int lockExecutorThreadCount;
29 private final int semanticsExecutorThreadCount;
30
31 static {
32 var lookup = MethodHandles.lookup();
33 MethodHandle getter;
34 try {
35 var privateLookup = MethodHandles.privateLookupIn(XtextWebDocumentAccess.class, lookup);
36 getter = privateLookup.findStaticGetter(XtextWebDocumentAccess.class, "DOCUMENT_LOCK_EXECUTOR",
37 String.class);
38 } catch (IllegalAccessException | NoSuchFieldException e) {
39 throw new IllegalStateException("Failed to find getter", e);
40 }
41 try {
42 DOCUMENT_LOCK_EXECUTOR = (String) getter.invokeExact();
43 } catch (Error e) {
44 // Rethrow JVM errors.
45 throw e;
46 } catch (Throwable e) {
47 throw new IllegalStateException("Failed to get DOCUMENT_LOCK_EXECUTOR", e);
48 }
49 }
50
51 public ThreadPoolExecutorServiceProvider() {
52 executorThreadCount = getCount("REFINERY_XTEXT_THREAD_COUNT").orElse(0);
53 lockExecutorThreadCount = getCount("REFINERY_XTEXT_LOCKING_THREAD_COUNT").orElse(executorThreadCount);
54 semanticsExecutorThreadCount = getCount("REFINERY_XTEXT_SEMANTICS_THREAD_COUNT").orElse(executorThreadCount);
55 }
56
57 private static Optional<Integer> getCount(String name) {
58 return Optional.ofNullable(System.getenv(name)).map(Integer::parseUnsignedInt);
59 }
60
61 @Override
62 protected ExecutorService createInstance(String key) {
63 String name = "xtext-" + POOL_ID.getAndIncrement();
64 if (key != null) {
65 name = name + key + "-";
66 }
67 var threadFactory = new Factory(name, 5);
68 int size = getSize(key);
69 if (size == 0) {
70 return Executors.newCachedThreadPool(threadFactory);
71 }
72 return Executors.newFixedThreadPool(size, threadFactory);
73 }
74
75 private int getSize(String key) {
76 if (SemanticsService.SEMANTICS_EXECUTOR.equals(key)) {
77 return semanticsExecutorThreadCount;
78 } else if (DOCUMENT_LOCK_EXECUTOR.equals(key)) {
79 return lockExecutorThreadCount;
80 } else {
81 return executorThreadCount;
82 }
83 }
84
85 private static class Factory implements ThreadFactory {
86 // We have to explicitly store the {@link ThreadGroup} to create a {@link ThreadFactory}.
87 @SuppressWarnings("squid:S3014")
88 private final ThreadGroup threadGroup = Thread.currentThread().getThreadGroup();
89 private final AtomicInteger threadId = new AtomicInteger(1);
90 private final String namePrefix;
91 private final int priority;
92
93 public Factory(String name, int priority) {
94 namePrefix = name + "-thread-";
95 this.priority = priority;
96 }
97
98 @Override
99 public Thread newThread(@NotNull Runnable runnable) {
100 var thread = new Thread(threadGroup, runnable, namePrefix + threadId.getAndIncrement());
101 if (thread.isDaemon()) {
102 thread.setDaemon(false);
103 }
104 if (thread.getPriority() != priority) {
105 thread.setPriority(priority);
106 }
107 return thread;
108 }
109 }
110}
diff --git a/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/server/TransactionExecutor.java b/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/server/TransactionExecutor.java
index 0135d8f5..74456604 100644
--- a/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/server/TransactionExecutor.java
+++ b/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/server/TransactionExecutor.java
@@ -42,6 +42,8 @@ public class TransactionExecutor implements IDisposable, PrecomputationListener
42 42
43 private final List<XtextWebPushMessage> pendingPushMessages = new ArrayList<>(); 43 private final List<XtextWebPushMessage> pendingPushMessages = new ArrayList<>();
44 44
45 private volatile boolean disposed;
46
45 public TransactionExecutor(ISession session, IResourceServiceProvider.Registry resourceServiceProviderRegistry) { 47 public TransactionExecutor(ISession session, IResourceServiceProvider.Registry resourceServiceProviderRegistry) {
46 this.session = session; 48 this.session = session;
47 this.resourceServiceProviderRegistry = resourceServiceProviderRegistry; 49 this.resourceServiceProviderRegistry = resourceServiceProviderRegistry;
@@ -52,10 +54,13 @@ public class TransactionExecutor implements IDisposable, PrecomputationListener
52 } 54 }
53 55
54 public void handleRequest(XtextWebRequest request) throws ResponseHandlerException { 56 public void handleRequest(XtextWebRequest request) throws ResponseHandlerException {
57 if (disposed) {
58 return;
59 }
55 var serviceContext = new SimpleServiceContext(session, request.getRequestData()); 60 var serviceContext = new SimpleServiceContext(session, request.getRequestData());
56 var ping = serviceContext.getParameter("ping"); 61 var ping = serviceContext.getParameter("ping");
57 if (ping != null) { 62 if (ping != null) {
58 responseHandler.onResponse(new XtextWebOkResponse(request, new PongResult(ping))); 63 onResponse(new XtextWebOkResponse(request, new PongResult(ping)));
59 return; 64 return;
60 } 65 }
61 synchronized (callPendingLock) { 66 synchronized (callPendingLock) {
@@ -72,23 +77,36 @@ public class TransactionExecutor implements IDisposable, PrecomputationListener
72 var serviceDispatcher = injector.getInstance(XtextServiceDispatcher.class); 77 var serviceDispatcher = injector.getInstance(XtextServiceDispatcher.class);
73 var service = serviceDispatcher.getService(new SubscribingServiceContext(serviceContext, this)); 78 var service = serviceDispatcher.getService(new SubscribingServiceContext(serviceContext, this));
74 var serviceResult = service.getService().apply(); 79 var serviceResult = service.getService().apply();
75 responseHandler.onResponse(new XtextWebOkResponse(request, serviceResult)); 80 onResponse(new XtextWebOkResponse(request, serviceResult));
76 } catch (InvalidRequestException e) { 81 } catch (InvalidRequestException e) {
77 responseHandler.onResponse(new XtextWebErrorResponse(request, XtextWebErrorKind.REQUEST_ERROR, e)); 82 onResponse(new XtextWebErrorResponse(request, XtextWebErrorKind.REQUEST_ERROR, e));
78 } catch (RuntimeException e) { 83 } catch (RuntimeException e) {
79 responseHandler.onResponse(new XtextWebErrorResponse(request, XtextWebErrorKind.SERVER_ERROR, e)); 84 onResponse(new XtextWebErrorResponse(request, XtextWebErrorKind.SERVER_ERROR, e));
80 } finally { 85 } finally {
81 synchronized (callPendingLock) { 86 flushPendingPushMessages();
82 for (var message : pendingPushMessages) { 87 }
83 try { 88 }
84 responseHandler.onResponse(message); 89
85 } catch (ResponseHandlerException | RuntimeException e) { 90 private void onResponse(XtextWebResponse response) throws ResponseHandlerException {
86 LOG.error("Error while flushing push message", e); 91 if (!disposed) {
87 } 92 responseHandler.onResponse(response);
93 }
94 }
95
96 private void flushPendingPushMessages() {
97 synchronized (callPendingLock) {
98 for (var message : pendingPushMessages) {
99 if (disposed) {
100 return;
101 }
102 try {
103 responseHandler.onResponse(message);
104 } catch (ResponseHandlerException | RuntimeException e) {
105 LOG.error("Error while flushing push message", e);
88 } 106 }
89 pendingPushMessages.clear();
90 callPending = false;
91 } 107 }
108 pendingPushMessages.clear();
109 callPending = false;
92 } 110 }
93 } 111 }
94 112
@@ -134,7 +152,7 @@ public class TransactionExecutor implements IDisposable, PrecomputationListener
134 * @throws UnknownLanguageException if the Xtext language cannot be determined 152 * @throws UnknownLanguageException if the Xtext language cannot be determined
135 */ 153 */
136 protected Injector getInjector(IServiceContext context) { 154 protected Injector getInjector(IServiceContext context) {
137 IResourceServiceProvider resourceServiceProvider = null; 155 IResourceServiceProvider resourceServiceProvider;
138 var resourceName = context.getParameter("resource"); 156 var resourceName = context.getParameter("resource");
139 if (resourceName == null) { 157 if (resourceName == null) {
140 resourceName = ""; 158 resourceName = "";
@@ -164,10 +182,12 @@ public class TransactionExecutor implements IDisposable, PrecomputationListener
164 182
165 @Override 183 @Override
166 public void dispose() { 184 public void dispose() {
185 disposed = true;
167 for (var subscription : subscriptions.values()) { 186 for (var subscription : subscriptions.values()) {
168 var document = subscription.get(); 187 var document = subscription.get();
169 if (document != null) { 188 if (document != null) {
170 document.removePrecomputationListener(this); 189 document.removePrecomputationListener(this);
190 document.cancelBackgroundWork();
171 } 191 }
172 } 192 }
173 } 193 }
diff --git a/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/server/message/XtextWebOkResponse.java b/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/server/message/XtextWebOkResponse.java
index 73527ee5..c3379329 100644
--- a/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/server/message/XtextWebOkResponse.java
+++ b/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/server/message/XtextWebOkResponse.java
@@ -5,12 +5,11 @@
5 */ 5 */
6package tools.refinery.language.web.xtext.server.message; 6package tools.refinery.language.web.xtext.server.message;
7 7
8import java.util.Objects; 8import com.google.gson.annotations.SerializedName;
9
10import org.eclipse.xtext.web.server.IServiceResult; 9import org.eclipse.xtext.web.server.IServiceResult;
11import org.eclipse.xtext.web.server.IUnwrappableServiceResult; 10import org.eclipse.xtext.web.server.IUnwrappableServiceResult;
12 11
13import com.google.gson.annotations.SerializedName; 12import java.util.Objects;
14 13
15public final class XtextWebOkResponse implements XtextWebResponse { 14public final class XtextWebOkResponse implements XtextWebResponse {
16 private String id; 15 private String id;
@@ -19,7 +18,6 @@ public final class XtextWebOkResponse implements XtextWebResponse {
19 private Object responseData; 18 private Object responseData;
20 19
21 public XtextWebOkResponse(String id, Object responseData) { 20 public XtextWebOkResponse(String id, Object responseData) {
22 super();
23 this.id = id; 21 this.id = id;
24 this.responseData = responseData; 22 this.responseData = responseData;
25 } 23 }
diff --git a/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/server/message/XtextWebRequest.java b/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/server/message/XtextWebRequest.java
index ff788e94..7c4562bf 100644
--- a/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/server/message/XtextWebRequest.java
+++ b/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/server/message/XtextWebRequest.java
@@ -5,19 +5,22 @@
5 */ 5 */
6package tools.refinery.language.web.xtext.server.message; 6package tools.refinery.language.web.xtext.server.message;
7 7
8import com.google.gson.annotations.SerializedName;
9
8import java.util.Map; 10import java.util.Map;
9import java.util.Objects; 11import java.util.Objects;
10 12
11import com.google.gson.annotations.SerializedName;
12
13public class XtextWebRequest { 13public class XtextWebRequest {
14 private String id; 14 private String id;
15 15
16 @SerializedName("request") 16 @SerializedName("request")
17 private Map<String, String> requestData; 17 private Map<String, String> requestData;
18 18
19 public XtextWebRequest() {
20 this(null, null);
21 }
22
19 public XtextWebRequest(String id, Map<String, String> requestData) { 23 public XtextWebRequest(String id, Map<String, String> requestData) {
20 super();
21 this.id = id; 24 this.id = id;
22 this.requestData = requestData; 25 this.requestData = requestData;
23 } 26 }
diff --git a/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/server/message/XtextWebResponse.java b/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/server/message/XtextWebResponse.java
index 61444c99..c370fb56 100644
--- a/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/server/message/XtextWebResponse.java
+++ b/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/server/message/XtextWebResponse.java
@@ -5,5 +5,5 @@
5 */ 5 */
6package tools.refinery.language.web.xtext.server.message; 6package tools.refinery.language.web.xtext.server.message;
7 7
8public sealed interface XtextWebResponse permits XtextWebOkResponse,XtextWebErrorResponse,XtextWebPushMessage { 8public sealed interface XtextWebResponse permits XtextWebOkResponse, XtextWebErrorResponse, XtextWebPushMessage {
9} 9}
diff --git a/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/server/push/PushServiceDispatcher.java b/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/server/push/PushServiceDispatcher.java
index 4c9135c8..d4a8c433 100644
--- a/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/server/push/PushServiceDispatcher.java
+++ b/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/server/push/PushServiceDispatcher.java
@@ -5,16 +5,28 @@
5 */ 5 */
6package tools.refinery.language.web.xtext.server.push; 6package tools.refinery.language.web.xtext.server.push;
7 7
8import com.google.inject.Inject;
8import org.eclipse.xtext.web.server.IServiceContext; 9import org.eclipse.xtext.web.server.IServiceContext;
9import org.eclipse.xtext.web.server.XtextServiceDispatcher; 10import org.eclipse.xtext.web.server.XtextServiceDispatcher;
11import org.eclipse.xtext.web.server.model.PrecomputedServiceRegistry;
10import org.eclipse.xtext.web.server.model.XtextWebDocument; 12import org.eclipse.xtext.web.server.model.XtextWebDocument;
11 13
12import com.google.inject.Singleton; 14import com.google.inject.Singleton;
13 15
16import tools.refinery.language.web.semantics.SemanticsService;
14import tools.refinery.language.web.xtext.server.SubscribingServiceContext; 17import tools.refinery.language.web.xtext.server.SubscribingServiceContext;
15 18
16@Singleton 19@Singleton
17public class PushServiceDispatcher extends XtextServiceDispatcher { 20public class PushServiceDispatcher extends XtextServiceDispatcher {
21 @Inject
22 private SemanticsService semanticsService;
23
24 @Override
25 @Inject
26 protected void registerPreComputedServices(PrecomputedServiceRegistry registry) {
27 super.registerPreComputedServices(registry);
28 registry.addPrecomputedService(semanticsService);
29 }
18 30
19 @Override 31 @Override
20 protected XtextWebDocument getFullTextDocument(String fullText, String resourceId, IServiceContext context) { 32 protected XtextWebDocument getFullTextDocument(String fullText, String resourceId, IServiceContext context) {
diff --git a/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/server/push/PushWebDocument.java b/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/server/push/PushWebDocument.java
index 56fd12c9..2d43fb26 100644
--- a/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/server/push/PushWebDocument.java
+++ b/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/server/push/PushWebDocument.java
@@ -5,11 +5,7 @@
5 */ 5 */
6package tools.refinery.language.web.xtext.server.push; 6package tools.refinery.language.web.xtext.server.push;
7 7
8import java.util.ArrayList; 8import com.google.common.collect.ImmutableList;
9import java.util.HashMap;
10import java.util.List;
11import java.util.Map;
12
13import org.eclipse.xtext.util.CancelIndicator; 9import org.eclipse.xtext.util.CancelIndicator;
14import org.eclipse.xtext.web.server.IServiceResult; 10import org.eclipse.xtext.web.server.IServiceResult;
15import org.eclipse.xtext.web.server.model.AbstractCachedService; 11import org.eclipse.xtext.web.server.model.AbstractCachedService;
@@ -17,11 +13,13 @@ import org.eclipse.xtext.web.server.model.DocumentSynchronizer;
17import org.eclipse.xtext.web.server.model.XtextWebDocument; 13import org.eclipse.xtext.web.server.model.XtextWebDocument;
18import org.slf4j.Logger; 14import org.slf4j.Logger;
19import org.slf4j.LoggerFactory; 15import org.slf4j.LoggerFactory;
20
21import com.google.common.collect.ImmutableList;
22
23import tools.refinery.language.web.xtext.server.ResponseHandlerException; 16import tools.refinery.language.web.xtext.server.ResponseHandlerException;
24 17
18import java.util.ArrayList;
19import java.util.HashMap;
20import java.util.List;
21import java.util.Map;
22
25public class PushWebDocument extends XtextWebDocument { 23public class PushWebDocument extends XtextWebDocument {
26 private static final Logger LOG = LoggerFactory.getLogger(PushWebDocument.class); 24 private static final Logger LOG = LoggerFactory.getLogger(PushWebDocument.class);
27 25
@@ -29,48 +27,44 @@ public class PushWebDocument extends XtextWebDocument {
29 27
30 private final Map<Class<?>, IServiceResult> precomputedServices = new HashMap<>(); 28 private final Map<Class<?>, IServiceResult> precomputedServices = new HashMap<>();
31 29
30 private final DocumentSynchronizer synchronizer;
31
32 public PushWebDocument(String resourceId, DocumentSynchronizer synchronizer) { 32 public PushWebDocument(String resourceId, DocumentSynchronizer synchronizer) {
33 super(resourceId, synchronizer); 33 super(resourceId, synchronizer);
34 if (resourceId == null) { 34 this.synchronizer = synchronizer;
35 throw new IllegalArgumentException("resourceId must not be null");
36 }
37 } 35 }
38 36
39 public boolean addPrecomputationListener(PrecomputationListener listener) { 37 public void addPrecomputationListener(PrecomputationListener listener) {
40 synchronized (precomputationListeners) { 38 synchronized (precomputationListeners) {
41 if (precomputationListeners.contains(listener)) { 39 if (precomputationListeners.contains(listener)) {
42 return false; 40 return;
43 } 41 }
44 precomputationListeners.add(listener); 42 precomputationListeners.add(listener);
45 listener.onSubscribeToPrecomputationEvents(getResourceId(), this); 43 listener.onSubscribeToPrecomputationEvents(getResourceId(), this);
46 return true;
47 } 44 }
48 } 45 }
49 46
50 public boolean removePrecomputationListener(PrecomputationListener listener) { 47 public void removePrecomputationListener(PrecomputationListener listener) {
51 synchronized (precomputationListeners) { 48 synchronized (precomputationListeners) {
52 return precomputationListeners.remove(listener); 49 precomputationListeners.remove(listener);
53 } 50 }
54 } 51 }
55 52
56 public <T extends IServiceResult> void precomputeServiceResult(AbstractCachedService<T> service, String serviceName, 53 public <T extends IServiceResult> void precomputeServiceResult(AbstractCachedService<T> service, String serviceName,
57 CancelIndicator cancelIndicator, boolean logCacheMiss) { 54 CancelIndicator cancelIndicator, boolean logCacheMiss) {
58 var result = getCachedServiceResult(service, cancelIndicator, logCacheMiss);
59 if (result == null) {
60 LOG.error("{} service returned null result", serviceName);
61 return;
62 }
63 var serviceClass = service.getClass(); 55 var serviceClass = service.getClass();
64 var previousResult = precomputedServices.get(serviceClass); 56 var result = getCachedServiceResult(service, cancelIndicator, logCacheMiss);
65 if (previousResult != null && previousResult.equals(result)) {
66 return;
67 }
68 precomputedServices.put(serviceClass, result); 57 precomputedServices.put(serviceClass, result);
69 notifyPrecomputationListeners(serviceName, result); 58 if (result != null) {
59 notifyPrecomputationListeners(serviceName, result);
60 }
70 } 61 }
71 62
72 private <T extends IServiceResult> void notifyPrecomputationListeners(String serviceName, T result) { 63 private <T extends IServiceResult> void notifyPrecomputationListeners(String serviceName, T result) {
73 var resourceId = getResourceId(); 64 var resourceId = getResourceId();
65 if (resourceId == null) {
66 return;
67 }
74 var stateId = getStateId(); 68 var stateId = getStateId();
75 List<PrecomputationListener> copyOfListeners; 69 List<PrecomputationListener> copyOfListeners;
76 synchronized (precomputationListeners) { 70 synchronized (precomputationListeners) {
@@ -91,4 +85,8 @@ public class PushWebDocument extends XtextWebDocument {
91 } 85 }
92 } 86 }
93 } 87 }
88
89 public void cancelBackgroundWork() {
90 synchronizer.setCanceled(true);
91 }
94} 92}
diff --git a/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/server/push/PushWebDocumentAccess.java b/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/server/push/PushWebDocumentAccess.java
index d9e548cd..c72e8e67 100644
--- a/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/server/push/PushWebDocumentAccess.java
+++ b/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/server/push/PushWebDocumentAccess.java
@@ -18,6 +18,7 @@ import org.eclipse.xtext.web.server.syntaxcoloring.HighlightingService;
18import org.eclipse.xtext.web.server.validation.ValidationService; 18import org.eclipse.xtext.web.server.validation.ValidationService;
19 19
20import com.google.inject.Inject; 20import com.google.inject.Inject;
21import tools.refinery.language.web.semantics.SemanticsService;
21 22
22public class PushWebDocumentAccess extends XtextWebDocumentAccess { 23public class PushWebDocumentAccess extends XtextWebDocumentAccess {
23 24
@@ -49,7 +50,7 @@ public class PushWebDocumentAccess extends XtextWebDocumentAccess {
49 precomputeServiceResult(service, false); 50 precomputeServiceResult(service, false);
50 } 51 }
51 } 52 }
52 53
53 protected <T extends IServiceResult> void precomputeServiceResult(AbstractCachedService<T> service, boolean logCacheMiss) { 54 protected <T extends IServiceResult> void precomputeServiceResult(AbstractCachedService<T> service, boolean logCacheMiss) {
54 var serviceName = getPrecomputedServiceName(service); 55 var serviceName = getPrecomputedServiceName(service);
55 readOnly(new CancelableUnitOfWork<Void, IXtextWebDocument>() { 56 readOnly(new CancelableUnitOfWork<Void, IXtextWebDocument>() {
@@ -60,7 +61,7 @@ public class PushWebDocumentAccess extends XtextWebDocumentAccess {
60 } 61 }
61 }); 62 });
62 } 63 }
63 64
64 protected String getPrecomputedServiceName(AbstractCachedService<? extends IServiceResult> service) { 65 protected String getPrecomputedServiceName(AbstractCachedService<? extends IServiceResult> service) {
65 if (service instanceof ValidationService) { 66 if (service instanceof ValidationService) {
66 return "validate"; 67 return "validate";
@@ -68,6 +69,9 @@ public class PushWebDocumentAccess extends XtextWebDocumentAccess {
68 if (service instanceof HighlightingService) { 69 if (service instanceof HighlightingService) {
69 return "highlight"; 70 return "highlight";
70 } 71 }
72 if (service instanceof SemanticsService) {
73 return "semantics";
74 }
71 throw new IllegalArgumentException("Unknown precomputed service: " + service); 75 throw new IllegalArgumentException("Unknown precomputed service: " + service);
72 } 76 }
73} 77}
diff --git a/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/server/push/PushWebDocumentProvider.java b/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/server/push/PushWebDocumentProvider.java
index b6f4fb43..ec6204ef 100644
--- a/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/server/push/PushWebDocumentProvider.java
+++ b/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/server/push/PushWebDocumentProvider.java
@@ -27,12 +27,7 @@ public class PushWebDocumentProvider implements IWebDocumentProvider {
27 27
28 @Override 28 @Override
29 public XtextWebDocument get(String resourceId, IServiceContext serviceContext) { 29 public XtextWebDocument get(String resourceId, IServiceContext serviceContext) {
30 if (resourceId == null) { 30 return new PushWebDocument(resourceId,
31 return new XtextWebDocument(null, synchronizerProvider.get()); 31 serviceContext.getSession().get(DocumentSynchronizer.class, () -> this.synchronizerProvider.get()));
32 } else {
33 // We only need to send push messages if a resourceId is specified.
34 return new PushWebDocument(resourceId,
35 serviceContext.getSession().get(DocumentSynchronizer.class, () -> this.synchronizerProvider.get()));
36 }
37 } 32 }
38} 33}
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 }
diff --git a/subprojects/language-web/src/test/java/tools/refinery/language/web/ProblemWebSocketServletIntegrationTest.java b/subprojects/language-web/src/test/java/tools/refinery/language/web/ProblemWebSocketServletIntegrationTest.java
index 927eeab1..889a55cb 100644
--- a/subprojects/language-web/src/test/java/tools/refinery/language/web/ProblemWebSocketServletIntegrationTest.java
+++ b/subprojects/language-web/src/test/java/tools/refinery/language/web/ProblemWebSocketServletIntegrationTest.java
@@ -93,7 +93,7 @@ class ProblemWebSocketServletIntegrationTest {
93 clientSocket.waitForTestResult(); 93 clientSocket.waitForTestResult();
94 assertThat(clientSocket.getCloseStatusCode(), equalTo(StatusCode.NORMAL)); 94 assertThat(clientSocket.getCloseStatusCode(), equalTo(StatusCode.NORMAL));
95 var responses = clientSocket.getResponses(); 95 var responses = clientSocket.getResponses();
96 assertThat(responses, hasSize(5)); 96 assertThat(responses, hasSize(8));
97 assertThat(responses.get(0), equalTo("{\"id\":\"foo\",\"response\":{\"stateId\":\"-80000000\"}}")); 97 assertThat(responses.get(0), equalTo("{\"id\":\"foo\",\"response\":{\"stateId\":\"-80000000\"}}"));
98 assertThat(responses.get(1), startsWith( 98 assertThat(responses.get(1), startsWith(
99 "{\"resource\":\"test.problem\",\"stateId\":\"-80000000\",\"service\":\"highlight\"," + 99 "{\"resource\":\"test.problem\",\"stateId\":\"-80000000\",\"service\":\"highlight\"," +
@@ -101,10 +101,19 @@ class ProblemWebSocketServletIntegrationTest {
101 assertThat(responses.get(2), equalTo( 101 assertThat(responses.get(2), equalTo(
102 "{\"resource\":\"test.problem\",\"stateId\":\"-80000000\",\"service\":\"validate\"," + 102 "{\"resource\":\"test.problem\",\"stateId\":\"-80000000\",\"service\":\"validate\"," +
103 "\"push\":{\"issues\":[]}}")); 103 "\"push\":{\"issues\":[]}}"));
104 assertThat(responses.get(3), equalTo("{\"id\":\"bar\",\"response\":{\"stateId\":\"-7fffffff\"}}")); 104 assertThat(responses.get(3), startsWith(
105 assertThat(responses.get(4), startsWith( 105 "{\"resource\":\"test.problem\",\"stateId\":\"-80000000\",\"service\":\"semantics\"," +
106 "\"push\":{"));
107 assertThat(responses.get(4), equalTo("{\"id\":\"bar\",\"response\":{\"stateId\":\"-7fffffff\"}}"));
108 assertThat(responses.get(5), startsWith(
106 "{\"resource\":\"test.problem\",\"stateId\":\"-7fffffff\",\"service\":\"highlight\"," + 109 "{\"resource\":\"test.problem\",\"stateId\":\"-7fffffff\",\"service\":\"highlight\"," +
107 "\"push\":{\"regions\":[")); 110 "\"push\":{\"regions\":["));
111 assertThat(responses.get(6), equalTo(
112 "{\"resource\":\"test.problem\",\"stateId\":\"-7fffffff\",\"service\":\"validate\"," +
113 "\"push\":{\"issues\":[]}}"));
114 assertThat(responses.get(7), startsWith(
115 "{\"resource\":\"test.problem\",\"stateId\":\"-7fffffff\",\"service\":\"semantics\"," +
116 "\"push\":{"));
108 } 117 }
109 118
110 @WebSocket 119 @WebSocket
@@ -117,14 +126,14 @@ class ProblemWebSocketServletIntegrationTest {
117 "\"fullText\":\"class Person.\n\"}}", 126 "\"fullText\":\"class Person.\n\"}}",
118 Callback.NOOP 127 Callback.NOOP
119 ); 128 );
120 case 3 -> //noinspection TextBlockMigration 129 case 4 -> //noinspection TextBlockMigration
121 session.sendText( 130 session.sendText(
122 "{\"id\":\"bar\",\"request\":{\"resource\":\"test.problem\",\"serviceType\":\"update\"," + 131 "{\"id\":\"bar\",\"request\":{\"resource\":\"test.problem\",\"serviceType\":\"update\"," +
123 "\"requiredStateId\":\"-80000000\",\"deltaText\":\"indiv q.\nnode(q).\n\"," + 132 "\"requiredStateId\":\"-80000000\",\"deltaText\":\"indiv q.\nnode(q).\n\"," +
124 "\"deltaOffset\":\"0\",\"deltaReplaceLength\":\"0\"}}", 133 "\"deltaOffset\":\"0\",\"deltaReplaceLength\":\"0\"}}",
125 Callback.NOOP 134 Callback.NOOP
126 ); 135 );
127 case 5 -> session.close(); 136 case 8 -> session.close();
128 } 137 }
129 } 138 }
130 } 139 }
diff --git a/subprojects/language-web/src/test/java/tools/refinery/language/web/tests/ProblemWebInjectorProvider.java b/subprojects/language-web/src/test/java/tools/refinery/language/web/tests/ProblemWebInjectorProvider.java
index 4a5eed95..e9d889c4 100644
--- a/subprojects/language-web/src/test/java/tools/refinery/language/web/tests/ProblemWebInjectorProvider.java
+++ b/subprojects/language-web/src/test/java/tools/refinery/language/web/tests/ProblemWebInjectorProvider.java
@@ -34,6 +34,7 @@ public class ProblemWebInjectorProvider extends ProblemInjectorProvider {
34 // the tasks in the service and the {@link 34 // the tasks in the service and the {@link
35 // org.eclipse.xtext.testing.extensions.InjectionExtension}. 35 // org.eclipse.xtext.testing.extensions.InjectionExtension}.
36 return new ProblemWebModule() { 36 return new ProblemWebModule() {
37 @Override
37 @SuppressWarnings("unused") 38 @SuppressWarnings("unused")
38 public Class<? extends ExecutorServiceProvider> bindExecutorServiceProvider() { 39 public Class<? extends ExecutorServiceProvider> bindExecutorServiceProvider() {
39 return AwaitTerminationExecutorServiceProvider.class; 40 return AwaitTerminationExecutorServiceProvider.class;
diff --git a/subprojects/language-web/src/test/java/tools/refinery/language/web/tests/RestartableCachedThreadPool.java b/subprojects/language-web/src/test/java/tools/refinery/language/web/tests/RestartableCachedThreadPool.java
index 09079aa8..991ff114 100644
--- a/subprojects/language-web/src/test/java/tools/refinery/language/web/tests/RestartableCachedThreadPool.java
+++ b/subprojects/language-web/src/test/java/tools/refinery/language/web/tests/RestartableCachedThreadPool.java
@@ -35,7 +35,7 @@ public class RestartableCachedThreadPool implements ExecutorService {
35 public void waitForTermination() { 35 public void waitForTermination() {
36 boolean result = false; 36 boolean result = false;
37 try { 37 try {
38 result = delegate.awaitTermination(1, TimeUnit.SECONDS); 38 result = delegate.awaitTermination(10, TimeUnit.SECONDS);
39 } catch (InterruptedException e) { 39 } catch (InterruptedException e) {
40 LOG.warn("Interrupted while waiting for delegate executor to stop", e); 40 LOG.warn("Interrupted while waiting for delegate executor to stop", e);
41 } 41 }
diff --git a/subprojects/language-web/src/test/java/tools/refinery/language/web/xtext/servlet/TransactionExecutorTest.java b/subprojects/language-web/src/test/java/tools/refinery/language/web/xtext/servlet/TransactionExecutorTest.java
index 841bacd3..22ce1b47 100644
--- a/subprojects/language-web/src/test/java/tools/refinery/language/web/xtext/servlet/TransactionExecutorTest.java
+++ b/subprojects/language-web/src/test/java/tools/refinery/language/web/xtext/servlet/TransactionExecutorTest.java
@@ -18,6 +18,7 @@ import org.junit.jupiter.api.Test;
18import org.junit.jupiter.api.extension.ExtendWith; 18import org.junit.jupiter.api.extension.ExtendWith;
19import org.mockito.ArgumentCaptor; 19import org.mockito.ArgumentCaptor;
20import org.mockito.junit.jupiter.MockitoExtension; 20import org.mockito.junit.jupiter.MockitoExtension;
21import tools.refinery.language.web.semantics.SemanticsService;
21import tools.refinery.language.web.tests.AwaitTerminationExecutorServiceProvider; 22import tools.refinery.language.web.tests.AwaitTerminationExecutorServiceProvider;
22import tools.refinery.language.web.tests.ProblemWebInjectorProvider; 23import tools.refinery.language.web.tests.ProblemWebInjectorProvider;
23import tools.refinery.language.web.xtext.server.ResponseHandler; 24import tools.refinery.language.web.xtext.server.ResponseHandler;
@@ -59,11 +60,16 @@ class TransactionExecutorTest {
59 @Inject 60 @Inject
60 private AwaitTerminationExecutorServiceProvider executorServices; 61 private AwaitTerminationExecutorServiceProvider executorServices;
61 62
63 @Inject
64 private SemanticsService semanticsService;
65
62 private TransactionExecutor transactionExecutor; 66 private TransactionExecutor transactionExecutor;
63 67
64 @BeforeEach 68 @BeforeEach
65 void beforeEach() { 69 void beforeEach() {
66 transactionExecutor = new TransactionExecutor(new SimpleSession(), resourceServiceProviderRegistry); 70 transactionExecutor = new TransactionExecutor(new SimpleSession(), resourceServiceProviderRegistry);
71 // Manually re-create the semantics analysis thread pool if it was disposed by the previous test.
72 semanticsService.setExecutorServiceProvider(executorServices);
67 } 73 }
68 74
69 @Test 75 @Test
@@ -95,7 +101,7 @@ class TransactionExecutorTest {
95 "0"))); 101 "0")));
96 102
97 var captor = newCaptor(); 103 var captor = newCaptor();
98 verify(responseHandler, times(2)).onResponse(captor.capture()); 104 verify(responseHandler, times(4)).onResponse(captor.capture());
99 var newStateId = getStateId("bar", captor.getAllValues().get(0)); 105 var newStateId = getStateId("bar", captor.getAllValues().get(0));
100 assertHighlightingResponse(newStateId, captor.getAllValues().get(1)); 106 assertHighlightingResponse(newStateId, captor.getAllValues().get(1));
101 } 107 }
@@ -126,7 +132,7 @@ class TransactionExecutorTest {
126 private String updateFullText(ArgumentCaptor<XtextWebResponse> captor) throws ResponseHandlerException { 132 private String updateFullText(ArgumentCaptor<XtextWebResponse> captor) throws ResponseHandlerException {
127 var responseHandler = sendRequestAndWaitForAllResponses(new XtextWebRequest("foo", UPDATE_FULL_TEXT_PARAMS)); 133 var responseHandler = sendRequestAndWaitForAllResponses(new XtextWebRequest("foo", UPDATE_FULL_TEXT_PARAMS));
128 134
129 verify(responseHandler, times(3)).onResponse(captor.capture()); 135 verify(responseHandler, times(4)).onResponse(captor.capture());
130 return getStateId("foo", captor.getAllValues().get(0)); 136 return getStateId("foo", captor.getAllValues().get(0));
131 } 137 }
132 138