aboutsummaryrefslogtreecommitdiffstats
path: root/subprojects/language-web
diff options
context:
space:
mode:
authorLibravatar Kristóf Marussy <marussy@mit.bme.hu>2023-09-14 19:29:36 +0200
committerLibravatar GitHub <noreply@github.com>2023-09-14 19:29:36 +0200
commit98ed3b6db5f4e51961a161050cc31c66015116e8 (patch)
tree8bfd6d9bc8d6ed23b9eb0f889dd40b6c24fe8f92 /subprojects/language-web
parentMerge pull request #38 from nagilooh/design-space-exploration (diff)
parentMerge remote-tracking branch 'upstream/main' into partial-interpretation (diff)
downloadrefinery-98ed3b6db5f4e51961a161050cc31c66015116e8.tar.gz
refinery-98ed3b6db5f4e51961a161050cc31c66015116e8.tar.zst
refinery-98ed3b6db5f4e51961a161050cc31c66015116e8.zip
Merge pull request #39 from kris7t/partial-interpretation
Implement partial interpretation based model generation
Diffstat (limited to 'subprojects/language-web')
-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/generator/ModelGenerationCancelledResult.java11
-rw-r--r--subprojects/language-web/src/main/java/tools/refinery/language/web/generator/ModelGenerationErrorResult.java11
-rw-r--r--subprojects/language-web/src/main/java/tools/refinery/language/web/generator/ModelGenerationManager.java41
-rw-r--r--subprojects/language-web/src/main/java/tools/refinery/language/web/generator/ModelGenerationResult.java15
-rw-r--r--subprojects/language-web/src/main/java/tools/refinery/language/web/generator/ModelGenerationService.java60
-rw-r--r--subprojects/language-web/src/main/java/tools/refinery/language/web/generator/ModelGenerationStartedResult.java13
-rw-r--r--subprojects/language-web/src/main/java/tools/refinery/language/web/generator/ModelGenerationStatusResult.java11
-rw-r--r--subprojects/language-web/src/main/java/tools/refinery/language/web/generator/ModelGenerationSuccessResult.java17
-rw-r--r--subprojects/language-web/src/main/java/tools/refinery/language/web/generator/ModelGenerationWorker.java233
-rw-r--r--subprojects/language-web/src/main/java/tools/refinery/language/web/semantics/PartialInterpretation2Json.java81
-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.java113
-rw-r--r--subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/server/ThreadPoolExecutorServiceProvider.java158
-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.java53
-rw-r--r--subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/server/push/PushWebDocument.java60
-rw-r--r--subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/server/push/PushWebDocumentAccess.java12
-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
37 files changed, 1489 insertions, 97 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/generator/ModelGenerationCancelledResult.java b/subprojects/language-web/src/main/java/tools/refinery/language/web/generator/ModelGenerationCancelledResult.java
new file mode 100644
index 00000000..fc06fd2e
--- /dev/null
+++ b/subprojects/language-web/src/main/java/tools/refinery/language/web/generator/ModelGenerationCancelledResult.java
@@ -0,0 +1,11 @@
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.generator;
7
8import org.eclipse.xtext.web.server.IServiceResult;
9
10public record ModelGenerationCancelledResult() implements IServiceResult {
11}
diff --git a/subprojects/language-web/src/main/java/tools/refinery/language/web/generator/ModelGenerationErrorResult.java b/subprojects/language-web/src/main/java/tools/refinery/language/web/generator/ModelGenerationErrorResult.java
new file mode 100644
index 00000000..bedaeb35
--- /dev/null
+++ b/subprojects/language-web/src/main/java/tools/refinery/language/web/generator/ModelGenerationErrorResult.java
@@ -0,0 +1,11 @@
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.generator;
7
8import java.util.UUID;
9
10public record ModelGenerationErrorResult(UUID uuid, String error) implements ModelGenerationResult {
11}
diff --git a/subprojects/language-web/src/main/java/tools/refinery/language/web/generator/ModelGenerationManager.java b/subprojects/language-web/src/main/java/tools/refinery/language/web/generator/ModelGenerationManager.java
new file mode 100644
index 00000000..b0a1912c
--- /dev/null
+++ b/subprojects/language-web/src/main/java/tools/refinery/language/web/generator/ModelGenerationManager.java
@@ -0,0 +1,41 @@
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.generator;
7
8import org.eclipse.xtext.util.CancelIndicator;
9
10public class ModelGenerationManager {
11 private final Object lockObject = new Object();
12 private ModelGenerationWorker worker;
13 private boolean disposed;
14
15 boolean setActiveModelGenerationWorker(ModelGenerationWorker worker, CancelIndicator cancelIndicator) {
16 synchronized (lockObject) {
17 cancel();
18 if (disposed || cancelIndicator.isCanceled()) {
19 return true;
20 }
21 this.worker = worker;
22 }
23 return false;
24 }
25
26 public void cancel() {
27 synchronized (lockObject) {
28 if (worker != null) {
29 worker.cancel();
30 worker = null;
31 }
32 }
33 }
34
35 public void dispose() {
36 synchronized (lockObject) {
37 disposed = true;
38 cancel();
39 }
40 }
41}
diff --git a/subprojects/language-web/src/main/java/tools/refinery/language/web/generator/ModelGenerationResult.java b/subprojects/language-web/src/main/java/tools/refinery/language/web/generator/ModelGenerationResult.java
new file mode 100644
index 00000000..cf06f447
--- /dev/null
+++ b/subprojects/language-web/src/main/java/tools/refinery/language/web/generator/ModelGenerationResult.java
@@ -0,0 +1,15 @@
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.generator;
7
8import org.eclipse.xtext.web.server.IServiceResult;
9
10import java.util.UUID;
11
12public sealed interface ModelGenerationResult extends IServiceResult permits ModelGenerationSuccessResult,
13 ModelGenerationErrorResult, ModelGenerationStatusResult {
14 UUID uuid();
15}
diff --git a/subprojects/language-web/src/main/java/tools/refinery/language/web/generator/ModelGenerationService.java b/subprojects/language-web/src/main/java/tools/refinery/language/web/generator/ModelGenerationService.java
new file mode 100644
index 00000000..9f72e462
--- /dev/null
+++ b/subprojects/language-web/src/main/java/tools/refinery/language/web/generator/ModelGenerationService.java
@@ -0,0 +1,60 @@
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.generator;
7
8import com.google.inject.Inject;
9import com.google.inject.Provider;
10import com.google.inject.Singleton;
11import org.eclipse.xtext.service.OperationCanceledManager;
12import org.eclipse.xtext.util.CancelIndicator;
13import org.eclipse.xtext.util.concurrent.CancelableUnitOfWork;
14import org.eclipse.xtext.web.server.model.IXtextWebDocument;
15import tools.refinery.language.web.semantics.SemanticsService;
16import tools.refinery.language.web.xtext.server.push.PushWebDocument;
17import tools.refinery.language.web.xtext.server.push.PushWebDocumentAccess;
18
19@Singleton
20public class ModelGenerationService {
21 public static final String SERVICE_NAME = "modelGeneration";
22 public static final String MODEL_GENERATION_EXECUTOR = "modelGeneration";
23 public static final String MODEL_GENERATION_TIMEOUT_EXECUTOR = "modelGenerationTimeout";
24
25 @Inject
26 private OperationCanceledManager operationCanceledManager;
27
28 @Inject
29 private Provider<ModelGenerationWorker> workerProvider;
30
31 private final long timeoutSec;
32
33 public ModelGenerationService() {
34 timeoutSec = SemanticsService.getTimeout("REFINERY_MODEL_GENERATION_TIMEOUT_SEC").orElse(600L);
35 }
36
37 public ModelGenerationStartedResult generateModel(PushWebDocumentAccess document, int randomSeed) {
38 return document.modify(new CancelableUnitOfWork<>() {
39 @Override
40 public ModelGenerationStartedResult exec(IXtextWebDocument state, CancelIndicator cancelIndicator) {
41 var pushState = (PushWebDocument) state;
42 var worker = workerProvider.get();
43 worker.setState(pushState, randomSeed, timeoutSec);
44 var manager = pushState.getModelGenerationManager();
45 worker.start();
46 boolean canceled = manager.setActiveModelGenerationWorker(worker, cancelIndicator);
47 if (canceled) {
48 worker.cancel();
49 operationCanceledManager.throwOperationCanceledException();
50 }
51 return new ModelGenerationStartedResult(worker.getUuid());
52 }
53 });
54 }
55
56 public ModelGenerationCancelledResult cancelModelGeneration(PushWebDocumentAccess document) {
57 document.cancelModelGeneration();
58 return new ModelGenerationCancelledResult();
59 }
60}
diff --git a/subprojects/language-web/src/main/java/tools/refinery/language/web/generator/ModelGenerationStartedResult.java b/subprojects/language-web/src/main/java/tools/refinery/language/web/generator/ModelGenerationStartedResult.java
new file mode 100644
index 00000000..8c0e73c7
--- /dev/null
+++ b/subprojects/language-web/src/main/java/tools/refinery/language/web/generator/ModelGenerationStartedResult.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.generator;
7
8import org.eclipse.xtext.web.server.IServiceResult;
9
10import java.util.UUID;
11
12public record ModelGenerationStartedResult(UUID uuid) implements IServiceResult {
13}
diff --git a/subprojects/language-web/src/main/java/tools/refinery/language/web/generator/ModelGenerationStatusResult.java b/subprojects/language-web/src/main/java/tools/refinery/language/web/generator/ModelGenerationStatusResult.java
new file mode 100644
index 00000000..a6589870
--- /dev/null
+++ b/subprojects/language-web/src/main/java/tools/refinery/language/web/generator/ModelGenerationStatusResult.java
@@ -0,0 +1,11 @@
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.generator;
7
8import java.util.UUID;
9
10public record ModelGenerationStatusResult(UUID uuid, String status) implements ModelGenerationResult {
11}
diff --git a/subprojects/language-web/src/main/java/tools/refinery/language/web/generator/ModelGenerationSuccessResult.java b/subprojects/language-web/src/main/java/tools/refinery/language/web/generator/ModelGenerationSuccessResult.java
new file mode 100644
index 00000000..21be4e08
--- /dev/null
+++ b/subprojects/language-web/src/main/java/tools/refinery/language/web/generator/ModelGenerationSuccessResult.java
@@ -0,0 +1,17 @@
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.generator;
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;
13import java.util.UUID;
14
15public record ModelGenerationSuccessResult(UUID uuid, List<NodeMetadata> nodes, List<RelationMetadata> relations,
16 JsonObject partialInterpretation) implements ModelGenerationResult {
17}
diff --git a/subprojects/language-web/src/main/java/tools/refinery/language/web/generator/ModelGenerationWorker.java b/subprojects/language-web/src/main/java/tools/refinery/language/web/generator/ModelGenerationWorker.java
new file mode 100644
index 00000000..9ee74207
--- /dev/null
+++ b/subprojects/language-web/src/main/java/tools/refinery/language/web/generator/ModelGenerationWorker.java
@@ -0,0 +1,233 @@
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.generator;
7
8import com.google.inject.Inject;
9import com.google.inject.Provider;
10import org.eclipse.emf.common.util.URI;
11import org.eclipse.xtext.diagnostics.Severity;
12import org.eclipse.xtext.resource.IResourceFactory;
13import org.eclipse.xtext.resource.XtextResourceSet;
14import org.eclipse.xtext.service.OperationCanceledManager;
15import org.eclipse.xtext.util.LazyStringInputStream;
16import org.eclipse.xtext.validation.CheckMode;
17import org.eclipse.xtext.validation.IResourceValidator;
18import org.slf4j.Logger;
19import org.slf4j.LoggerFactory;
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.web.semantics.PartialInterpretation2Json;
24import tools.refinery.language.web.xtext.server.ThreadPoolExecutorServiceProvider;
25import tools.refinery.language.web.xtext.server.push.PushWebDocument;
26import tools.refinery.store.dse.propagation.PropagationAdapter;
27import tools.refinery.store.dse.strategy.BestFirstStoreManager;
28import tools.refinery.store.dse.transition.DesignSpaceExplorationAdapter;
29import tools.refinery.store.model.ModelStore;
30import tools.refinery.store.query.viatra.ViatraModelQueryAdapter;
31import tools.refinery.store.reasoning.ReasoningAdapter;
32import tools.refinery.store.reasoning.ReasoningStoreAdapter;
33import tools.refinery.store.reasoning.literal.Concreteness;
34import tools.refinery.store.statecoding.StateCoderAdapter;
35import tools.refinery.store.util.CancellationToken;
36
37import java.io.IOException;
38import java.util.Map;
39import java.util.UUID;
40import java.util.concurrent.*;
41
42public class ModelGenerationWorker implements Runnable {
43 private static final Logger LOG = LoggerFactory.getLogger(ModelGenerationWorker.class);
44
45 private final UUID uuid = UUID.randomUUID();
46
47 private PushWebDocument state;
48
49 private String text;
50
51 private volatile boolean timedOut;
52
53 private volatile boolean cancelled;
54
55 @Inject
56 private OperationCanceledManager operationCanceledManager;
57
58 @Inject
59 private Provider<XtextResourceSet> resourceSetProvider;
60
61 @Inject
62 private IResourceFactory resourceFactory;
63
64 @Inject
65 private IResourceValidator resourceValidator;
66
67 @Inject
68 private ModelInitializer initializer;
69
70 @Inject
71 private MetadataCreator metadataCreator;
72
73 @Inject
74 private PartialInterpretation2Json partialInterpretation2Json;
75
76 private final Object lockObject = new Object();
77
78 private ExecutorService executorService;
79
80 private ScheduledExecutorService scheduledExecutorService;
81
82 private int randomSeed;
83
84 private long timeoutSec;
85
86 private Future<?> future;
87
88 private ScheduledFuture<?> timeoutFuture;
89
90 private final CancellationToken cancellationToken = () -> {
91 if (cancelled || Thread.interrupted()) {
92 operationCanceledManager.throwOperationCanceledException();
93 }
94 };
95
96 @Inject
97 public void setExecutorServiceProvider(ThreadPoolExecutorServiceProvider provider) {
98 executorService = provider.get(ModelGenerationService.MODEL_GENERATION_EXECUTOR);
99 scheduledExecutorService = provider.getScheduled(ModelGenerationService.MODEL_GENERATION_TIMEOUT_EXECUTOR);
100 }
101
102 public void setState(PushWebDocument state, int randomSeed, long timeoutSec) {
103 this.state = state;
104 this.randomSeed = randomSeed;
105 this.timeoutSec = timeoutSec;
106 text = state.getText();
107 }
108
109 public UUID getUuid() {
110 return uuid;
111 }
112
113 public void start() {
114 synchronized (lockObject) {
115 LOG.debug("Enqueueing model generation: {}", uuid);
116 future = executorService.submit(this);
117 }
118 }
119
120 public void startTimeout() {
121 synchronized (lockObject) {
122 LOG.debug("Starting model generation: {}", uuid);
123 cancellationToken.checkCancelled();
124 timeoutFuture = scheduledExecutorService.schedule(() -> cancel(true), timeoutSec, TimeUnit.SECONDS);
125 }
126 }
127
128 // We catch {@code Throwable} to handle {@code OperationCancelledError}, but we rethrow fatal JVM errors.
129 @SuppressWarnings("squid:S1181")
130 @Override
131 public void run() {
132 startTimeout();
133 notifyResult(new ModelGenerationStatusResult(uuid, "Initializing model generator"));
134 ModelGenerationResult result;
135 try {
136 result = doRun();
137 } catch (Throwable e) {
138 if (operationCanceledManager.isOperationCanceledException(e)) {
139 var message = timedOut ? "Model generation timed out" : "Model generation cancelled";
140 LOG.debug("{}: {}", message, uuid);
141 notifyResult(new ModelGenerationErrorResult(uuid, message));
142 } else if (e instanceof Error error) {
143 // Make sure we don't try to recover from any fatal JVM errors.
144 throw error;
145 } else {
146 LOG.debug("Model generation error", e);
147 notifyResult(new ModelGenerationErrorResult(uuid, e.toString()));
148 }
149 return;
150 }
151 notifyResult(result);
152 }
153
154 private void notifyResult(ModelGenerationResult result) {
155 state.notifyPrecomputationListeners(ModelGenerationService.SERVICE_NAME, result);
156 }
157
158 public ModelGenerationResult doRun() throws IOException {
159 cancellationToken.checkCancelled();
160 var resourceSet = resourceSetProvider.get();
161 var uri = URI.createURI("__synthetic_" + uuid + ".problem");
162 var resource = resourceFactory.createResource(uri);
163 resourceSet.getResources().add(resource);
164 var inputStream = new LazyStringInputStream(text);
165 resource.load(inputStream, Map.of());
166 cancellationToken.checkCancelled();
167 var issues = resourceValidator.validate(resource, CheckMode.ALL, () -> cancelled || Thread.interrupted());
168 cancellationToken.checkCancelled();
169 for (var issue : issues) {
170 if (issue.getSeverity() == Severity.ERROR) {
171 return new ModelGenerationErrorResult(uuid, "Validation error: " + issue.getMessage());
172 }
173 }
174 if (resource.getContents().isEmpty() || !(resource.getContents().get(0) instanceof Problem problem)) {
175 return new ModelGenerationErrorResult(uuid, "Model generation problem not found");
176 }
177 cancellationToken.checkCancelled();
178 var storeBuilder = ModelStore.builder()
179 .cancellationToken(cancellationToken)
180 .with(ViatraModelQueryAdapter.builder())
181 .with(PropagationAdapter.builder())
182 .with(StateCoderAdapter.builder())
183 .with(DesignSpaceExplorationAdapter.builder())
184 .with(ReasoningAdapter.builder()
185 .requiredInterpretations(Concreteness.CANDIDATE));
186 var modelSeed = initializer.createModel(problem, storeBuilder);
187 var store = storeBuilder.build();
188 cancellationToken.checkCancelled();
189 var model = store.getAdapter(ReasoningStoreAdapter.class).createInitialModel(modelSeed);
190 var initialVersion = model.commit();
191 cancellationToken.checkCancelled();
192 notifyResult(new ModelGenerationStatusResult(uuid, "Generating model"));
193 var bestFirst = new BestFirstStoreManager(store, 1);
194 bestFirst.startExploration(initialVersion, randomSeed);
195 cancellationToken.checkCancelled();
196 var solutionStore = bestFirst.getSolutionStore();
197 if (solutionStore.getSolutions().isEmpty()) {
198 return new ModelGenerationErrorResult(uuid, "Problem is unsatisfiable");
199 }
200 notifyResult(new ModelGenerationStatusResult(uuid, "Saving generated model"));
201 model.restore(solutionStore.getSolutions().get(0).version());
202 cancellationToken.checkCancelled();
203 metadataCreator.setInitializer(initializer);
204 var nodesMetadata = metadataCreator.getNodesMetadata(model.getAdapter(ReasoningAdapter.class).getNodeCount(),
205 false);
206 cancellationToken.checkCancelled();
207 var relationsMetadata = metadataCreator.getRelationsMetadata();
208 cancellationToken.checkCancelled();
209 var partialInterpretation = partialInterpretation2Json.getPartialInterpretation(initializer, model,
210 Concreteness.CANDIDATE, cancellationToken);
211 return new ModelGenerationSuccessResult(uuid, nodesMetadata, relationsMetadata, partialInterpretation);
212 }
213
214 public void cancel() {
215 cancel(false);
216 }
217
218 public void cancel(boolean timedOut) {
219 synchronized (lockObject) {
220 LOG.trace("Cancelling model generation: {}", uuid);
221 this.timedOut = timedOut;
222 cancelled = true;
223 if (future != null) {
224 future.cancel(true);
225 future = null;
226 }
227 if (timeoutFuture != null) {
228 timeoutFuture.cancel(true);
229 timeoutFuture = null;
230 }
231 }
232 }
233}
diff --git a/subprojects/language-web/src/main/java/tools/refinery/language/web/semantics/PartialInterpretation2Json.java b/subprojects/language-web/src/main/java/tools/refinery/language/web/semantics/PartialInterpretation2Json.java
new file mode 100644
index 00000000..5d5da8fe
--- /dev/null
+++ b/subprojects/language-web/src/main/java/tools/refinery/language/web/semantics/PartialInterpretation2Json.java
@@ -0,0 +1,81 @@
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 com.google.inject.Singleton;
12import tools.refinery.language.semantics.model.ModelInitializer;
13import tools.refinery.language.semantics.model.SemanticsUtils;
14import tools.refinery.store.map.Cursor;
15import tools.refinery.store.model.Model;
16import tools.refinery.store.reasoning.ReasoningAdapter;
17import tools.refinery.store.reasoning.literal.Concreteness;
18import tools.refinery.store.reasoning.representation.PartialRelation;
19import tools.refinery.store.reasoning.translator.multiobject.MultiObjectTranslator;
20import tools.refinery.store.tuple.Tuple;
21import tools.refinery.store.util.CancellationToken;
22
23import java.util.TreeMap;
24
25@Singleton
26public class PartialInterpretation2Json {
27 @Inject
28 private SemanticsUtils semanticsUtils;
29
30 public JsonObject getPartialInterpretation(ModelInitializer initializer, Model model, Concreteness concreteness,
31 CancellationToken cancellationToken) {
32 var adapter = model.getAdapter(ReasoningAdapter.class);
33 var json = new JsonObject();
34 for (var entry : initializer.getRelationTrace().entrySet()) {
35 var relation = entry.getKey();
36 var partialSymbol = entry.getValue();
37 var tuples = getTuplesJson(adapter, concreteness, partialSymbol);
38 var name = semanticsUtils.getName(relation).orElse(partialSymbol.name());
39 json.add(name, tuples);
40 cancellationToken.checkCancelled();
41 }
42 json.add("builtin::count", getCountJson(model));
43 return json;
44 }
45
46 private static JsonArray getTuplesJson(ReasoningAdapter adapter, Concreteness concreteness,
47 PartialRelation partialSymbol) {
48 var interpretation = adapter.getPartialInterpretation(concreteness, partialSymbol);
49 var cursor = interpretation.getAll();
50 return getTuplesJson(cursor);
51 }
52
53 private static JsonArray getTuplesJson(Cursor<Tuple, ?> cursor) {
54 var map = new TreeMap<Tuple, Object>();
55 while (cursor.move()) {
56 map.put(cursor.getKey(), cursor.getValue());
57 }
58 var tuples = new JsonArray();
59 for (var entry : map.entrySet()) {
60 tuples.add(toArray(entry.getKey(), entry.getValue()));
61 }
62 return tuples;
63 }
64
65 private static JsonArray toArray(Tuple tuple, Object value) {
66 int arity = tuple.getSize();
67 var json = new JsonArray(arity + 1);
68 for (int i = 0; i < arity; i++) {
69 json.add(tuple.get(i));
70 }
71 json.add(value.toString());
72 return json;
73 }
74
75 private static JsonArray getCountJson(Model model) {
76 var interpretation = model.getInterpretation(MultiObjectTranslator.COUNT_STORAGE);
77 var cursor = interpretation.getAll();
78 return getTuplesJson(cursor);
79
80 }
81}
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..331ef84b
--- /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 public 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..512c2778
--- /dev/null
+++ b/subprojects/language-web/src/main/java/tools/refinery/language/web/semantics/SemanticsWorker.java
@@ -0,0 +1,113 @@
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.inject.Inject;
9import org.eclipse.emf.common.util.Diagnostic;
10import org.eclipse.emf.ecore.EObject;
11import org.eclipse.xtext.service.OperationCanceledManager;
12import org.eclipse.xtext.util.CancelIndicator;
13import org.eclipse.xtext.validation.CheckType;
14import org.eclipse.xtext.validation.FeatureBasedDiagnostic;
15import org.eclipse.xtext.validation.IDiagnosticConverter;
16import org.eclipse.xtext.validation.Issue;
17import org.eclipse.xtext.web.server.validation.ValidationResult;
18import tools.refinery.language.model.problem.Problem;
19import tools.refinery.language.semantics.metadata.MetadataCreator;
20import tools.refinery.language.semantics.model.ModelInitializer;
21import tools.refinery.language.semantics.model.TracedException;
22import tools.refinery.store.dse.propagation.PropagationAdapter;
23import tools.refinery.store.model.ModelStore;
24import tools.refinery.store.query.viatra.ViatraModelQueryAdapter;
25import tools.refinery.store.reasoning.ReasoningAdapter;
26import tools.refinery.store.reasoning.ReasoningStoreAdapter;
27import tools.refinery.store.reasoning.literal.Concreteness;
28import tools.refinery.store.reasoning.translator.TranslationException;
29import tools.refinery.store.util.CancellationToken;
30
31import java.util.ArrayList;
32import java.util.concurrent.Callable;
33
34class SemanticsWorker implements Callable<SemanticsResult> {
35 private static final String DIAGNOSTIC_ID = "tools.refinery.language.semantics.SemanticError";
36
37 @Inject
38 private PartialInterpretation2Json partialInterpretation2Json;
39
40 @Inject
41 private OperationCanceledManager operationCanceledManager;
42
43 @Inject
44 private IDiagnosticConverter diagnosticConverter;
45
46 @Inject
47 private ModelInitializer initializer;
48
49 @Inject
50 private MetadataCreator metadataCreator;
51
52 private Problem problem;
53
54 private CancellationToken cancellationToken;
55
56 public void setProblem(Problem problem, CancelIndicator parentIndicator) {
57 this.problem = problem;
58 cancellationToken = () -> {
59 if (Thread.interrupted() || parentIndicator.isCanceled()) {
60 operationCanceledManager.throwOperationCanceledException();
61 }
62 };
63 }
64
65 @Override
66 public SemanticsResult call() {
67 var builder = ModelStore.builder()
68 .cancellationToken(cancellationToken)
69 .with(ViatraModelQueryAdapter.builder())
70 .with(PropagationAdapter.builder())
71 .with(ReasoningAdapter.builder()
72 .requiredInterpretations(Concreteness.PARTIAL));
73 cancellationToken.checkCancelled();
74 try {
75 var modelSeed = initializer.createModel(problem, builder);
76 cancellationToken.checkCancelled();
77 metadataCreator.setInitializer(initializer);
78 cancellationToken.checkCancelled();
79 var nodesMetadata = metadataCreator.getNodesMetadata();
80 cancellationToken.checkCancelled();
81 var relationsMetadata = metadataCreator.getRelationsMetadata();
82 cancellationToken.checkCancelled();
83 var store = builder.build();
84 cancellationToken.checkCancelled();
85 var model = store.getAdapter(ReasoningStoreAdapter.class).createInitialModel(modelSeed);
86 cancellationToken.checkCancelled();
87 var partialInterpretation = partialInterpretation2Json.getPartialInterpretation(initializer, model,
88 Concreteness.PARTIAL, cancellationToken);
89
90 return new SemanticsSuccessResult(nodesMetadata, relationsMetadata, partialInterpretation);
91 } catch (TracedException e) {
92 return getTracedErrorResult(e.getSourceElement(), e.getMessage());
93 } catch (TranslationException e) {
94 var sourceElement = initializer.getInverseTrace(e.getPartialSymbol());
95 return getTracedErrorResult(sourceElement, e.getMessage());
96 }
97 }
98
99 private SemanticsResult getTracedErrorResult(EObject sourceElement, String message) {
100 if (sourceElement == null || !problem.eResource().equals(sourceElement.eResource())) {
101 return new SemanticsInternalErrorResult(message);
102 }
103 var diagnostic = new FeatureBasedDiagnostic(Diagnostic.ERROR, message, sourceElement, null, 0,
104 CheckType.EXPENSIVE, DIAGNOSTIC_ID);
105 var xtextIssues = new ArrayList<Issue>();
106 diagnosticConverter.convertValidatorDiagnostic(diagnostic, xtextIssues::add);
107 var issues = xtextIssues.stream()
108 .map(issue -> new ValidationResult.Issue(issue.getMessage(), "error", issue.getLineNumber(),
109 issue.getColumn(), issue.getOffset(), issue.getLength()))
110 .toList();
111 return new SemanticsIssuesResult(issues);
112 }
113}
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..ff8f4943
--- /dev/null
+++ b/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/server/ThreadPoolExecutorServiceProvider.java
@@ -0,0 +1,158 @@
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 org.slf4j.Logger;
13import org.slf4j.LoggerFactory;
14import tools.refinery.language.web.generator.ModelGenerationService;
15import tools.refinery.language.web.semantics.SemanticsService;
16
17import java.lang.invoke.MethodHandle;
18import java.lang.invoke.MethodHandles;
19import java.util.Collections;
20import java.util.HashMap;
21import java.util.Map;
22import java.util.Optional;
23import java.util.concurrent.ExecutorService;
24import java.util.concurrent.Executors;
25import java.util.concurrent.ScheduledExecutorService;
26import java.util.concurrent.ThreadFactory;
27import java.util.concurrent.atomic.AtomicInteger;
28
29@Singleton
30public class ThreadPoolExecutorServiceProvider extends ExecutorServiceProvider {
31 private static final Logger LOG = LoggerFactory.getLogger(ThreadPoolExecutorServiceProvider.class);
32 private static final String DOCUMENT_LOCK_EXECUTOR;
33 private static final AtomicInteger POOL_ID = new AtomicInteger(1);
34
35 private final Map<String, ScheduledExecutorService> scheduledInstanceCache =
36 Collections.synchronizedMap(new HashMap<>());
37 private final int executorThreadCount;
38 private final int lockExecutorThreadCount;
39 private final int semanticsExecutorThreadCount;
40 private final int generatorExecutorThreadCount;
41
42 static {
43 var lookup = MethodHandles.lookup();
44 MethodHandle getter;
45 try {
46 var privateLookup = MethodHandles.privateLookupIn(XtextWebDocumentAccess.class, lookup);
47 getter = privateLookup.findStaticGetter(XtextWebDocumentAccess.class, "DOCUMENT_LOCK_EXECUTOR",
48 String.class);
49 } catch (IllegalAccessException | NoSuchFieldException e) {
50 throw new IllegalStateException("Failed to find getter", e);
51 }
52 try {
53 DOCUMENT_LOCK_EXECUTOR = (String) getter.invokeExact();
54 } catch (Error e) {
55 // Rethrow JVM errors.
56 throw e;
57 } catch (Throwable e) {
58 throw new IllegalStateException("Failed to get DOCUMENT_LOCK_EXECUTOR", e);
59 }
60 }
61
62 public ThreadPoolExecutorServiceProvider() {
63 executorThreadCount = getCount("REFINERY_XTEXT_THREAD_COUNT").orElse(0);
64 lockExecutorThreadCount = getCount("REFINERY_XTEXT_LOCKING_THREAD_COUNT").orElse(executorThreadCount);
65 int semanticsCount = getCount("REFINERY_XTEXT_SEMANTICS_THREAD_COUNT").orElse(0);
66 if (semanticsCount == 0 || executorThreadCount == 0) {
67 semanticsExecutorThreadCount = 0;
68 } else {
69 semanticsExecutorThreadCount = Math.max(semanticsCount, executorThreadCount);
70 }
71 if (semanticsExecutorThreadCount != semanticsCount) {
72 LOG.warn("Setting REFINERY_XTEXT_SEMANTICS_THREAD_COUNT to {} to avoid deadlock. This value must be " +
73 "either 0 or at least as large as REFINERY_XTEXT_THREAD_COUNT to avoid lock contention.",
74 semanticsExecutorThreadCount);
75 }
76 generatorExecutorThreadCount = getCount("REFINERY_MODEL_GENERATION_THREAD_COUNT").orElse(executorThreadCount);
77 }
78
79 private static Optional<Integer> getCount(String name) {
80 return Optional.ofNullable(System.getenv(name)).map(Integer::parseUnsignedInt);
81 }
82
83 public ScheduledExecutorService getScheduled(String key) {
84 return scheduledInstanceCache.computeIfAbsent(key, this::createScheduledInstance);
85 }
86
87 @Override
88 protected ExecutorService createInstance(String key) {
89 String name = "xtext-" + POOL_ID.getAndIncrement();
90 if (key != null) {
91 name = name + "-" + key;
92 }
93 var threadFactory = new Factory(name, 5);
94 int size = getSize(key);
95 if (size == 0) {
96 return Executors.newCachedThreadPool(threadFactory);
97 }
98 return Executors.newFixedThreadPool(size, threadFactory);
99 }
100
101 protected ScheduledExecutorService createScheduledInstance(String key) {
102 String name = "xtext-scheduled-" + POOL_ID.getAndIncrement();
103 if (key != null) {
104 name = name + "-" + key;
105 }
106 var threadFactory = new Factory(name, 5);
107 return Executors.newScheduledThreadPool(1, threadFactory);
108 }
109
110 private int getSize(String key) {
111 if (SemanticsService.SEMANTICS_EXECUTOR.equals(key)) {
112 return semanticsExecutorThreadCount;
113 } else if (ModelGenerationService.MODEL_GENERATION_EXECUTOR.equals(key)) {
114 return generatorExecutorThreadCount;
115 } else if (DOCUMENT_LOCK_EXECUTOR.equals(key)) {
116 return lockExecutorThreadCount;
117 } else {
118 return executorThreadCount;
119 }
120 }
121
122 @Override
123 public void dispose() {
124 super.dispose();
125 synchronized (scheduledInstanceCache) {
126 for (var instance : scheduledInstanceCache.values()) {
127 instance.shutdown();
128 }
129 scheduledInstanceCache.clear();
130 }
131 }
132
133 private static class Factory implements ThreadFactory {
134 // We have to explicitly store the {@link ThreadGroup} to create a {@link ThreadFactory}.
135 @SuppressWarnings("squid:S3014")
136 private final ThreadGroup threadGroup = Thread.currentThread().getThreadGroup();
137 private final AtomicInteger threadId = new AtomicInteger(1);
138 private final String namePrefix;
139 private final int priority;
140
141 public Factory(String name, int priority) {
142 namePrefix = name + "-thread-";
143 this.priority = priority;
144 }
145
146 @Override
147 public Thread newThread(@NotNull Runnable runnable) {
148 var thread = new Thread(threadGroup, runnable, namePrefix + threadId.getAndIncrement());
149 if (thread.isDaemon()) {
150 thread.setDaemon(false);
151 }
152 if (thread.getPriority() != priority) {
153 thread.setPriority(priority);
154 }
155 return thread;
156 }
157 }
158}
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..a3792bac 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.dispose();
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..e1d00d8f 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,34 @@
5 */ 5 */
6package tools.refinery.language.web.xtext.server.push; 6package tools.refinery.language.web.xtext.server.push;
7 7
8import com.google.common.base.Optional;
9import com.google.inject.Inject;
8import org.eclipse.xtext.web.server.IServiceContext; 10import org.eclipse.xtext.web.server.IServiceContext;
11import org.eclipse.xtext.web.server.InvalidRequestException;
9import org.eclipse.xtext.web.server.XtextServiceDispatcher; 12import org.eclipse.xtext.web.server.XtextServiceDispatcher;
13import org.eclipse.xtext.web.server.model.PrecomputedServiceRegistry;
10import org.eclipse.xtext.web.server.model.XtextWebDocument; 14import org.eclipse.xtext.web.server.model.XtextWebDocument;
11 15
12import com.google.inject.Singleton; 16import com.google.inject.Singleton;
13 17
18import tools.refinery.language.web.generator.ModelGenerationService;
19import tools.refinery.language.web.semantics.SemanticsService;
14import tools.refinery.language.web.xtext.server.SubscribingServiceContext; 20import tools.refinery.language.web.xtext.server.SubscribingServiceContext;
15 21
16@Singleton 22@Singleton
17public class PushServiceDispatcher extends XtextServiceDispatcher { 23public class PushServiceDispatcher extends XtextServiceDispatcher {
24 @Inject
25 private SemanticsService semanticsService;
26
27 @Inject
28 private ModelGenerationService modelGenerationService;
29
30 @Override
31 @Inject
32 protected void registerPreComputedServices(PrecomputedServiceRegistry registry) {
33 super.registerPreComputedServices(registry);
34 registry.addPrecomputedService(semanticsService);
35 }
18 36
19 @Override 37 @Override
20 protected XtextWebDocument getFullTextDocument(String fullText, String resourceId, IServiceContext context) { 38 protected XtextWebDocument getFullTextDocument(String fullText, String resourceId, IServiceContext context) {
@@ -25,4 +43,39 @@ public class PushServiceDispatcher extends XtextServiceDispatcher {
25 } 43 }
26 return document; 44 return document;
27 } 45 }
46
47 @Override
48 protected ServiceDescriptor createServiceDescriptor(String serviceType, IServiceContext context) {
49 if (ModelGenerationService.SERVICE_NAME.equals(serviceType)) {
50 return getModelGenerationService(context);
51 }
52 return super.createServiceDescriptor(serviceType, context);
53 }
54
55 protected ServiceDescriptor getModelGenerationService(IServiceContext context) throws InvalidRequestException {
56 var document = (PushWebDocumentAccess) getDocumentAccess(context);
57 // Using legacy Guava methods because of the Xtext dependency.
58 @SuppressWarnings({"Guava", "squid:S4738"})
59 boolean start = getBoolean(context, "start", Optional.of(false));
60 @SuppressWarnings({"Guava", "squid:S4738"})
61 boolean cancel = getBoolean(context, "cancel", Optional.of(false));
62 if (!start && !cancel) {
63 throw new InvalidRequestException("Either start of cancel must be specified");
64 }
65 @SuppressWarnings({"squid:S4738"})
66 int randomSeed = start ? getInt(context, "randomSeed", Optional.absent()) : 0;
67 var descriptor = new ServiceDescriptor();
68 descriptor.setService(() -> {
69 try {
70 if (start) {
71 return modelGenerationService.generateModel(document, randomSeed);
72 } else {
73 return modelGenerationService.cancelModelGeneration(document);
74 }
75 } catch (RuntimeException e) {
76 return handleError(descriptor, e);
77 }
78 });
79 return descriptor;
80 }
28} 81}
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..ca97147a 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,60 +13,59 @@ 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 16import tools.refinery.language.web.generator.ModelGenerationManager;
21import com.google.common.collect.ImmutableList;
22
23import tools.refinery.language.web.xtext.server.ResponseHandlerException; 17import tools.refinery.language.web.xtext.server.ResponseHandlerException;
24 18
19import java.util.ArrayList;
20import java.util.List;
21
25public class PushWebDocument extends XtextWebDocument { 22public class PushWebDocument extends XtextWebDocument {
26 private static final Logger LOG = LoggerFactory.getLogger(PushWebDocument.class); 23 private static final Logger LOG = LoggerFactory.getLogger(PushWebDocument.class);
27 24
28 private final List<PrecomputationListener> precomputationListeners = new ArrayList<>(); 25 private final List<PrecomputationListener> precomputationListeners = new ArrayList<>();
29 26
30 private final Map<Class<?>, IServiceResult> precomputedServices = new HashMap<>(); 27 private final ModelGenerationManager modelGenerationManager = new ModelGenerationManager();
28
29 private final DocumentSynchronizer synchronizer;
31 30
32 public PushWebDocument(String resourceId, DocumentSynchronizer synchronizer) { 31 public PushWebDocument(String resourceId, DocumentSynchronizer synchronizer) {
33 super(resourceId, synchronizer); 32 super(resourceId, synchronizer);
34 if (resourceId == null) { 33 this.synchronizer = synchronizer;
35 throw new IllegalArgumentException("resourceId must not be null");
36 }
37 } 34 }
38 35
39 public boolean addPrecomputationListener(PrecomputationListener listener) { 36 public ModelGenerationManager getModelGenerationManager() {
37 return modelGenerationManager;
38 }
39
40 public void addPrecomputationListener(PrecomputationListener listener) {
40 synchronized (precomputationListeners) { 41 synchronized (precomputationListeners) {
41 if (precomputationListeners.contains(listener)) { 42 if (precomputationListeners.contains(listener)) {
42 return false; 43 return;
43 } 44 }
44 precomputationListeners.add(listener); 45 precomputationListeners.add(listener);
45 listener.onSubscribeToPrecomputationEvents(getResourceId(), this); 46 listener.onSubscribeToPrecomputationEvents(getResourceId(), this);
46 return true;
47 } 47 }
48 } 48 }
49 49
50 public boolean removePrecomputationListener(PrecomputationListener listener) { 50 public void removePrecomputationListener(PrecomputationListener listener) {
51 synchronized (precomputationListeners) { 51 synchronized (precomputationListeners) {
52 return precomputationListeners.remove(listener); 52 precomputationListeners.remove(listener);
53 } 53 }
54 } 54 }
55 55
56 public <T extends IServiceResult> void precomputeServiceResult(AbstractCachedService<T> service, String serviceName, 56 public <T extends IServiceResult> void precomputeServiceResult(AbstractCachedService<T> service, String serviceName,
57 CancelIndicator cancelIndicator, boolean logCacheMiss) { 57 CancelIndicator cancelIndicator, boolean logCacheMiss) {
58 var result = getCachedServiceResult(service, cancelIndicator, logCacheMiss); 58 var result = getCachedServiceResult(service, cancelIndicator, logCacheMiss);
59 if (result == null) { 59 if (result != null) {
60 LOG.error("{} service returned null result", serviceName); 60 notifyPrecomputationListeners(serviceName, result);
61 return;
62 } 61 }
63 var serviceClass = service.getClass();
64 var previousResult = precomputedServices.get(serviceClass);
65 if (previousResult != null && previousResult.equals(result)) {
66 return;
67 }
68 precomputedServices.put(serviceClass, result);
69 notifyPrecomputationListeners(serviceName, result);
70 } 62 }
71 63
72 private <T extends IServiceResult> void notifyPrecomputationListeners(String serviceName, T result) { 64 public <T extends IServiceResult> void notifyPrecomputationListeners(String serviceName, T result) {
73 var resourceId = getResourceId(); 65 var resourceId = getResourceId();
66 if (resourceId == null) {
67 return;
68 }
74 var stateId = getStateId(); 69 var stateId = getStateId();
75 List<PrecomputationListener> copyOfListeners; 70 List<PrecomputationListener> copyOfListeners;
76 synchronized (precomputationListeners) { 71 synchronized (precomputationListeners) {
@@ -91,4 +86,13 @@ public class PushWebDocument extends XtextWebDocument {
91 } 86 }
92 } 87 }
93 } 88 }
89
90 public void cancelModelGeneration() {
91 modelGenerationManager.cancel();
92 }
93
94 public void dispose() {
95 synchronizer.setCanceled(true);
96 modelGenerationManager.dispose();
97 }
94} 98}
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..1e68b244 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,13 @@ 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 }
77
78 public void cancelModelGeneration() {
79 pushDocument.cancelModelGeneration();
80 }
73} 81}
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