diff options
Diffstat (limited to 'subprojects/language-web/src')
36 files changed, 1473 insertions, 94 deletions
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 | */ |
10 | package tools.refinery.language.web; | 10 | package tools.refinery.language.web; |
11 | 11 | ||
12 | import org.eclipse.xtext.ide.ExecutorServiceProvider; | ||
12 | import org.eclipse.xtext.web.server.XtextServiceDispatcher; | 13 | import org.eclipse.xtext.web.server.XtextServiceDispatcher; |
13 | import org.eclipse.xtext.web.server.model.IWebDocumentProvider; | 14 | import org.eclipse.xtext.web.server.model.IWebDocumentProvider; |
14 | import org.eclipse.xtext.web.server.model.XtextWebDocumentAccess; | 15 | import org.eclipse.xtext.web.server.model.XtextWebDocumentAccess; |
15 | import org.eclipse.xtext.web.server.occurrences.OccurrencesService; | 16 | import org.eclipse.xtext.web.server.occurrences.OccurrencesService; |
16 | import tools.refinery.language.web.occurrences.ProblemOccurrencesService; | 17 | import tools.refinery.language.web.occurrences.ProblemOccurrencesService; |
18 | import tools.refinery.language.web.xtext.server.ThreadPoolExecutorServiceProvider; | ||
17 | import tools.refinery.language.web.xtext.server.push.PushServiceDispatcher; | 19 | import tools.refinery.language.web.xtext.server.push.PushServiceDispatcher; |
18 | import tools.refinery.language.web.xtext.server.push.PushWebDocumentAccess; | 20 | import tools.refinery.language.web.xtext.server.push.PushWebDocumentAccess; |
19 | import tools.refinery.language.web.xtext.server.push.PushWebDocumentProvider; | 21 | import 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; | |||
10 | import jakarta.servlet.ServletException; | 10 | import jakarta.servlet.ServletException; |
11 | import tools.refinery.language.web.xtext.servlet.XtextWebSocketServlet; | 11 | import tools.refinery.language.web.xtext.servlet.XtextWebSocketServlet; |
12 | 12 | ||
13 | public class ProblemWebSocketServlet extends XtextWebSocketServlet { | 13 | import java.io.Serial; |
14 | 14 | ||
15 | public 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; | |||
33 | import java.util.Set; | 33 | import java.util.Set; |
34 | 34 | ||
35 | public class ServerLauncher { | 35 | public 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 | */ | ||
6 | package tools.refinery.language.web.generator; | ||
7 | |||
8 | import org.eclipse.xtext.web.server.IServiceResult; | ||
9 | |||
10 | public 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 | */ | ||
6 | package tools.refinery.language.web.generator; | ||
7 | |||
8 | import java.util.UUID; | ||
9 | |||
10 | public 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 | */ | ||
6 | package tools.refinery.language.web.generator; | ||
7 | |||
8 | import org.eclipse.xtext.util.CancelIndicator; | ||
9 | |||
10 | public 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 | */ | ||
6 | package tools.refinery.language.web.generator; | ||
7 | |||
8 | import org.eclipse.xtext.web.server.IServiceResult; | ||
9 | |||
10 | import java.util.UUID; | ||
11 | |||
12 | public 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 | */ | ||
6 | package tools.refinery.language.web.generator; | ||
7 | |||
8 | import com.google.inject.Inject; | ||
9 | import com.google.inject.Provider; | ||
10 | import com.google.inject.Singleton; | ||
11 | import org.eclipse.xtext.service.OperationCanceledManager; | ||
12 | import org.eclipse.xtext.util.CancelIndicator; | ||
13 | import org.eclipse.xtext.util.concurrent.CancelableUnitOfWork; | ||
14 | import org.eclipse.xtext.web.server.model.IXtextWebDocument; | ||
15 | import tools.refinery.language.web.semantics.SemanticsService; | ||
16 | import tools.refinery.language.web.xtext.server.push.PushWebDocument; | ||
17 | import tools.refinery.language.web.xtext.server.push.PushWebDocumentAccess; | ||
18 | |||
19 | @Singleton | ||
20 | public 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 | */ | ||
6 | package tools.refinery.language.web.generator; | ||
7 | |||
8 | import org.eclipse.xtext.web.server.IServiceResult; | ||
9 | |||
10 | import java.util.UUID; | ||
11 | |||
12 | public 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 | */ | ||
6 | package tools.refinery.language.web.generator; | ||
7 | |||
8 | import java.util.UUID; | ||
9 | |||
10 | public 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 | */ | ||
6 | package tools.refinery.language.web.generator; | ||
7 | |||
8 | import com.google.gson.JsonObject; | ||
9 | import tools.refinery.language.semantics.metadata.NodeMetadata; | ||
10 | import tools.refinery.language.semantics.metadata.RelationMetadata; | ||
11 | |||
12 | import java.util.List; | ||
13 | import java.util.UUID; | ||
14 | |||
15 | public 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 | */ | ||
6 | package tools.refinery.language.web.generator; | ||
7 | |||
8 | import com.google.inject.Inject; | ||
9 | import com.google.inject.Provider; | ||
10 | import org.eclipse.emf.common.util.URI; | ||
11 | import org.eclipse.xtext.diagnostics.Severity; | ||
12 | import org.eclipse.xtext.resource.IResourceFactory; | ||
13 | import org.eclipse.xtext.resource.XtextResourceSet; | ||
14 | import org.eclipse.xtext.service.OperationCanceledManager; | ||
15 | import org.eclipse.xtext.util.LazyStringInputStream; | ||
16 | import org.eclipse.xtext.validation.CheckMode; | ||
17 | import org.eclipse.xtext.validation.IResourceValidator; | ||
18 | import org.slf4j.Logger; | ||
19 | import org.slf4j.LoggerFactory; | ||
20 | import tools.refinery.language.model.problem.Problem; | ||
21 | import tools.refinery.language.semantics.metadata.MetadataCreator; | ||
22 | import tools.refinery.language.semantics.model.ModelInitializer; | ||
23 | import tools.refinery.language.web.semantics.PartialInterpretation2Json; | ||
24 | import tools.refinery.language.web.xtext.server.ThreadPoolExecutorServiceProvider; | ||
25 | import tools.refinery.language.web.xtext.server.push.PushWebDocument; | ||
26 | import tools.refinery.store.dse.propagation.PropagationAdapter; | ||
27 | import tools.refinery.store.dse.strategy.BestFirstStoreManager; | ||
28 | import tools.refinery.store.dse.transition.DesignSpaceExplorationAdapter; | ||
29 | import tools.refinery.store.model.ModelStore; | ||
30 | import tools.refinery.store.query.viatra.ViatraModelQueryAdapter; | ||
31 | import tools.refinery.store.reasoning.ReasoningAdapter; | ||
32 | import tools.refinery.store.reasoning.ReasoningStoreAdapter; | ||
33 | import tools.refinery.store.reasoning.literal.Concreteness; | ||
34 | import tools.refinery.store.statecoding.StateCoderAdapter; | ||
35 | import tools.refinery.store.util.CancellationToken; | ||
36 | |||
37 | import java.io.IOException; | ||
38 | import java.util.Map; | ||
39 | import java.util.UUID; | ||
40 | import java.util.concurrent.*; | ||
41 | |||
42 | public 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 | */ | ||
6 | package tools.refinery.language.web.semantics; | ||
7 | |||
8 | import com.google.gson.JsonArray; | ||
9 | import com.google.gson.JsonObject; | ||
10 | import com.google.inject.Inject; | ||
11 | import com.google.inject.Singleton; | ||
12 | import tools.refinery.language.semantics.model.ModelInitializer; | ||
13 | import tools.refinery.language.semantics.model.SemanticsUtils; | ||
14 | import tools.refinery.store.map.Cursor; | ||
15 | import tools.refinery.store.model.Model; | ||
16 | import tools.refinery.store.reasoning.ReasoningAdapter; | ||
17 | import tools.refinery.store.reasoning.literal.Concreteness; | ||
18 | import tools.refinery.store.reasoning.representation.PartialRelation; | ||
19 | import tools.refinery.store.reasoning.translator.multiobject.MultiObjectTranslator; | ||
20 | import tools.refinery.store.tuple.Tuple; | ||
21 | import tools.refinery.store.util.CancellationToken; | ||
22 | |||
23 | import java.util.TreeMap; | ||
24 | |||
25 | @Singleton | ||
26 | public 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 | */ | ||
6 | package tools.refinery.language.web.semantics; | ||
7 | |||
8 | public 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 | */ | ||
6 | package tools.refinery.language.web.semantics; | ||
7 | |||
8 | import org.eclipse.xtext.web.server.validation.ValidationResult; | ||
9 | |||
10 | import java.util.List; | ||
11 | |||
12 | public 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 | */ | ||
6 | package tools.refinery.language.web.semantics; | ||
7 | |||
8 | import org.eclipse.xtext.web.server.IServiceResult; | ||
9 | |||
10 | public 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 | */ | ||
6 | package tools.refinery.language.web.semantics; | ||
7 | |||
8 | import com.google.gson.JsonObject; | ||
9 | import com.google.inject.Inject; | ||
10 | import com.google.inject.Provider; | ||
11 | import com.google.inject.Singleton; | ||
12 | import org.eclipse.xtext.ide.ExecutorServiceProvider; | ||
13 | import org.eclipse.xtext.service.OperationCanceledManager; | ||
14 | import org.eclipse.xtext.util.CancelIndicator; | ||
15 | import org.eclipse.xtext.web.server.model.AbstractCachedService; | ||
16 | import org.eclipse.xtext.web.server.model.IXtextWebDocument; | ||
17 | import org.eclipse.xtext.web.server.validation.ValidationService; | ||
18 | import org.jetbrains.annotations.Nullable; | ||
19 | import org.slf4j.Logger; | ||
20 | import org.slf4j.LoggerFactory; | ||
21 | import tools.refinery.language.model.problem.Problem; | ||
22 | import tools.refinery.language.web.xtext.server.push.PushWebDocument; | ||
23 | |||
24 | import java.util.List; | ||
25 | import java.util.Optional; | ||
26 | import java.util.concurrent.*; | ||
27 | import java.util.concurrent.atomic.AtomicBoolean; | ||
28 | |||
29 | @Singleton | ||
30 | public 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 | */ | ||
6 | package tools.refinery.language.web.semantics; | ||
7 | |||
8 | import com.google.gson.JsonObject; | ||
9 | import tools.refinery.language.semantics.metadata.NodeMetadata; | ||
10 | import tools.refinery.language.semantics.metadata.RelationMetadata; | ||
11 | |||
12 | import java.util.List; | ||
13 | |||
14 | public 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 | */ | ||
6 | package tools.refinery.language.web.semantics; | ||
7 | |||
8 | import com.google.inject.Inject; | ||
9 | import org.eclipse.emf.common.util.Diagnostic; | ||
10 | import org.eclipse.emf.ecore.EObject; | ||
11 | import org.eclipse.xtext.service.OperationCanceledManager; | ||
12 | import org.eclipse.xtext.util.CancelIndicator; | ||
13 | import org.eclipse.xtext.validation.CheckType; | ||
14 | import org.eclipse.xtext.validation.FeatureBasedDiagnostic; | ||
15 | import org.eclipse.xtext.validation.IDiagnosticConverter; | ||
16 | import org.eclipse.xtext.validation.Issue; | ||
17 | import org.eclipse.xtext.web.server.validation.ValidationResult; | ||
18 | import tools.refinery.language.model.problem.Problem; | ||
19 | import tools.refinery.language.semantics.metadata.MetadataCreator; | ||
20 | import tools.refinery.language.semantics.model.ModelInitializer; | ||
21 | import tools.refinery.language.semantics.model.TracedException; | ||
22 | import tools.refinery.store.dse.propagation.PropagationAdapter; | ||
23 | import tools.refinery.store.model.ModelStore; | ||
24 | import tools.refinery.store.query.viatra.ViatraModelQueryAdapter; | ||
25 | import tools.refinery.store.reasoning.ReasoningAdapter; | ||
26 | import tools.refinery.store.reasoning.ReasoningStoreAdapter; | ||
27 | import tools.refinery.store.reasoning.literal.Concreteness; | ||
28 | import tools.refinery.store.reasoning.translator.TranslationException; | ||
29 | import tools.refinery.store.util.CancellationToken; | ||
30 | |||
31 | import java.util.ArrayList; | ||
32 | import java.util.concurrent.Callable; | ||
33 | |||
34 | class 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 | */ | ||
6 | package tools.refinery.language.web.xtext.server; | ||
7 | |||
8 | import com.google.inject.Singleton; | ||
9 | import org.eclipse.xtext.ide.ExecutorServiceProvider; | ||
10 | import org.eclipse.xtext.web.server.model.XtextWebDocumentAccess; | ||
11 | import org.jetbrains.annotations.NotNull; | ||
12 | import org.slf4j.Logger; | ||
13 | import org.slf4j.LoggerFactory; | ||
14 | import tools.refinery.language.web.generator.ModelGenerationService; | ||
15 | import tools.refinery.language.web.semantics.SemanticsService; | ||
16 | |||
17 | import java.lang.invoke.MethodHandle; | ||
18 | import java.lang.invoke.MethodHandles; | ||
19 | import java.util.Collections; | ||
20 | import java.util.HashMap; | ||
21 | import java.util.Map; | ||
22 | import java.util.Optional; | ||
23 | import java.util.concurrent.ExecutorService; | ||
24 | import java.util.concurrent.Executors; | ||
25 | import java.util.concurrent.ScheduledExecutorService; | ||
26 | import java.util.concurrent.ThreadFactory; | ||
27 | import java.util.concurrent.atomic.AtomicInteger; | ||
28 | |||
29 | @Singleton | ||
30 | public 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 | */ |
6 | package tools.refinery.language.web.xtext.server.message; | 6 | package tools.refinery.language.web.xtext.server.message; |
7 | 7 | ||
8 | import java.util.Objects; | 8 | import com.google.gson.annotations.SerializedName; |
9 | |||
10 | import org.eclipse.xtext.web.server.IServiceResult; | 9 | import org.eclipse.xtext.web.server.IServiceResult; |
11 | import org.eclipse.xtext.web.server.IUnwrappableServiceResult; | 10 | import org.eclipse.xtext.web.server.IUnwrappableServiceResult; |
12 | 11 | ||
13 | import com.google.gson.annotations.SerializedName; | 12 | import java.util.Objects; |
14 | 13 | ||
15 | public final class XtextWebOkResponse implements XtextWebResponse { | 14 | public 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 | */ |
6 | package tools.refinery.language.web.xtext.server.message; | 6 | package tools.refinery.language.web.xtext.server.message; |
7 | 7 | ||
8 | import com.google.gson.annotations.SerializedName; | ||
9 | |||
8 | import java.util.Map; | 10 | import java.util.Map; |
9 | import java.util.Objects; | 11 | import java.util.Objects; |
10 | 12 | ||
11 | import com.google.gson.annotations.SerializedName; | ||
12 | |||
13 | public class XtextWebRequest { | 13 | public 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 | */ |
6 | package tools.refinery.language.web.xtext.server.message; | 6 | package tools.refinery.language.web.xtext.server.message; |
7 | 7 | ||
8 | public sealed interface XtextWebResponse permits XtextWebOkResponse,XtextWebErrorResponse,XtextWebPushMessage { | 8 | public 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 | */ |
6 | package tools.refinery.language.web.xtext.server.push; | 6 | package tools.refinery.language.web.xtext.server.push; |
7 | 7 | ||
8 | import com.google.common.base.Optional; | ||
9 | import com.google.inject.Inject; | ||
8 | import org.eclipse.xtext.web.server.IServiceContext; | 10 | import org.eclipse.xtext.web.server.IServiceContext; |
11 | import org.eclipse.xtext.web.server.InvalidRequestException; | ||
9 | import org.eclipse.xtext.web.server.XtextServiceDispatcher; | 12 | import org.eclipse.xtext.web.server.XtextServiceDispatcher; |
13 | import org.eclipse.xtext.web.server.model.PrecomputedServiceRegistry; | ||
10 | import org.eclipse.xtext.web.server.model.XtextWebDocument; | 14 | import org.eclipse.xtext.web.server.model.XtextWebDocument; |
11 | 15 | ||
12 | import com.google.inject.Singleton; | 16 | import com.google.inject.Singleton; |
13 | 17 | ||
18 | import tools.refinery.language.web.generator.ModelGenerationService; | ||
19 | import tools.refinery.language.web.semantics.SemanticsService; | ||
14 | import tools.refinery.language.web.xtext.server.SubscribingServiceContext; | 20 | import tools.refinery.language.web.xtext.server.SubscribingServiceContext; |
15 | 21 | ||
16 | @Singleton | 22 | @Singleton |
17 | public class PushServiceDispatcher extends XtextServiceDispatcher { | 23 | public 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 | */ |
6 | package tools.refinery.language.web.xtext.server.push; | 6 | package tools.refinery.language.web.xtext.server.push; |
7 | 7 | ||
8 | import java.util.ArrayList; | 8 | import com.google.common.collect.ImmutableList; |
9 | import java.util.HashMap; | ||
10 | import java.util.List; | ||
11 | import java.util.Map; | ||
12 | |||
13 | import org.eclipse.xtext.util.CancelIndicator; | 9 | import org.eclipse.xtext.util.CancelIndicator; |
14 | import org.eclipse.xtext.web.server.IServiceResult; | 10 | import org.eclipse.xtext.web.server.IServiceResult; |
15 | import org.eclipse.xtext.web.server.model.AbstractCachedService; | 11 | import org.eclipse.xtext.web.server.model.AbstractCachedService; |
@@ -17,60 +13,59 @@ import org.eclipse.xtext.web.server.model.DocumentSynchronizer; | |||
17 | import org.eclipse.xtext.web.server.model.XtextWebDocument; | 13 | import org.eclipse.xtext.web.server.model.XtextWebDocument; |
18 | import org.slf4j.Logger; | 14 | import org.slf4j.Logger; |
19 | import org.slf4j.LoggerFactory; | 15 | import org.slf4j.LoggerFactory; |
20 | 16 | import tools.refinery.language.web.generator.ModelGenerationManager; | |
21 | import com.google.common.collect.ImmutableList; | ||
22 | |||
23 | import tools.refinery.language.web.xtext.server.ResponseHandlerException; | 17 | import tools.refinery.language.web.xtext.server.ResponseHandlerException; |
24 | 18 | ||
19 | import java.util.ArrayList; | ||
20 | import java.util.List; | ||
21 | |||
25 | public class PushWebDocument extends XtextWebDocument { | 22 | public 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; | |||
18 | import org.eclipse.xtext.web.server.validation.ValidationService; | 18 | import org.eclipse.xtext.web.server.validation.ValidationService; |
19 | 19 | ||
20 | import com.google.inject.Inject; | 20 | import com.google.inject.Inject; |
21 | import tools.refinery.language.web.semantics.SemanticsService; | ||
21 | 22 | ||
22 | public class PushWebDocumentAccess extends XtextWebDocumentAccess { | 23 | public 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 | */ | ||
24 | package tools.refinery.language.web.xtext.servlet; | ||
25 | |||
26 | import com.google.errorprone.annotations.CanIgnoreReturnValue; | ||
27 | import com.google.gson.Gson; | ||
28 | import com.google.gson.JsonElement; | ||
29 | import com.google.gson.JsonObject; | ||
30 | import com.google.gson.JsonParseException; | ||
31 | import com.google.gson.JsonPrimitive; | ||
32 | import com.google.gson.TypeAdapter; | ||
33 | import com.google.gson.TypeAdapterFactory; | ||
34 | import com.google.gson.reflect.TypeToken; | ||
35 | import com.google.gson.stream.JsonReader; | ||
36 | import com.google.gson.stream.JsonWriter; | ||
37 | import java.io.IOException; | ||
38 | import java.util.LinkedHashMap; | ||
39 | import java.util.Map; | ||
40 | |||
41 | /** | ||
42 | * Adapts values whose runtime type may differ from their declaration type. This | ||
43 | * is necessary when a field's type is not the same type that GSON should create | ||
44 | * when deserializing that field. For example, consider these types: | ||
45 | * <pre> {@code | ||
46 | * abstract class Shape { | ||
47 | * int x; | ||
48 | * int y; | ||
49 | * } | ||
50 | * class Circle extends Shape { | ||
51 | * int radius; | ||
52 | * } | ||
53 | * class Rectangle extends Shape { | ||
54 | * int width; | ||
55 | * int height; | ||
56 | * } | ||
57 | * class Diamond extends Shape { | ||
58 | * int width; | ||
59 | * int height; | ||
60 | * } | ||
61 | * class Drawing { | ||
62 | * Shape bottomShape; | ||
63 | * Shape topShape; | ||
64 | * } | ||
65 | * }</pre> | ||
66 | * <p>Without additional type information, the serialized JSON is ambiguous. Is | ||
67 | * the bottom shape in this drawing a rectangle or a diamond? <pre> {@code | ||
68 | * { | ||
69 | * "bottomShape": { | ||
70 | * "width": 10, | ||
71 | * "height": 5, | ||
72 | * "x": 0, | ||
73 | * "y": 0 | ||
74 | * }, | ||
75 | * "topShape": { | ||
76 | * "radius": 2, | ||
77 | * "x": 4, | ||
78 | * "y": 1 | ||
79 | * } | ||
80 | * }}</pre> | ||
81 | * This class addresses this problem by adding type information to the | ||
82 | * serialized JSON and honoring that type information when the JSON is | ||
83 | * deserialized: <pre> {@code | ||
84 | * { | ||
85 | * "bottomShape": { | ||
86 | * "type": "Diamond", | ||
87 | * "width": 10, | ||
88 | * "height": 5, | ||
89 | * "x": 0, | ||
90 | * "y": 0 | ||
91 | * }, | ||
92 | * "topShape": { | ||
93 | * "type": "Circle", | ||
94 | * "radius": 2, | ||
95 | * "x": 4, | ||
96 | * "y": 1 | ||
97 | * } | ||
98 | * }}</pre> | ||
99 | * Both the type field name ({@code "type"}) and the type labels ({@code | ||
100 | * "Rectangle"}) are configurable. | ||
101 | * | ||
102 | * <h2>Registering Types</h2> | ||
103 | * Create a {@code RuntimeTypeAdapterFactory} by passing the base type and type field | ||
104 | * name to the {@link #of} factory method. If you don't supply an explicit type | ||
105 | * field name, {@code "type"} will be used. <pre> {@code | ||
106 | * RuntimeTypeAdapterFactory<Shape> shapeAdapterFactory | ||
107 | * = RuntimeTypeAdapterFactory.of(Shape.class, "type"); | ||
108 | * }</pre> | ||
109 | * Next register all of your subtypes. Every subtype must be explicitly | ||
110 | * registered. This protects your application from injection attacks. If you | ||
111 | * don't supply an explicit type label, the type's simple name will be used. | ||
112 | * <pre> {@code | ||
113 | * shapeAdapterFactory.registerSubtype(Rectangle.class, "Rectangle"); | ||
114 | * shapeAdapterFactory.registerSubtype(Circle.class, "Circle"); | ||
115 | * shapeAdapterFactory.registerSubtype(Diamond.class, "Diamond"); | ||
116 | * }</pre> | ||
117 | * Finally, register the type adapter factory in your application's GSON builder: | ||
118 | * <pre> {@code | ||
119 | * Gson gson = new GsonBuilder() | ||
120 | * .registerTypeAdapterFactory(shapeAdapterFactory) | ||
121 | * .create(); | ||
122 | * }</pre> | ||
123 | * Like {@code GsonBuilder}, this API supports chaining: <pre> {@code | ||
124 | * RuntimeTypeAdapterFactory<Shape> shapeAdapterFactory = RuntimeTypeAdapterFactory.of(Shape.class) | ||
125 | * .registerSubtype(Rectangle.class) | ||
126 | * .registerSubtype(Circle.class) | ||
127 | * .registerSubtype(Diamond.class); | ||
128 | * }</pre> | ||
129 | * | ||
130 | * <h2>Serialization and deserialization</h2> | ||
131 | * In order to serialize and deserialize a polymorphic object, | ||
132 | * you must specify the base type explicitly. | ||
133 | * <pre> {@code | ||
134 | * Diamond diamond = new Diamond(); | ||
135 | * String json = gson.toJson(diamond, Shape.class); | ||
136 | * }</pre> | ||
137 | * And then: | ||
138 | * <pre> {@code | ||
139 | * Shape shape = gson.fromJson(json, Shape.class); | ||
140 | * }</pre> | ||
141 | */ | ||
142 | public final class RuntimeTypeAdapterFactory<T> implements TypeAdapterFactory { | ||
143 | private final Class<?> baseType; | ||
144 | private final String typeFieldName; | ||
145 | private final Map<String, Class<?>> labelToSubtype = new LinkedHashMap<>(); | ||
146 | private final Map<Class<?>, String> subtypeToLabel = new LinkedHashMap<>(); | ||
147 | private final boolean maintainType; | ||
148 | private boolean recognizeSubtypes; | ||
149 | |||
150 | private RuntimeTypeAdapterFactory( | ||
151 | Class<?> baseType, String typeFieldName, boolean maintainType) { | ||
152 | if (typeFieldName == null || baseType == null) { | ||
153 | throw new NullPointerException(); | ||
154 | } | ||
155 | this.baseType = baseType; | ||
156 | this.typeFieldName = typeFieldName; | ||
157 | this.maintainType = maintainType; | ||
158 | } | ||
159 | |||
160 | /** | ||
161 | * Creates a new runtime type adapter using for {@code baseType} using {@code | ||
162 | * typeFieldName} as the type field name. Type field names are case sensitive. | ||
163 | * | ||
164 | * @param maintainType true if the type field should be included in deserialized objects | ||
165 | */ | ||
166 | public static <T> RuntimeTypeAdapterFactory<T> of(Class<T> baseType, String typeFieldName, boolean maintainType) { | ||
167 | return new RuntimeTypeAdapterFactory<>(baseType, typeFieldName, maintainType); | ||
168 | } | ||
169 | |||
170 | /** | ||
171 | * Creates a new runtime type adapter using for {@code baseType} using {@code | ||
172 | * typeFieldName} as the type field name. Type field names are case sensitive. | ||
173 | */ | ||
174 | public static <T> RuntimeTypeAdapterFactory<T> of(Class<T> baseType, String typeFieldName) { | ||
175 | return new RuntimeTypeAdapterFactory<>(baseType, typeFieldName, false); | ||
176 | } | ||
177 | |||
178 | /** | ||
179 | * Creates a new runtime type adapter for {@code baseType} using {@code "type"} as | ||
180 | * the type field name. | ||
181 | */ | ||
182 | public static <T> RuntimeTypeAdapterFactory<T> of(Class<T> baseType) { | ||
183 | return new RuntimeTypeAdapterFactory<>(baseType, "type", false); | ||
184 | } | ||
185 | |||
186 | /** | ||
187 | * Ensures that this factory will handle not just the given {@code baseType}, but any subtype | ||
188 | * of that type. | ||
189 | */ | ||
190 | @CanIgnoreReturnValue | ||
191 | public RuntimeTypeAdapterFactory<T> recognizeSubtypes() { | ||
192 | this.recognizeSubtypes = true; | ||
193 | return this; | ||
194 | } | ||
195 | |||
196 | /** | ||
197 | * Registers {@code type} identified by {@code label}. Labels are case | ||
198 | * sensitive. | ||
199 | * | ||
200 | * @throws IllegalArgumentException if either {@code type} or {@code label} | ||
201 | * have already been registered on this type adapter. | ||
202 | */ | ||
203 | @CanIgnoreReturnValue | ||
204 | public RuntimeTypeAdapterFactory<T> registerSubtype(Class<? extends T> type, String label) { | ||
205 | if (type == null || label == null) { | ||
206 | throw new NullPointerException(); | ||
207 | } | ||
208 | if (subtypeToLabel.containsKey(type) || labelToSubtype.containsKey(label)) { | ||
209 | throw new IllegalArgumentException("types and labels must be unique"); | ||
210 | } | ||
211 | labelToSubtype.put(label, type); | ||
212 | subtypeToLabel.put(type, label); | ||
213 | return this; | ||
214 | } | ||
215 | |||
216 | /** | ||
217 | * Registers {@code type} identified by its {@link Class#getSimpleName simple | ||
218 | * name}. Labels are case sensitive. | ||
219 | * | ||
220 | * @throws IllegalArgumentException if either {@code type} or its simple name | ||
221 | * have already been registered on this type adapter. | ||
222 | */ | ||
223 | @CanIgnoreReturnValue | ||
224 | public RuntimeTypeAdapterFactory<T> registerSubtype(Class<? extends T> type) { | ||
225 | return registerSubtype(type, type.getSimpleName()); | ||
226 | } | ||
227 | |||
228 | @Override | ||
229 | public <R> TypeAdapter<R> create(Gson gson, TypeToken<R> type) { | ||
230 | if (type == null) { | ||
231 | return null; | ||
232 | } | ||
233 | Class<?> rawType = type.getRawType(); | ||
234 | boolean handle = | ||
235 | recognizeSubtypes ? baseType.isAssignableFrom(rawType) : baseType.equals(rawType); | ||
236 | if (!handle) { | ||
237 | return null; | ||
238 | } | ||
239 | |||
240 | final TypeAdapter<JsonElement> jsonElementAdapter = gson.getAdapter(JsonElement.class); | ||
241 | final Map<String, TypeAdapter<?>> labelToDelegate = new LinkedHashMap<>(); | ||
242 | final Map<Class<?>, TypeAdapter<?>> subtypeToDelegate = new LinkedHashMap<>(); | ||
243 | for (Map.Entry<String, Class<?>> entry : labelToSubtype.entrySet()) { | ||
244 | TypeAdapter<?> delegate = gson.getDelegateAdapter(this, TypeToken.get(entry.getValue())); | ||
245 | labelToDelegate.put(entry.getKey(), delegate); | ||
246 | subtypeToDelegate.put(entry.getValue(), delegate); | ||
247 | } | ||
248 | |||
249 | return new TypeAdapter<R>() { | ||
250 | @Override public R read(JsonReader in) throws IOException { | ||
251 | JsonElement jsonElement = jsonElementAdapter.read(in); | ||
252 | JsonElement labelJsonElement; | ||
253 | if (maintainType) { | ||
254 | labelJsonElement = jsonElement.getAsJsonObject().get(typeFieldName); | ||
255 | } else { | ||
256 | labelJsonElement = jsonElement.getAsJsonObject().remove(typeFieldName); | ||
257 | } | ||
258 | |||
259 | if (labelJsonElement == null) { | ||
260 | throw new JsonParseException("cannot deserialize " + baseType | ||
261 | + " because it does not define a field named " + typeFieldName); | ||
262 | } | ||
263 | String label = labelJsonElement.getAsString(); | ||
264 | @SuppressWarnings("unchecked") // registration requires that subtype extends T | ||
265 | TypeAdapter<R> delegate = (TypeAdapter<R>) labelToDelegate.get(label); | ||
266 | if (delegate == null) { | ||
267 | throw new JsonParseException("cannot deserialize " + baseType + " subtype named " | ||
268 | + label + "; did you forget to register a subtype?"); | ||
269 | } | ||
270 | return delegate.fromJsonTree(jsonElement); | ||
271 | } | ||
272 | |||
273 | @Override public void write(JsonWriter out, R value) throws IOException { | ||
274 | Class<?> srcType = value.getClass(); | ||
275 | String label = subtypeToLabel.get(srcType); | ||
276 | @SuppressWarnings("unchecked") // registration requires that subtype extends T | ||
277 | TypeAdapter<R> delegate = (TypeAdapter<R>) subtypeToDelegate.get(srcType); | ||
278 | if (delegate == null) { | ||
279 | throw new JsonParseException("cannot serialize " + srcType.getName() | ||
280 | + "; did you forget to register a subtype?"); | ||
281 | } | ||
282 | JsonObject jsonObject = delegate.toJsonTree(value).getAsJsonObject(); | ||
283 | |||
284 | if (maintainType) { | ||
285 | jsonElementAdapter.write(out, jsonObject); | ||
286 | return; | ||
287 | } | ||
288 | |||
289 | JsonObject clone = new JsonObject(); | ||
290 | |||
291 | if (jsonObject.has(typeFieldName)) { | ||
292 | throw new JsonParseException("cannot serialize " + srcType.getName() | ||
293 | + " because it already defines a field named " + typeFieldName); | ||
294 | } | ||
295 | clone.add(typeFieldName, new JsonPrimitive(label)); | ||
296 | |||
297 | for (Map.Entry<String, JsonElement> e : jsonObject.entrySet()) { | ||
298 | clone.add(e.getKey(), e.getValue()); | ||
299 | } | ||
300 | jsonElementAdapter.write(out, clone); | ||
301 | } | ||
302 | }.nullSafe(); | ||
303 | } | ||
304 | } | ||
diff --git a/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/servlet/XtextWebSocket.java b/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/servlet/XtextWebSocket.java index 043d318c..1fde1be5 100644 --- a/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/servlet/XtextWebSocket.java +++ b/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/servlet/XtextWebSocket.java | |||
@@ -6,6 +6,7 @@ | |||
6 | package tools.refinery.language.web.xtext.servlet; | 6 | package tools.refinery.language.web.xtext.servlet; |
7 | 7 | ||
8 | import com.google.gson.Gson; | 8 | import com.google.gson.Gson; |
9 | import com.google.gson.GsonBuilder; | ||
9 | import com.google.gson.JsonIOException; | 10 | import com.google.gson.JsonIOException; |
10 | import com.google.gson.JsonParseException; | 11 | import com.google.gson.JsonParseException; |
11 | import org.eclipse.jetty.websocket.api.Callback; | 12 | import org.eclipse.jetty.websocket.api.Callback; |
@@ -16,6 +17,7 @@ import org.eclipse.xtext.resource.IResourceServiceProvider; | |||
16 | import org.eclipse.xtext.web.server.ISession; | 17 | import org.eclipse.xtext.web.server.ISession; |
17 | import org.slf4j.Logger; | 18 | import org.slf4j.Logger; |
18 | import org.slf4j.LoggerFactory; | 19 | import org.slf4j.LoggerFactory; |
20 | import tools.refinery.language.semantics.metadata.*; | ||
19 | import tools.refinery.language.web.xtext.server.ResponseHandler; | 21 | import tools.refinery.language.web.xtext.server.ResponseHandler; |
20 | import tools.refinery.language.web.xtext.server.ResponseHandlerException; | 22 | import tools.refinery.language.web.xtext.server.ResponseHandlerException; |
21 | import tools.refinery.language.web.xtext.server.TransactionExecutor; | 23 | import tools.refinery.language.web.xtext.server.TransactionExecutor; |
@@ -28,7 +30,15 @@ import java.io.Reader; | |||
28 | public class XtextWebSocket implements ResponseHandler { | 30 | public class XtextWebSocket implements ResponseHandler { |
29 | private static final Logger LOG = LoggerFactory.getLogger(XtextWebSocket.class); | 31 | private static final Logger LOG = LoggerFactory.getLogger(XtextWebSocket.class); |
30 | 32 | ||
31 | private final Gson gson = new Gson(); | 33 | private final Gson gson = new GsonBuilder() |
34 | .disableJdkUnsafe() | ||
35 | .registerTypeAdapterFactory(RuntimeTypeAdapterFactory.of(RelationDetail.class, "type") | ||
36 | .registerSubtype(ClassDetail.class, "class") | ||
37 | .registerSubtype(ReferenceDetail.class, "reference") | ||
38 | .registerSubtype(OppositeReferenceDetail.class, "opposite") | ||
39 | .registerSubtype(PredicateDetail.class, "predicate") | ||
40 | .registerSubtype(BuiltInDetail.class, "builtin")) | ||
41 | .create(); | ||
32 | 42 | ||
33 | private final TransactionExecutor executor; | 43 | private final TransactionExecutor executor; |
34 | 44 | ||
@@ -70,10 +80,11 @@ public class XtextWebSocket implements ResponseHandler { | |||
70 | 80 | ||
71 | @OnWebSocketError | 81 | @OnWebSocketError |
72 | public void onError(Throwable error) { | 82 | public void onError(Throwable error) { |
83 | executor.dispose(); | ||
73 | if (webSocketSession == null) { | 84 | if (webSocketSession == null) { |
74 | return; | 85 | return; |
75 | } | 86 | } |
76 | LOG.error("Internal websocket error in connection from" + webSocketSession.getRemoteSocketAddress(), error); | 87 | LOG.error("Internal websocket error in connection from " + webSocketSession.getRemoteSocketAddress(), error); |
77 | } | 88 | } |
78 | 89 | ||
79 | @OnWebSocketMessage | 90 | @OnWebSocketMessage |
@@ -86,14 +97,18 @@ public class XtextWebSocket implements ResponseHandler { | |||
86 | try { | 97 | try { |
87 | request = gson.fromJson(reader, XtextWebRequest.class); | 98 | request = gson.fromJson(reader, XtextWebRequest.class); |
88 | } catch (JsonIOException e) { | 99 | } catch (JsonIOException e) { |
89 | LOG.error("Cannot read from websocket from" + webSocketSession.getRemoteSocketAddress(), e); | 100 | LOG.error("Cannot read from websocket from " + webSocketSession.getRemoteSocketAddress(), e); |
90 | if (webSocketSession.isOpen()) { | 101 | if (webSocketSession.isOpen()) { |
102 | executor.dispose(); | ||
91 | webSocketSession.close(StatusCode.SERVER_ERROR, "Cannot read payload", Callback.NOOP); | 103 | webSocketSession.close(StatusCode.SERVER_ERROR, "Cannot read payload", Callback.NOOP); |
92 | } | 104 | } |
93 | return; | 105 | return; |
94 | } catch (JsonParseException e) { | 106 | } catch (JsonParseException e) { |
95 | LOG.warn("Malformed websocket request from" + webSocketSession.getRemoteSocketAddress(), e); | 107 | LOG.warn("Malformed websocket request from " + webSocketSession.getRemoteSocketAddress(), e); |
96 | webSocketSession.close(XtextStatusCode.INVALID_JSON, "Invalid JSON payload", Callback.NOOP); | 108 | if (webSocketSession.isOpen()) { |
109 | executor.dispose(); | ||
110 | webSocketSession.close(XtextStatusCode.INVALID_JSON, "Invalid JSON payload", Callback.NOOP); | ||
111 | } | ||
97 | return; | 112 | return; |
98 | } | 113 | } |
99 | try { | 114 | try { |
@@ -101,6 +116,7 @@ public class XtextWebSocket implements ResponseHandler { | |||
101 | } catch (ResponseHandlerException e) { | 116 | } catch (ResponseHandlerException e) { |
102 | LOG.warn("Cannot write websocket response", e); | 117 | LOG.warn("Cannot write websocket response", e); |
103 | if (webSocketSession.isOpen()) { | 118 | if (webSocketSession.isOpen()) { |
119 | executor.dispose(); | ||
104 | webSocketSession.close(StatusCode.SERVER_ERROR, "Cannot write response", Callback.NOOP); | 120 | webSocketSession.close(StatusCode.SERVER_ERROR, "Cannot write response", Callback.NOOP); |
105 | } | 121 | } |
106 | } | 122 | } |
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; | |||
18 | import org.junit.jupiter.api.extension.ExtendWith; | 18 | import org.junit.jupiter.api.extension.ExtendWith; |
19 | import org.mockito.ArgumentCaptor; | 19 | import org.mockito.ArgumentCaptor; |
20 | import org.mockito.junit.jupiter.MockitoExtension; | 20 | import org.mockito.junit.jupiter.MockitoExtension; |
21 | import tools.refinery.language.web.semantics.SemanticsService; | ||
21 | import tools.refinery.language.web.tests.AwaitTerminationExecutorServiceProvider; | 22 | import tools.refinery.language.web.tests.AwaitTerminationExecutorServiceProvider; |
22 | import tools.refinery.language.web.tests.ProblemWebInjectorProvider; | 23 | import tools.refinery.language.web.tests.ProblemWebInjectorProvider; |
23 | import tools.refinery.language.web.xtext.server.ResponseHandler; | 24 | import 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 | ||