aboutsummaryrefslogtreecommitdiffstats
path: root/language-web/src/main/java/tools
diff options
context:
space:
mode:
authorLibravatar Kristóf Marussy <kristof@marussy.com>2021-12-12 17:48:47 +0100
committerLibravatar Kristóf Marussy <kristof@marussy.com>2021-12-12 17:48:47 +0100
commitfc7e9312d00e60171ed77c477ed91231d3dbfff9 (patch)
treecc185dd088b5fa6e9357aab3c9062a70626d1953 /language-web/src/main/java/tools
parentbuild: refactor java-application conventions (diff)
downloadrefinery-fc7e9312d00e60171ed77c477ed91231d3dbfff9.tar.gz
refinery-fc7e9312d00e60171ed77c477ed91231d3dbfff9.tar.zst
refinery-fc7e9312d00e60171ed77c477ed91231d3dbfff9.zip
build: move modules into subproject directory
Diffstat (limited to 'language-web/src/main/java/tools')
-rw-r--r--language-web/src/main/java/tools/refinery/language/web/CacheControlFilter.java52
-rw-r--r--language-web/src/main/java/tools/refinery/language/web/ProblemWebModule.java35
-rw-r--r--language-web/src/main/java/tools/refinery/language/web/ProblemWebSetup.java25
-rw-r--r--language-web/src/main/java/tools/refinery/language/web/ProblemWebSocketServlet.java29
-rw-r--r--language-web/src/main/java/tools/refinery/language/web/ServerLauncher.java192
-rw-r--r--language-web/src/main/java/tools/refinery/language/web/occurrences/ProblemOccurrencesService.java16
-rw-r--r--language-web/src/main/java/tools/refinery/language/web/xtext/server/PongResult.java44
-rw-r--r--language-web/src/main/java/tools/refinery/language/web/xtext/server/ResponseHandler.java8
-rw-r--r--language-web/src/main/java/tools/refinery/language/web/xtext/server/ResponseHandlerException.java14
-rw-r--r--language-web/src/main/java/tools/refinery/language/web/xtext/server/SubscribingServiceContext.java26
-rw-r--r--language-web/src/main/java/tools/refinery/language/web/xtext/server/TransactionExecutor.java180
-rw-r--r--language-web/src/main/java/tools/refinery/language/web/xtext/server/message/XtextWebErrorKind.java11
-rw-r--r--language-web/src/main/java/tools/refinery/language/web/xtext/server/message/XtextWebErrorResponse.java79
-rw-r--r--language-web/src/main/java/tools/refinery/language/web/xtext/server/message/XtextWebOkResponse.java72
-rw-r--r--language-web/src/main/java/tools/refinery/language/web/xtext/server/message/XtextWebPushMessage.java81
-rw-r--r--language-web/src/main/java/tools/refinery/language/web/xtext/server/message/XtextWebRequest.java57
-rw-r--r--language-web/src/main/java/tools/refinery/language/web/xtext/server/message/XtextWebResponse.java4
-rw-r--r--language-web/src/main/java/tools/refinery/language/web/xtext/server/push/PrecomputationListener.java15
-rw-r--r--language-web/src/main/java/tools/refinery/language/web/xtext/server/push/PushServiceDispatcher.java23
-rw-r--r--language-web/src/main/java/tools/refinery/language/web/xtext/server/push/PushWebDocument.java89
-rw-r--r--language-web/src/main/java/tools/refinery/language/web/xtext/server/push/PushWebDocumentAccess.java68
-rw-r--r--language-web/src/main/java/tools/refinery/language/web/xtext/server/push/PushWebDocumentProvider.java33
-rw-r--r--language-web/src/main/java/tools/refinery/language/web/xtext/servlet/SimpleServiceContext.java26
-rw-r--r--language-web/src/main/java/tools/refinery/language/web/xtext/servlet/SimpleSession.java35
-rw-r--r--language-web/src/main/java/tools/refinery/language/web/xtext/servlet/XtextStatusCode.java9
-rw-r--r--language-web/src/main/java/tools/refinery/language/web/xtext/servlet/XtextWebSocket.java133
-rw-r--r--language-web/src/main/java/tools/refinery/language/web/xtext/servlet/XtextWebSocketServlet.java83
27 files changed, 0 insertions, 1439 deletions
diff --git a/language-web/src/main/java/tools/refinery/language/web/CacheControlFilter.java b/language-web/src/main/java/tools/refinery/language/web/CacheControlFilter.java
deleted file mode 100644
index b13ae95d..00000000
--- a/language-web/src/main/java/tools/refinery/language/web/CacheControlFilter.java
+++ /dev/null
@@ -1,52 +0,0 @@
1package tools.refinery.language.web;
2
3import java.io.IOException;
4import java.time.Duration;
5import java.util.regex.Pattern;
6
7import org.eclipse.jetty.http.HttpHeader;
8
9import jakarta.servlet.Filter;
10import jakarta.servlet.FilterChain;
11import jakarta.servlet.FilterConfig;
12import jakarta.servlet.ServletException;
13import jakarta.servlet.ServletRequest;
14import jakarta.servlet.ServletResponse;
15import jakarta.servlet.http.HttpServletRequest;
16import jakarta.servlet.http.HttpServletResponse;
17
18public class CacheControlFilter implements Filter {
19 private static final Pattern CACHE_URI_PATTERN = Pattern.compile(".*\\.(css|gif|js|map|png|svg|woff2)");
20
21 private static final Duration EXPIRY = Duration.ofDays(365);
22
23 private static final String CACHE_CONTROL_CACHE_VALUE = "public, max-age: " + EXPIRY.toSeconds() + ", immutable";
24
25 private static final String CACHE_CONTROL_NO_CACHE_VALUE = "no-cache, no-store, max-age: 0, must-revalidate";
26
27 @Override
28 public void init(FilterConfig filterConfig) throws ServletException {
29 // Nothing to initialize.
30 }
31
32 @Override
33 public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
34 throws IOException, ServletException {
35 if (request instanceof HttpServletRequest httpRequest && response instanceof HttpServletResponse httpResponse) {
36 if (CACHE_URI_PATTERN.matcher(httpRequest.getRequestURI()).matches()) {
37 httpResponse.setHeader(HttpHeader.CACHE_CONTROL.asString(), CACHE_CONTROL_CACHE_VALUE);
38 httpResponse.setDateHeader(HttpHeader.EXPIRES.asString(),
39 System.currentTimeMillis() + EXPIRY.toMillis());
40 } else {
41 httpResponse.setHeader(HttpHeader.CACHE_CONTROL.asString(), CACHE_CONTROL_NO_CACHE_VALUE);
42 httpResponse.setDateHeader(HttpHeader.EXPIRES.asString(), 0);
43 }
44 }
45 chain.doFilter(request, response);
46 }
47
48 @Override
49 public void destroy() {
50 // Nothing to dispose.
51 }
52}
diff --git a/language-web/src/main/java/tools/refinery/language/web/ProblemWebModule.java b/language-web/src/main/java/tools/refinery/language/web/ProblemWebModule.java
deleted file mode 100644
index ec55036f..00000000
--- a/language-web/src/main/java/tools/refinery/language/web/ProblemWebModule.java
+++ /dev/null
@@ -1,35 +0,0 @@
1/*
2 * generated by Xtext 2.25.0
3 */
4package tools.refinery.language.web;
5
6import org.eclipse.xtext.web.server.XtextServiceDispatcher;
7import org.eclipse.xtext.web.server.model.IWebDocumentProvider;
8import org.eclipse.xtext.web.server.model.XtextWebDocumentAccess;
9import org.eclipse.xtext.web.server.occurrences.OccurrencesService;
10
11import tools.refinery.language.web.occurrences.ProblemOccurrencesService;
12import tools.refinery.language.web.xtext.server.push.PushServiceDispatcher;
13import tools.refinery.language.web.xtext.server.push.PushWebDocumentAccess;
14import tools.refinery.language.web.xtext.server.push.PushWebDocumentProvider;
15
16/**
17 * Use this class to register additional components to be used within the web application.
18 */
19public class ProblemWebModule extends AbstractProblemWebModule {
20 public Class<? extends IWebDocumentProvider> bindIWebDocumentProvider() {
21 return PushWebDocumentProvider.class;
22 }
23
24 public Class<? extends XtextWebDocumentAccess> bindXtextWebDocumentAccess() {
25 return PushWebDocumentAccess.class;
26 }
27
28 public Class<? extends XtextServiceDispatcher> bindXtextServiceDispatcher() {
29 return PushServiceDispatcher.class;
30 }
31
32 public Class<? extends OccurrencesService> bindOccurrencesService() {
33 return ProblemOccurrencesService.class;
34 }
35}
diff --git a/language-web/src/main/java/tools/refinery/language/web/ProblemWebSetup.java b/language-web/src/main/java/tools/refinery/language/web/ProblemWebSetup.java
deleted file mode 100644
index 4738bc80..00000000
--- a/language-web/src/main/java/tools/refinery/language/web/ProblemWebSetup.java
+++ /dev/null
@@ -1,25 +0,0 @@
1/*
2 * generated by Xtext 2.25.0
3 */
4package tools.refinery.language.web;
5
6import org.eclipse.xtext.util.Modules2;
7
8import com.google.inject.Guice;
9import com.google.inject.Injector;
10
11import tools.refinery.language.ProblemRuntimeModule;
12import tools.refinery.language.ProblemStandaloneSetup;
13import tools.refinery.language.ide.ProblemIdeModule;
14
15/**
16 * Initialization support for running Xtext languages in web applications.
17 */
18public class ProblemWebSetup extends ProblemStandaloneSetup {
19
20 @Override
21 public Injector createInjector() {
22 return Guice.createInjector(Modules2.mixin(new ProblemRuntimeModule(), new ProblemIdeModule(), new ProblemWebModule()));
23 }
24
25}
diff --git a/language-web/src/main/java/tools/refinery/language/web/ProblemWebSocketServlet.java b/language-web/src/main/java/tools/refinery/language/web/ProblemWebSocketServlet.java
deleted file mode 100644
index df67b521..00000000
--- a/language-web/src/main/java/tools/refinery/language/web/ProblemWebSocketServlet.java
+++ /dev/null
@@ -1,29 +0,0 @@
1package tools.refinery.language.web;
2
3import org.eclipse.xtext.util.DisposableRegistry;
4
5import jakarta.servlet.ServletException;
6import tools.refinery.language.web.xtext.servlet.XtextWebSocketServlet;
7
8public class ProblemWebSocketServlet extends XtextWebSocketServlet {
9
10 private static final long serialVersionUID = -7040955470384797008L;
11
12 private transient DisposableRegistry disposableRegistry;
13
14 @Override
15 public void init() throws ServletException {
16 super.init();
17 var injector = new ProblemWebSetup().createInjectorAndDoEMFRegistration();
18 this.disposableRegistry = injector.getInstance(DisposableRegistry.class);
19 }
20
21 @Override
22 public void destroy() {
23 if (disposableRegistry != null) {
24 disposableRegistry.dispose();
25 disposableRegistry = null;
26 }
27 super.destroy();
28 }
29}
diff --git a/language-web/src/main/java/tools/refinery/language/web/ServerLauncher.java b/language-web/src/main/java/tools/refinery/language/web/ServerLauncher.java
deleted file mode 100644
index ffd903d0..00000000
--- a/language-web/src/main/java/tools/refinery/language/web/ServerLauncher.java
+++ /dev/null
@@ -1,192 +0,0 @@
1/*
2 * generated by Xtext 2.25.0
3 */
4package tools.refinery.language.web;
5
6import java.io.File;
7import java.io.IOException;
8import java.net.InetSocketAddress;
9import java.net.URI;
10import java.net.URISyntaxException;
11import java.util.EnumSet;
12import java.util.Optional;
13import java.util.Set;
14
15import org.eclipse.jetty.server.Server;
16import org.eclipse.jetty.server.session.SessionHandler;
17import org.eclipse.jetty.servlet.DefaultServlet;
18import org.eclipse.jetty.servlet.ServletContextHandler;
19import org.eclipse.jetty.servlet.ServletHolder;
20import org.eclipse.jetty.util.resource.Resource;
21import org.eclipse.jetty.websocket.server.config.JettyWebSocketServletContainerInitializer;
22import org.slf4j.Logger;
23import org.slf4j.LoggerFactory;
24
25import jakarta.servlet.DispatcherType;
26import jakarta.servlet.SessionTrackingMode;
27import tools.refinery.language.web.xtext.servlet.XtextWebSocketServlet;
28
29public class ServerLauncher {
30 public static final String DEFAULT_LISTEN_ADDRESS = "localhost";
31
32 public static final int DEFAULT_LISTEN_PORT = 1312;
33
34 public static final int DEFAULT_PUBLIC_PORT = 443;
35
36 public static final int HTTP_DEFAULT_PORT = 80;
37
38 public static final int HTTPS_DEFAULT_PORT = 443;
39
40 public static final String ALLOWED_ORIGINS_SEPARATOR = ";";
41
42 private static final Logger LOG = LoggerFactory.getLogger(ServerLauncher.class);
43
44 private final Server server;
45
46 public ServerLauncher(InetSocketAddress bindAddress, Resource baseResource, Optional<String[]> allowedOrigins) {
47 server = new Server(bindAddress);
48 var handler = new ServletContextHandler();
49 addSessionHandler(handler);
50 addProblemServlet(handler, allowedOrigins);
51 if (baseResource != null) {
52 handler.setBaseResource(baseResource);
53 handler.setWelcomeFiles(new String[] { "index.html" });
54 addDefaultServlet(handler);
55 }
56 handler.addFilter(CacheControlFilter.class, "/*", EnumSet.of(DispatcherType.REQUEST));
57 server.setHandler(handler);
58 }
59
60 private void addSessionHandler(ServletContextHandler handler) {
61 var sessionHandler = new SessionHandler();
62 sessionHandler.setSessionTrackingModes(Set.of(SessionTrackingMode.COOKIE));
63 handler.setSessionHandler(sessionHandler);
64 }
65
66 private void addProblemServlet(ServletContextHandler handler, Optional<String[]> allowedOrigins) {
67 var problemServletHolder = new ServletHolder(ProblemWebSocketServlet.class);
68 if (allowedOrigins.isEmpty()) {
69 LOG.warn("All WebSocket origins are allowed! This setting should not be used in production!");
70 } else {
71 var allowedOriginsString = String.join(XtextWebSocketServlet.ALLOWED_ORIGINS_SEPARATOR,
72 allowedOrigins.get());
73 problemServletHolder.setInitParameter(XtextWebSocketServlet.ALLOWED_ORIGINS_INIT_PARAM,
74 allowedOriginsString);
75 }
76 handler.addServlet(problemServletHolder, "/xtext-service");
77 JettyWebSocketServletContainerInitializer.configure(handler, null);
78 }
79
80 private void addDefaultServlet(ServletContextHandler handler) {
81 var defaultServletHolder = new ServletHolder(DefaultServlet.class);
82 var isWindows = System.getProperty("os.name").toLowerCase().contains("win");
83 // Avoid file locking on Windows: https://stackoverflow.com/a/4985717
84 // See also the related Jetty ticket:
85 // https://github.com/eclipse/jetty.project/issues/2925
86 defaultServletHolder.setInitParameter("useFileMappedBuffer", isWindows ? "false" : "true");
87 handler.addServlet(defaultServletHolder, "/");
88 }
89
90 public void start() throws Exception {
91 server.start();
92 LOG.info("Server started on {}", server.getURI());
93 server.join();
94 }
95
96 public static void main(String[] args) {
97 try {
98 var bindAddress = getBindAddress();
99 var baseResource = getBaseResource();
100 var allowedOrigins = getAllowedOrigins();
101 var serverLauncher = new ServerLauncher(bindAddress, baseResource, allowedOrigins);
102 serverLauncher.start();
103 } catch (Exception exception) {
104 LOG.error("Fatal server error", exception);
105 System.exit(1);
106 }
107 }
108
109 private static String getListenAddress() {
110 var listenAddress = System.getenv("LISTEN_ADDRESS");
111 if (listenAddress == null) {
112 return DEFAULT_LISTEN_ADDRESS;
113 }
114 return listenAddress;
115 }
116
117 private static int getListenPort() {
118 var portStr = System.getenv("LISTEN_PORT");
119 if (portStr != null) {
120 return Integer.parseInt(portStr);
121 }
122 return DEFAULT_LISTEN_PORT;
123 }
124
125 private static InetSocketAddress getBindAddress() {
126 var listenAddress = getListenAddress();
127 var listenPort = getListenPort();
128 return new InetSocketAddress(listenAddress, listenPort);
129 }
130
131 private static Resource getBaseResource() throws IOException, URISyntaxException {
132 var baseResourceOverride = System.getenv("BASE_RESOURCE");
133 if (baseResourceOverride != null) {
134 // If a user override is provided, use it.
135 return Resource.newResource(baseResourceOverride);
136 }
137 var indexUrlInJar = ServerLauncher.class.getResource("/webapp/index.html");
138 if (indexUrlInJar != null) {
139 // If the app is packaged in the jar, serve it.
140 var webRootUri = URI.create(indexUrlInJar.toURI().toASCIIString().replaceFirst("/index.html$", "/"));
141 return Resource.newResource(webRootUri);
142 }
143 // Look for unpacked production artifacts (convenience for running from IDE).
144 var unpackedResourcePathComponents = new String[] { System.getProperty("user.dir"), "build", "webpack",
145 "production" };
146 var unpackedResourceDir = new File(String.join(File.separator, unpackedResourcePathComponents));
147 if (unpackedResourceDir.isDirectory()) {
148 return Resource.newResource(unpackedResourceDir);
149 }
150 // Fall back to just serving a 404.
151 return null;
152 }
153
154 private static String getPublicHost() {
155 var publicHost = System.getenv("PUBLIC_HOST");
156 if (publicHost != null) {
157 return publicHost.toLowerCase();
158 }
159 return null;
160 }
161
162 private static int getPublicPort() {
163 var portStr = System.getenv("PUBLIC_PORT");
164 if (portStr != null) {
165 return Integer.parseInt(portStr);
166 }
167 return DEFAULT_LISTEN_PORT;
168 }
169
170 private static Optional<String[]> getAllowedOrigins() {
171 var allowedOrigins = System.getenv("ALLOWED_ORIGINS");
172 if (allowedOrigins != null) {
173 return Optional.of(allowedOrigins.split(ALLOWED_ORIGINS_SEPARATOR));
174 }
175 return getAllowedOriginsFromPublicHostAndPort();
176 }
177
178 private static Optional<String[]> getAllowedOriginsFromPublicHostAndPort() {
179 var publicHost = getPublicHost();
180 if (publicHost == null) {
181 return Optional.empty();
182 }
183 int publicPort = getPublicPort();
184 var scheme = publicPort == HTTPS_DEFAULT_PORT ? "https" : "http";
185 var urlWithPort = String.format("%s://%s:%d", scheme, publicHost, publicPort);
186 if (publicPort == HTTPS_DEFAULT_PORT || publicPort == HTTP_DEFAULT_PORT) {
187 var urlWithoutPort = String.format("%s://%s", scheme, publicHost);
188 return Optional.of(new String[] { urlWithPort, urlWithoutPort });
189 }
190 return Optional.of(new String[] { urlWithPort });
191 }
192}
diff --git a/language-web/src/main/java/tools/refinery/language/web/occurrences/ProblemOccurrencesService.java b/language-web/src/main/java/tools/refinery/language/web/occurrences/ProblemOccurrencesService.java
deleted file mode 100644
index d32bbb54..00000000
--- a/language-web/src/main/java/tools/refinery/language/web/occurrences/ProblemOccurrencesService.java
+++ /dev/null
@@ -1,16 +0,0 @@
1package tools.refinery.language.web.occurrences;
2
3import org.eclipse.emf.ecore.EObject;
4import org.eclipse.xtext.web.server.occurrences.OccurrencesService;
5
6import com.google.inject.Singleton;
7
8import tools.refinery.language.model.problem.NamedElement;
9
10@Singleton
11public class ProblemOccurrencesService extends OccurrencesService {
12 @Override
13 protected boolean filter(EObject element) {
14 return super.filter(element) && element instanceof NamedElement;
15 }
16}
diff --git a/language-web/src/main/java/tools/refinery/language/web/xtext/server/PongResult.java b/language-web/src/main/java/tools/refinery/language/web/xtext/server/PongResult.java
deleted file mode 100644
index fe510f51..00000000
--- a/language-web/src/main/java/tools/refinery/language/web/xtext/server/PongResult.java
+++ /dev/null
@@ -1,44 +0,0 @@
1package tools.refinery.language.web.xtext.server;
2
3import java.util.Objects;
4
5import org.eclipse.xtext.web.server.IServiceResult;
6
7public class PongResult implements IServiceResult {
8 private String pong;
9
10 public PongResult(String pong) {
11 super();
12 this.pong = pong;
13 }
14
15 public String getPong() {
16 return pong;
17 }
18
19 public void setPong(String pong) {
20 this.pong = pong;
21 }
22
23 @Override
24 public int hashCode() {
25 return Objects.hash(pong);
26 }
27
28 @Override
29 public boolean equals(Object obj) {
30 if (this == obj)
31 return true;
32 if (obj == null)
33 return false;
34 if (getClass() != obj.getClass())
35 return false;
36 PongResult other = (PongResult) obj;
37 return Objects.equals(pong, other.pong);
38 }
39
40 @Override
41 public String toString() {
42 return "PongResult [pong=" + pong + "]";
43 }
44}
diff --git a/language-web/src/main/java/tools/refinery/language/web/xtext/server/ResponseHandler.java b/language-web/src/main/java/tools/refinery/language/web/xtext/server/ResponseHandler.java
deleted file mode 100644
index 2a85afe3..00000000
--- a/language-web/src/main/java/tools/refinery/language/web/xtext/server/ResponseHandler.java
+++ /dev/null
@@ -1,8 +0,0 @@
1package tools.refinery.language.web.xtext.server;
2
3import tools.refinery.language.web.xtext.server.message.XtextWebResponse;
4
5@FunctionalInterface
6public interface ResponseHandler {
7 void onResponse(XtextWebResponse response) throws ResponseHandlerException;
8}
diff --git a/language-web/src/main/java/tools/refinery/language/web/xtext/server/ResponseHandlerException.java b/language-web/src/main/java/tools/refinery/language/web/xtext/server/ResponseHandlerException.java
deleted file mode 100644
index 34fcb546..00000000
--- a/language-web/src/main/java/tools/refinery/language/web/xtext/server/ResponseHandlerException.java
+++ /dev/null
@@ -1,14 +0,0 @@
1package tools.refinery.language.web.xtext.server;
2
3public class ResponseHandlerException extends Exception {
4
5 private static final long serialVersionUID = 3589866922420268164L;
6
7 public ResponseHandlerException(String message, Throwable cause) {
8 super(message, cause);
9 }
10
11 public ResponseHandlerException(String message) {
12 super(message);
13 }
14}
diff --git a/language-web/src/main/java/tools/refinery/language/web/xtext/server/SubscribingServiceContext.java b/language-web/src/main/java/tools/refinery/language/web/xtext/server/SubscribingServiceContext.java
deleted file mode 100644
index 78e00a9e..00000000
--- a/language-web/src/main/java/tools/refinery/language/web/xtext/server/SubscribingServiceContext.java
+++ /dev/null
@@ -1,26 +0,0 @@
1package tools.refinery.language.web.xtext.server;
2
3import java.util.Set;
4
5import org.eclipse.xtext.web.server.IServiceContext;
6import org.eclipse.xtext.web.server.ISession;
7
8import tools.refinery.language.web.xtext.server.push.PrecomputationListener;
9
10public record SubscribingServiceContext(IServiceContext delegate, PrecomputationListener subscriber)
11 implements IServiceContext {
12 @Override
13 public Set<String> getParameterKeys() {
14 return delegate.getParameterKeys();
15 }
16
17 @Override
18 public String getParameter(String key) {
19 return delegate.getParameter(key);
20 }
21
22 @Override
23 public ISession getSession() {
24 return delegate.getSession();
25 }
26}
diff --git a/language-web/src/main/java/tools/refinery/language/web/xtext/server/TransactionExecutor.java b/language-web/src/main/java/tools/refinery/language/web/xtext/server/TransactionExecutor.java
deleted file mode 100644
index 0b417b06..00000000
--- a/language-web/src/main/java/tools/refinery/language/web/xtext/server/TransactionExecutor.java
+++ /dev/null
@@ -1,180 +0,0 @@
1package tools.refinery.language.web.xtext.server;
2
3import java.lang.ref.WeakReference;
4import java.util.ArrayList;
5import java.util.HashMap;
6import java.util.List;
7import java.util.Map;
8
9import org.eclipse.emf.common.util.URI;
10import org.eclipse.xtext.resource.IResourceServiceProvider;
11import org.eclipse.xtext.util.IDisposable;
12import org.eclipse.xtext.web.server.IServiceContext;
13import org.eclipse.xtext.web.server.IServiceResult;
14import org.eclipse.xtext.web.server.ISession;
15import org.eclipse.xtext.web.server.InvalidRequestException;
16import org.eclipse.xtext.web.server.InvalidRequestException.UnknownLanguageException;
17import org.eclipse.xtext.web.server.XtextServiceDispatcher;
18import org.slf4j.Logger;
19import org.slf4j.LoggerFactory;
20
21import com.google.common.base.Strings;
22import com.google.inject.Injector;
23
24import tools.refinery.language.web.xtext.server.message.XtextWebErrorKind;
25import tools.refinery.language.web.xtext.server.message.XtextWebErrorResponse;
26import tools.refinery.language.web.xtext.server.message.XtextWebOkResponse;
27import tools.refinery.language.web.xtext.server.message.XtextWebPushMessage;
28import tools.refinery.language.web.xtext.server.message.XtextWebRequest;
29import tools.refinery.language.web.xtext.server.push.PrecomputationListener;
30import tools.refinery.language.web.xtext.server.push.PushWebDocument;
31import tools.refinery.language.web.xtext.servlet.SimpleServiceContext;
32
33public class TransactionExecutor implements IDisposable, PrecomputationListener {
34 private static final Logger LOG = LoggerFactory.getLogger(TransactionExecutor.class);
35
36 private final ISession session;
37
38 private final IResourceServiceProvider.Registry resourceServiceProviderRegistry;
39
40 private final Map<String, WeakReference<PushWebDocument>> subscriptions = new HashMap<>();
41
42 private ResponseHandler responseHandler;
43
44 private Object callPendingLock = new Object();
45
46 private boolean callPending;
47
48 private List<XtextWebPushMessage> pendingPushMessages = new ArrayList<>();
49
50 public TransactionExecutor(ISession session, IResourceServiceProvider.Registry resourceServiceProviderRegistry) {
51 this.session = session;
52 this.resourceServiceProviderRegistry = resourceServiceProviderRegistry;
53 }
54
55 public void setResponseHandler(ResponseHandler responseHandler) {
56 this.responseHandler = responseHandler;
57 }
58
59 public void handleRequest(XtextWebRequest request) throws ResponseHandlerException {
60 var serviceContext = new SimpleServiceContext(session, request.getRequestData());
61 var ping = serviceContext.getParameter("ping");
62 if (ping != null) {
63 responseHandler.onResponse(new XtextWebOkResponse(request, new PongResult(ping)));
64 return;
65 }
66 synchronized (callPendingLock) {
67 if (callPending) {
68 LOG.error("Reentrant request detected");
69 }
70 if (!pendingPushMessages.isEmpty()) {
71 LOG.error("{} push messages got stuck without a pending request", pendingPushMessages.size());
72 }
73 callPending = true;
74 }
75 try {
76 var injector = getInjector(serviceContext);
77 var serviceDispatcher = injector.getInstance(XtextServiceDispatcher.class);
78 var service = serviceDispatcher.getService(new SubscribingServiceContext(serviceContext, this));
79 var serviceResult = service.getService().apply();
80 responseHandler.onResponse(new XtextWebOkResponse(request, serviceResult));
81 } catch (InvalidRequestException e) {
82 responseHandler.onResponse(new XtextWebErrorResponse(request, XtextWebErrorKind.REQUEST_ERROR, e));
83 } catch (RuntimeException e) {
84 responseHandler.onResponse(new XtextWebErrorResponse(request, XtextWebErrorKind.SERVER_ERROR, e));
85 } finally {
86 synchronized (callPendingLock) {
87 for (var message : pendingPushMessages) {
88 try {
89 responseHandler.onResponse(message);
90 } catch (ResponseHandlerException | RuntimeException e) {
91 LOG.error("Error while flushing push message", e);
92 }
93 }
94 pendingPushMessages.clear();
95 callPending = false;
96 }
97 }
98 }
99
100 @Override
101 public void onPrecomputedServiceResult(String resourceId, String stateId, String serviceName,
102 IServiceResult serviceResult) throws ResponseHandlerException {
103 var message = new XtextWebPushMessage(resourceId, stateId, serviceName, serviceResult);
104 synchronized (callPendingLock) {
105 // If we're currently responding to a call we must delay any push messages until
106 // the reply is sent, because push messages relating to the new state id must be
107 // sent after the response with the new state id so that the client knows about
108 // the new state when it receives the push message.
109 if (callPending) {
110 pendingPushMessages.add(message);
111 } else {
112 responseHandler.onResponse(message);
113 }
114 }
115 }
116
117 @Override
118 public void onSubscribeToPrecomputationEvents(String resourceId, PushWebDocument document) {
119 PushWebDocument previousDocument = null;
120 var previousSubscription = subscriptions.get(resourceId);
121 if (previousSubscription != null) {
122 previousDocument = previousSubscription.get();
123 }
124 if (previousDocument == document) {
125 return;
126 }
127 if (previousDocument != null) {
128 previousDocument.removePrecomputationListener(this);
129 }
130 subscriptions.put(resourceId, new WeakReference<>(document));
131 }
132
133 /**
134 * Get the injector to satisfy the request in the {@code serviceContext}.
135 *
136 * Based on {@link org.eclipse.xtext.web.servlet.XtextServlet#getInjector}.
137 *
138 * @param serviceContext the Xtext service context of the request
139 * @return the injector for the Xtext language in the request
140 * @throws UnknownLanguageException if the Xtext language cannot be determined
141 */
142 protected Injector getInjector(IServiceContext context) {
143 IResourceServiceProvider resourceServiceProvider = null;
144 var resourceName = context.getParameter("resource");
145 if (resourceName == null) {
146 resourceName = "";
147 }
148 var emfURI = URI.createURI(resourceName);
149 var contentType = context.getParameter("contentType");
150 if (Strings.isNullOrEmpty(contentType)) {
151 resourceServiceProvider = resourceServiceProviderRegistry.getResourceServiceProvider(emfURI);
152 if (resourceServiceProvider == null) {
153 if (emfURI.toString().isEmpty()) {
154 throw new UnknownLanguageException(
155 "Unable to identify the Xtext language: missing parameter 'resource' or 'contentType'.");
156 } else {
157 throw new UnknownLanguageException(
158 "Unable to identify the Xtext language for resource " + emfURI + ".");
159 }
160 }
161 } else {
162 resourceServiceProvider = resourceServiceProviderRegistry.getResourceServiceProvider(emfURI, contentType);
163 if (resourceServiceProvider == null) {
164 throw new UnknownLanguageException(
165 "Unable to identify the Xtext language for contentType " + contentType + ".");
166 }
167 }
168 return resourceServiceProvider.get(Injector.class);
169 }
170
171 @Override
172 public void dispose() {
173 for (var subscription : subscriptions.values()) {
174 var document = subscription.get();
175 if (document != null) {
176 document.removePrecomputationListener(this);
177 }
178 }
179 }
180}
diff --git a/language-web/src/main/java/tools/refinery/language/web/xtext/server/message/XtextWebErrorKind.java b/language-web/src/main/java/tools/refinery/language/web/xtext/server/message/XtextWebErrorKind.java
deleted file mode 100644
index f74bae74..00000000
--- a/language-web/src/main/java/tools/refinery/language/web/xtext/server/message/XtextWebErrorKind.java
+++ /dev/null
@@ -1,11 +0,0 @@
1package tools.refinery.language.web.xtext.server.message;
2
3import com.google.gson.annotations.SerializedName;
4
5public enum XtextWebErrorKind {
6 @SerializedName("request")
7 REQUEST_ERROR,
8
9 @SerializedName("server")
10 SERVER_ERROR,
11}
diff --git a/language-web/src/main/java/tools/refinery/language/web/xtext/server/message/XtextWebErrorResponse.java b/language-web/src/main/java/tools/refinery/language/web/xtext/server/message/XtextWebErrorResponse.java
deleted file mode 100644
index 01d78c31..00000000
--- a/language-web/src/main/java/tools/refinery/language/web/xtext/server/message/XtextWebErrorResponse.java
+++ /dev/null
@@ -1,79 +0,0 @@
1package tools.refinery.language.web.xtext.server.message;
2
3import java.util.Objects;
4
5import com.google.gson.annotations.SerializedName;
6
7public final class XtextWebErrorResponse implements XtextWebResponse {
8 private String id;
9
10 @SerializedName("error")
11 private XtextWebErrorKind errorKind;
12
13 @SerializedName("message")
14 private String errorMessage;
15
16 public XtextWebErrorResponse(String id, XtextWebErrorKind errorKind, String errorMessage) {
17 super();
18 this.id = id;
19 this.errorKind = errorKind;
20 this.errorMessage = errorMessage;
21 }
22
23 public XtextWebErrorResponse(XtextWebRequest request, XtextWebErrorKind errorKind,
24 String errorMessage) {
25 this(request.getId(), errorKind, errorMessage);
26 }
27
28 public XtextWebErrorResponse(XtextWebRequest request, XtextWebErrorKind errorKind, Throwable t) {
29 this(request, errorKind, t.getMessage());
30 }
31
32 public String getId() {
33 return id;
34 }
35
36 public void setId(String id) {
37 this.id = id;
38 }
39
40 public XtextWebErrorKind getErrorKind() {
41 return errorKind;
42 }
43
44 public void setErrorKind(XtextWebErrorKind errorKind) {
45 this.errorKind = errorKind;
46 }
47
48 public String getErrorMessage() {
49 return errorMessage;
50 }
51
52 public void setErrorMessage(String errorMessage) {
53 this.errorMessage = errorMessage;
54 }
55
56 @Override
57 public int hashCode() {
58 return Objects.hash(errorKind, errorMessage, id);
59 }
60
61 @Override
62 public boolean equals(Object obj) {
63 if (this == obj)
64 return true;
65 if (obj == null)
66 return false;
67 if (getClass() != obj.getClass())
68 return false;
69 XtextWebErrorResponse other = (XtextWebErrorResponse) obj;
70 return errorKind == other.errorKind && Objects.equals(errorMessage, other.errorMessage)
71 && Objects.equals(id, other.id);
72 }
73
74 @Override
75 public String toString() {
76 return "XtextWebSocketErrorResponse [id=" + id + ", errorKind=" + errorKind + ", errorMessage=" + errorMessage
77 + "]";
78 }
79}
diff --git a/language-web/src/main/java/tools/refinery/language/web/xtext/server/message/XtextWebOkResponse.java b/language-web/src/main/java/tools/refinery/language/web/xtext/server/message/XtextWebOkResponse.java
deleted file mode 100644
index 8af27247..00000000
--- a/language-web/src/main/java/tools/refinery/language/web/xtext/server/message/XtextWebOkResponse.java
+++ /dev/null
@@ -1,72 +0,0 @@
1package tools.refinery.language.web.xtext.server.message;
2
3import java.util.Objects;
4
5import org.eclipse.xtext.web.server.IServiceResult;
6import org.eclipse.xtext.web.server.IUnwrappableServiceResult;
7
8import com.google.gson.annotations.SerializedName;
9
10public final class XtextWebOkResponse implements XtextWebResponse {
11 private String id;
12
13 @SerializedName("response")
14 private Object responseData;
15
16 public XtextWebOkResponse(String id, Object responseData) {
17 super();
18 this.id = id;
19 this.responseData = responseData;
20 }
21
22 public XtextWebOkResponse(XtextWebRequest request, IServiceResult result) {
23 this(request.getId(), maybeUnwrap(result));
24 }
25
26 public String getId() {
27 return id;
28 }
29
30 public void setId(String id) {
31 this.id = id;
32 }
33
34 public Object getResponseData() {
35 return responseData;
36 }
37
38 public void setResponseData(Object responseData) {
39 this.responseData = responseData;
40 }
41
42 @Override
43 public int hashCode() {
44 return Objects.hash(id, responseData);
45 }
46
47 @Override
48 public boolean equals(Object obj) {
49 if (this == obj)
50 return true;
51 if (obj == null)
52 return false;
53 if (getClass() != obj.getClass())
54 return false;
55 XtextWebOkResponse other = (XtextWebOkResponse) obj;
56 return Objects.equals(id, other.id) && Objects.equals(responseData, other.responseData);
57 }
58
59 @Override
60 public String toString() {
61 return "XtextWebSocketOkResponse [id=" + id + ", responseData=" + responseData + "]";
62 }
63
64 private static Object maybeUnwrap(IServiceResult result) {
65 if (result instanceof IUnwrappableServiceResult unwrappableServiceResult
66 && unwrappableServiceResult.getContent() != null) {
67 return unwrappableServiceResult.getContent();
68 } else {
69 return result;
70 }
71 }
72}
diff --git a/language-web/src/main/java/tools/refinery/language/web/xtext/server/message/XtextWebPushMessage.java b/language-web/src/main/java/tools/refinery/language/web/xtext/server/message/XtextWebPushMessage.java
deleted file mode 100644
index c9432e1c..00000000
--- a/language-web/src/main/java/tools/refinery/language/web/xtext/server/message/XtextWebPushMessage.java
+++ /dev/null
@@ -1,81 +0,0 @@
1package tools.refinery.language.web.xtext.server.message;
2
3import java.util.Objects;
4
5import com.google.gson.annotations.SerializedName;
6
7public final class XtextWebPushMessage implements XtextWebResponse {
8 @SerializedName("resource")
9 private String resourceId;
10
11 private String stateId;
12
13 private String service;
14
15 @SerializedName("push")
16 private Object pushData;
17
18 public XtextWebPushMessage(String resourceId, String stateId, String service, Object pushData) {
19 super();
20 this.resourceId = resourceId;
21 this.stateId = stateId;
22 this.service = service;
23 this.pushData = pushData;
24 }
25
26 public String getResourceId() {
27 return resourceId;
28 }
29
30 public void setResourceId(String resourceId) {
31 this.resourceId = resourceId;
32 }
33
34 public String getStateId() {
35 return stateId;
36 }
37
38 public void setStateId(String stateId) {
39 this.stateId = stateId;
40 }
41
42 public String getService() {
43 return service;
44 }
45
46 public void setService(String service) {
47 this.service = service;
48 }
49
50 public Object getPushData() {
51 return pushData;
52 }
53
54 public void setPushData(Object pushData) {
55 this.pushData = pushData;
56 }
57
58 @Override
59 public int hashCode() {
60 return Objects.hash(pushData, resourceId, service, stateId);
61 }
62
63 @Override
64 public boolean equals(Object obj) {
65 if (this == obj)
66 return true;
67 if (obj == null)
68 return false;
69 if (getClass() != obj.getClass())
70 return false;
71 XtextWebPushMessage other = (XtextWebPushMessage) obj;
72 return Objects.equals(pushData, other.pushData) && Objects.equals(resourceId, other.resourceId)
73 && Objects.equals(service, other.service) && Objects.equals(stateId, other.stateId);
74 }
75
76 @Override
77 public String toString() {
78 return "XtextWebPushMessage [resourceId=" + resourceId + ", stateId=" + stateId + ", service=" + service
79 + ", pushData=" + pushData + "]";
80 }
81}
diff --git a/language-web/src/main/java/tools/refinery/language/web/xtext/server/message/XtextWebRequest.java b/language-web/src/main/java/tools/refinery/language/web/xtext/server/message/XtextWebRequest.java
deleted file mode 100644
index 959749f8..00000000
--- a/language-web/src/main/java/tools/refinery/language/web/xtext/server/message/XtextWebRequest.java
+++ /dev/null
@@ -1,57 +0,0 @@
1package tools.refinery.language.web.xtext.server.message;
2
3import java.util.Map;
4import java.util.Objects;
5
6import com.google.gson.annotations.SerializedName;
7
8public class XtextWebRequest {
9 private String id;
10
11 @SerializedName("request")
12 private Map<String, String> requestData;
13
14 public XtextWebRequest(String id, Map<String, String> requestData) {
15 super();
16 this.id = id;
17 this.requestData = requestData;
18 }
19
20 public String getId() {
21 return id;
22 }
23
24 public void setId(String id) {
25 this.id = id;
26 }
27
28 public Map<String, String> getRequestData() {
29 return requestData;
30 }
31
32 public void setRequestData(Map<String, String> requestData) {
33 this.requestData = requestData;
34 }
35
36 @Override
37 public int hashCode() {
38 return Objects.hash(id, requestData);
39 }
40
41 @Override
42 public boolean equals(Object obj) {
43 if (this == obj)
44 return true;
45 if (obj == null)
46 return false;
47 if (getClass() != obj.getClass())
48 return false;
49 XtextWebRequest other = (XtextWebRequest) obj;
50 return Objects.equals(id, other.id) && Objects.equals(requestData, other.requestData);
51 }
52
53 @Override
54 public String toString() {
55 return "XtextWebSocketRequest [id=" + id + ", requestData=" + requestData + "]";
56 }
57}
diff --git a/language-web/src/main/java/tools/refinery/language/web/xtext/server/message/XtextWebResponse.java b/language-web/src/main/java/tools/refinery/language/web/xtext/server/message/XtextWebResponse.java
deleted file mode 100644
index 3bd13047..00000000
--- a/language-web/src/main/java/tools/refinery/language/web/xtext/server/message/XtextWebResponse.java
+++ /dev/null
@@ -1,4 +0,0 @@
1package tools.refinery.language.web.xtext.server.message;
2
3public sealed interface XtextWebResponse permits XtextWebOkResponse,XtextWebErrorResponse,XtextWebPushMessage {
4}
diff --git a/language-web/src/main/java/tools/refinery/language/web/xtext/server/push/PrecomputationListener.java b/language-web/src/main/java/tools/refinery/language/web/xtext/server/push/PrecomputationListener.java
deleted file mode 100644
index 79a284db..00000000
--- a/language-web/src/main/java/tools/refinery/language/web/xtext/server/push/PrecomputationListener.java
+++ /dev/null
@@ -1,15 +0,0 @@
1package tools.refinery.language.web.xtext.server.push;
2
3import org.eclipse.xtext.web.server.IServiceResult;
4
5import tools.refinery.language.web.xtext.server.ResponseHandlerException;
6
7@FunctionalInterface
8public interface PrecomputationListener {
9 void onPrecomputedServiceResult(String resourceId, String stateId, String serviceName, IServiceResult serviceResult)
10 throws ResponseHandlerException;
11
12 default void onSubscribeToPrecomputationEvents(String resourceId, PushWebDocument document) {
13 // Nothing to handle by default.
14 }
15}
diff --git a/language-web/src/main/java/tools/refinery/language/web/xtext/server/push/PushServiceDispatcher.java b/language-web/src/main/java/tools/refinery/language/web/xtext/server/push/PushServiceDispatcher.java
deleted file mode 100644
index c7b8108d..00000000
--- a/language-web/src/main/java/tools/refinery/language/web/xtext/server/push/PushServiceDispatcher.java
+++ /dev/null
@@ -1,23 +0,0 @@
1package tools.refinery.language.web.xtext.server.push;
2
3import org.eclipse.xtext.web.server.IServiceContext;
4import org.eclipse.xtext.web.server.XtextServiceDispatcher;
5import org.eclipse.xtext.web.server.model.XtextWebDocument;
6
7import com.google.inject.Singleton;
8
9import tools.refinery.language.web.xtext.server.SubscribingServiceContext;
10
11@Singleton
12public class PushServiceDispatcher extends XtextServiceDispatcher {
13
14 @Override
15 protected XtextWebDocument getFullTextDocument(String fullText, String resourceId, IServiceContext context) {
16 var document = super.getFullTextDocument(fullText, resourceId, context);
17 if (document instanceof PushWebDocument pushWebDocument
18 && context instanceof SubscribingServiceContext subscribingContext) {
19 pushWebDocument.addPrecomputationListener(subscribingContext.subscriber());
20 }
21 return document;
22 }
23}
diff --git a/language-web/src/main/java/tools/refinery/language/web/xtext/server/push/PushWebDocument.java b/language-web/src/main/java/tools/refinery/language/web/xtext/server/push/PushWebDocument.java
deleted file mode 100644
index 906b9e30..00000000
--- a/language-web/src/main/java/tools/refinery/language/web/xtext/server/push/PushWebDocument.java
+++ /dev/null
@@ -1,89 +0,0 @@
1package tools.refinery.language.web.xtext.server.push;
2
3import java.util.ArrayList;
4import java.util.HashMap;
5import java.util.List;
6import java.util.Map;
7
8import org.eclipse.xtext.util.CancelIndicator;
9import org.eclipse.xtext.web.server.IServiceResult;
10import org.eclipse.xtext.web.server.model.AbstractCachedService;
11import org.eclipse.xtext.web.server.model.DocumentSynchronizer;
12import org.eclipse.xtext.web.server.model.XtextWebDocument;
13import org.slf4j.Logger;
14import org.slf4j.LoggerFactory;
15
16import com.google.common.collect.ImmutableList;
17
18import tools.refinery.language.web.xtext.server.ResponseHandlerException;
19
20public class PushWebDocument extends XtextWebDocument {
21 private static final Logger LOG = LoggerFactory.getLogger(PushWebDocument.class);
22
23 private final List<PrecomputationListener> precomputationListeners = new ArrayList<>();
24
25 private final Map<Class<?>, IServiceResult> precomputedServices = new HashMap<>();
26
27 public PushWebDocument(String resourceId, DocumentSynchronizer synchronizer) {
28 super(resourceId, synchronizer);
29 if (resourceId == null) {
30 throw new IllegalArgumentException("resourceId must not be null");
31 }
32 }
33
34 public boolean addPrecomputationListener(PrecomputationListener listener) {
35 synchronized (precomputationListeners) {
36 if (precomputationListeners.contains(listener)) {
37 return false;
38 }
39 precomputationListeners.add(listener);
40 listener.onSubscribeToPrecomputationEvents(getResourceId(), this);
41 return true;
42 }
43 }
44
45 public boolean removePrecomputationListener(PrecomputationListener listener) {
46 synchronized (precomputationListeners) {
47 return precomputationListeners.remove(listener);
48 }
49 }
50
51 public <T extends IServiceResult> void precomputeServiceResult(AbstractCachedService<T> service, String serviceName,
52 CancelIndicator cancelIndicator, boolean logCacheMiss) {
53 var result = getCachedServiceResult(service, cancelIndicator, logCacheMiss);
54 if (result == null) {
55 LOG.error("{} service returned null result", serviceName);
56 return;
57 }
58 var serviceClass = service.getClass();
59 var previousResult = precomputedServices.get(serviceClass);
60 if (previousResult != null && previousResult.equals(result)) {
61 return;
62 }
63 precomputedServices.put(serviceClass, result);
64 notifyPrecomputationListeners(serviceName, result);
65 }
66
67 private <T extends IServiceResult> void notifyPrecomputationListeners(String serviceName, T result) {
68 var resourceId = getResourceId();
69 var stateId = getStateId();
70 List<PrecomputationListener> copyOfListeners;
71 synchronized (precomputationListeners) {
72 copyOfListeners = ImmutableList.copyOf(precomputationListeners);
73 }
74 var toRemove = new ArrayList<PrecomputationListener>();
75 for (var listener : copyOfListeners) {
76 try {
77 listener.onPrecomputedServiceResult(resourceId, stateId, serviceName, result);
78 } catch (ResponseHandlerException e) {
79 LOG.error("Delivering precomputation push message failed", e);
80 toRemove.add(listener);
81 }
82 }
83 if (!toRemove.isEmpty()) {
84 synchronized (precomputationListeners) {
85 precomputationListeners.removeAll(toRemove);
86 }
87 }
88 }
89}
diff --git a/language-web/src/main/java/tools/refinery/language/web/xtext/server/push/PushWebDocumentAccess.java b/language-web/src/main/java/tools/refinery/language/web/xtext/server/push/PushWebDocumentAccess.java
deleted file mode 100644
index b3666a86..00000000
--- a/language-web/src/main/java/tools/refinery/language/web/xtext/server/push/PushWebDocumentAccess.java
+++ /dev/null
@@ -1,68 +0,0 @@
1package tools.refinery.language.web.xtext.server.push;
2
3import org.eclipse.xtext.service.OperationCanceledManager;
4import org.eclipse.xtext.util.CancelIndicator;
5import org.eclipse.xtext.util.concurrent.CancelableUnitOfWork;
6import org.eclipse.xtext.web.server.IServiceResult;
7import org.eclipse.xtext.web.server.model.AbstractCachedService;
8import org.eclipse.xtext.web.server.model.IXtextWebDocument;
9import org.eclipse.xtext.web.server.model.PrecomputedServiceRegistry;
10import org.eclipse.xtext.web.server.model.XtextWebDocument;
11import org.eclipse.xtext.web.server.model.XtextWebDocumentAccess;
12import org.eclipse.xtext.web.server.syntaxcoloring.HighlightingService;
13import org.eclipse.xtext.web.server.validation.ValidationService;
14
15import com.google.inject.Inject;
16
17public class PushWebDocumentAccess extends XtextWebDocumentAccess {
18
19 @Inject
20 private PrecomputedServiceRegistry preComputedServiceRegistry;
21
22 @Inject
23 private OperationCanceledManager operationCanceledManager;
24
25 private PushWebDocument pushDocument;
26
27 @Override
28 protected void init(XtextWebDocument document, String requiredStateId, boolean skipAsyncWork) {
29 super.init(document, requiredStateId, skipAsyncWork);
30 if (document instanceof PushWebDocument newPushDocument) {
31 pushDocument = newPushDocument;
32 }
33 }
34
35 @Override
36 protected void performPrecomputation(CancelIndicator cancelIndicator) {
37 if (pushDocument == null) {
38 super.performPrecomputation(cancelIndicator);
39 return;
40 }
41 for (AbstractCachedService<? extends IServiceResult> service : preComputedServiceRegistry
42 .getPrecomputedServices()) {
43 operationCanceledManager.checkCanceled(cancelIndicator);
44 precomputeServiceResult(service, false);
45 }
46 }
47
48 protected <T extends IServiceResult> void precomputeServiceResult(AbstractCachedService<T> service, boolean logCacheMiss) {
49 var serviceName = getPrecomputedServiceName(service);
50 readOnly(new CancelableUnitOfWork<Void, IXtextWebDocument>() {
51 @Override
52 public java.lang.Void exec(IXtextWebDocument d, CancelIndicator cancelIndicator) throws Exception {
53 pushDocument.precomputeServiceResult(service, serviceName, cancelIndicator, logCacheMiss);
54 return null;
55 }
56 });
57 }
58
59 protected String getPrecomputedServiceName(AbstractCachedService<? extends IServiceResult> service) {
60 if (service instanceof ValidationService) {
61 return "validate";
62 }
63 if (service instanceof HighlightingService) {
64 return "highlight";
65 }
66 throw new IllegalArgumentException("Unknown precomputed service: " + service);
67 }
68}
diff --git a/language-web/src/main/java/tools/refinery/language/web/xtext/server/push/PushWebDocumentProvider.java b/language-web/src/main/java/tools/refinery/language/web/xtext/server/push/PushWebDocumentProvider.java
deleted file mode 100644
index fc45f74a..00000000
--- a/language-web/src/main/java/tools/refinery/language/web/xtext/server/push/PushWebDocumentProvider.java
+++ /dev/null
@@ -1,33 +0,0 @@
1package tools.refinery.language.web.xtext.server.push;
2
3import org.eclipse.xtext.web.server.IServiceContext;
4import org.eclipse.xtext.web.server.model.DocumentSynchronizer;
5import org.eclipse.xtext.web.server.model.IWebDocumentProvider;
6import org.eclipse.xtext.web.server.model.XtextWebDocument;
7
8import com.google.inject.Inject;
9import com.google.inject.Provider;
10import com.google.inject.Singleton;
11
12/**
13 * Based on
14 * {@link org.eclipse.xtext.web.server.model.IWebDocumentProvider.DefaultImpl}.
15 *
16 * @author Kristóf Marussy
17 */
18@Singleton
19public class PushWebDocumentProvider implements IWebDocumentProvider {
20 @Inject
21 private Provider<DocumentSynchronizer> synchronizerProvider;
22
23 @Override
24 public XtextWebDocument get(String resourceId, IServiceContext serviceContext) {
25 if (resourceId == null) {
26 return new XtextWebDocument(resourceId, synchronizerProvider.get());
27 } else {
28 // We only need to send push messages if a resourceId is specified.
29 return new PushWebDocument(resourceId,
30 serviceContext.getSession().get(DocumentSynchronizer.class, () -> this.synchronizerProvider.get()));
31 }
32 }
33}
diff --git a/language-web/src/main/java/tools/refinery/language/web/xtext/servlet/SimpleServiceContext.java b/language-web/src/main/java/tools/refinery/language/web/xtext/servlet/SimpleServiceContext.java
deleted file mode 100644
index 43e37160..00000000
--- a/language-web/src/main/java/tools/refinery/language/web/xtext/servlet/SimpleServiceContext.java
+++ /dev/null
@@ -1,26 +0,0 @@
1package tools.refinery.language.web.xtext.servlet;
2
3import java.util.Map;
4import java.util.Set;
5
6import org.eclipse.xtext.web.server.IServiceContext;
7import org.eclipse.xtext.web.server.ISession;
8
9import com.google.common.collect.ImmutableSet;
10
11public record SimpleServiceContext(ISession session, Map<String, String> parameters) implements IServiceContext {
12 @Override
13 public Set<String> getParameterKeys() {
14 return ImmutableSet.copyOf(parameters.keySet());
15 }
16
17 @Override
18 public String getParameter(String key) {
19 return parameters.get(key);
20 }
21
22 @Override
23 public ISession getSession() {
24 return session;
25 }
26}
diff --git a/language-web/src/main/java/tools/refinery/language/web/xtext/servlet/SimpleSession.java b/language-web/src/main/java/tools/refinery/language/web/xtext/servlet/SimpleSession.java
deleted file mode 100644
index 09c055a2..00000000
--- a/language-web/src/main/java/tools/refinery/language/web/xtext/servlet/SimpleSession.java
+++ /dev/null
@@ -1,35 +0,0 @@
1package tools.refinery.language.web.xtext.servlet;
2
3import java.util.HashMap;
4import java.util.Map;
5
6import org.eclipse.xtext.web.server.ISession;
7import org.eclipse.xtext.xbase.lib.Functions.Function0;
8
9public class SimpleSession implements ISession {
10 private Map<Object, Object> map = new HashMap<>();
11
12 @Override
13 public <T> T get(Object key) {
14 @SuppressWarnings("unchecked")
15 var value = (T) map.get(key);
16 return value;
17 }
18
19 @Override
20 public <T> T get(Object key, Function0<? extends T> factory) {
21 @SuppressWarnings("unchecked")
22 var value = (T) map.computeIfAbsent(key, absentKey -> factory.apply());
23 return value;
24 }
25
26 @Override
27 public void put(Object key, Object value) {
28 map.put(key, value);
29 }
30
31 @Override
32 public void remove(Object key) {
33 map.remove(key);
34 }
35}
diff --git a/language-web/src/main/java/tools/refinery/language/web/xtext/servlet/XtextStatusCode.java b/language-web/src/main/java/tools/refinery/language/web/xtext/servlet/XtextStatusCode.java
deleted file mode 100644
index 0cd229e8..00000000
--- a/language-web/src/main/java/tools/refinery/language/web/xtext/servlet/XtextStatusCode.java
+++ /dev/null
@@ -1,9 +0,0 @@
1package tools.refinery.language.web.xtext.servlet;
2
3public final class XtextStatusCode {
4 public static final int INVALID_JSON = 4007;
5
6 private XtextStatusCode() {
7 throw new IllegalStateException("This is a static utility class and should not be instantiated directly");
8 }
9}
diff --git a/language-web/src/main/java/tools/refinery/language/web/xtext/servlet/XtextWebSocket.java b/language-web/src/main/java/tools/refinery/language/web/xtext/servlet/XtextWebSocket.java
deleted file mode 100644
index fd41f213..00000000
--- a/language-web/src/main/java/tools/refinery/language/web/xtext/servlet/XtextWebSocket.java
+++ /dev/null
@@ -1,133 +0,0 @@
1package tools.refinery.language.web.xtext.servlet;
2
3import java.io.IOException;
4import java.io.Reader;
5
6import org.eclipse.jetty.websocket.api.Session;
7import org.eclipse.jetty.websocket.api.StatusCode;
8import org.eclipse.jetty.websocket.api.WriteCallback;
9import org.eclipse.jetty.websocket.api.annotations.OnWebSocketClose;
10import org.eclipse.jetty.websocket.api.annotations.OnWebSocketConnect;
11import org.eclipse.jetty.websocket.api.annotations.OnWebSocketError;
12import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage;
13import org.eclipse.jetty.websocket.api.annotations.WebSocket;
14import org.eclipse.xtext.resource.IResourceServiceProvider;
15import org.eclipse.xtext.web.server.ISession;
16import org.slf4j.Logger;
17import org.slf4j.LoggerFactory;
18
19import com.google.gson.Gson;
20import com.google.gson.JsonIOException;
21import com.google.gson.JsonParseException;
22
23import tools.refinery.language.web.xtext.server.ResponseHandler;
24import tools.refinery.language.web.xtext.server.ResponseHandlerException;
25import tools.refinery.language.web.xtext.server.TransactionExecutor;
26import tools.refinery.language.web.xtext.server.message.XtextWebRequest;
27import tools.refinery.language.web.xtext.server.message.XtextWebResponse;
28
29@WebSocket
30public class XtextWebSocket implements WriteCallback, ResponseHandler {
31 private static final Logger LOG = LoggerFactory.getLogger(XtextWebSocket.class);
32
33 private final Gson gson = new Gson();
34
35 private final TransactionExecutor executor;
36
37 private Session webSocketSession;
38
39 public XtextWebSocket(TransactionExecutor executor) {
40 this.executor = executor;
41 executor.setResponseHandler(this);
42 }
43
44 public XtextWebSocket(ISession session, IResourceServiceProvider.Registry resourceServiceProviderRegistry) {
45 this(new TransactionExecutor(session, resourceServiceProviderRegistry));
46 }
47
48 @OnWebSocketConnect
49 public void onConnect(Session webSocketSession) {
50 if (this.webSocketSession != null) {
51 LOG.error("Websocket session onConnect when already connected");
52 return;
53 }
54 LOG.debug("New websocket connection from {}", webSocketSession.getRemoteAddress());
55 this.webSocketSession = webSocketSession;
56 }
57
58 @OnWebSocketClose
59 public void onClose(int statusCode, String reason) {
60 executor.dispose();
61 if (webSocketSession == null) {
62 return;
63 }
64 if (statusCode == StatusCode.NORMAL || statusCode == StatusCode.SHUTDOWN) {
65 LOG.debug("{} closed connection normally: {}", webSocketSession.getRemoteAddress(), reason);
66 } else {
67 LOG.warn("{} closed connection with status code {}: {}", webSocketSession.getRemoteAddress(), statusCode,
68 reason);
69 }
70 webSocketSession = null;
71 }
72
73 @OnWebSocketError
74 public void onError(Throwable error) {
75 if (webSocketSession == null) {
76 return;
77 }
78 LOG.error("Internal websocket error in connection from" + webSocketSession.getRemoteAddress(), error);
79 }
80
81 @OnWebSocketMessage
82 public void onMessage(Reader reader) {
83 if (webSocketSession == null) {
84 LOG.error("Trying to receive message when websocket is disconnected");
85 return;
86 }
87 XtextWebRequest request;
88 try {
89 request = gson.fromJson(reader, XtextWebRequest.class);
90 } catch (JsonIOException e) {
91 LOG.error("Cannot read from websocket from" + webSocketSession.getRemoteAddress(), e);
92 if (webSocketSession.isOpen()) {
93 webSocketSession.close(StatusCode.SERVER_ERROR, "Cannot read payload");
94 }
95 return;
96 } catch (JsonParseException e) {
97 LOG.warn("Malformed websocket request from" + webSocketSession.getRemoteAddress(), e);
98 webSocketSession.close(XtextStatusCode.INVALID_JSON, "Invalid JSON payload");
99 return;
100 }
101 try {
102 executor.handleRequest(request);
103 } catch (ResponseHandlerException e) {
104 LOG.warn("Cannot write websocket response", e);
105 if (webSocketSession.isOpen()) {
106 webSocketSession.close(StatusCode.SERVER_ERROR, "Cannot write response");
107 }
108 }
109 }
110
111 @Override
112 public void onResponse(XtextWebResponse response) throws ResponseHandlerException {
113 if (webSocketSession == null) {
114 throw new ResponseHandlerException("Trying to send message when websocket is disconnected");
115 }
116 var responseString = gson.toJson(response);
117 try {
118 webSocketSession.getRemote().sendPartialString(responseString, true, this);
119 } catch (IOException e) {
120 throw new ResponseHandlerException(
121 "Cannot initiaite async write to websocket " + webSocketSession.getRemoteAddress(), e);
122 }
123 }
124
125 @Override
126 public void writeFailed(Throwable x) {
127 if (webSocketSession == null) {
128 LOG.error("Cannot complete async write to disconnected websocket", x);
129 return;
130 }
131 LOG.warn("Cannot complete async write to websocket " + webSocketSession.getRemoteAddress(), x);
132 }
133}
diff --git a/language-web/src/main/java/tools/refinery/language/web/xtext/servlet/XtextWebSocketServlet.java b/language-web/src/main/java/tools/refinery/language/web/xtext/servlet/XtextWebSocketServlet.java
deleted file mode 100644
index 942ca380..00000000
--- a/language-web/src/main/java/tools/refinery/language/web/xtext/servlet/XtextWebSocketServlet.java
+++ /dev/null
@@ -1,83 +0,0 @@
1package tools.refinery.language.web.xtext.servlet;
2
3import java.io.IOException;
4import java.time.Duration;
5import java.util.Set;
6
7import org.eclipse.jetty.websocket.server.JettyServerUpgradeRequest;
8import org.eclipse.jetty.websocket.server.JettyServerUpgradeResponse;
9import org.eclipse.jetty.websocket.server.JettyWebSocketCreator;
10import org.eclipse.jetty.websocket.server.JettyWebSocketServlet;
11import org.eclipse.jetty.websocket.server.JettyWebSocketServletFactory;
12import org.eclipse.xtext.resource.IResourceServiceProvider;
13import org.slf4j.Logger;
14import org.slf4j.LoggerFactory;
15
16import jakarta.servlet.ServletConfig;
17import jakarta.servlet.ServletException;
18
19public abstract class XtextWebSocketServlet extends JettyWebSocketServlet implements JettyWebSocketCreator {
20
21 private static final long serialVersionUID = -3772740838165122685L;
22
23 public static final String ALLOWED_ORIGINS_SEPARATOR = ";";
24
25 public static final String ALLOWED_ORIGINS_INIT_PARAM = "tools.refinery.language.web.xtext.XtextWebSocketServlet.allowedOrigin";
26
27 public static final String XTEXT_SUBPROTOCOL_V1 = "tools.refinery.language.web.xtext.v1";
28
29 /**
30 * Maximum message size should be large enough to upload a full model file.
31 */
32 private static final long MAX_FRAME_SIZE = 4L * 1024L * 1024L;
33
34 private static final Duration IDLE_TIMEOUT = Duration.ofSeconds(30);
35
36 private transient Logger log = LoggerFactory.getLogger(getClass());
37
38 private transient Set<String> allowedOrigins = null;
39
40 @Override
41 public void init(ServletConfig config) throws ServletException {
42 var allowedOriginsStr = config.getInitParameter(ALLOWED_ORIGINS_INIT_PARAM);
43 if (allowedOriginsStr == null) {
44 log.warn("All WebSocket origins are allowed! This setting should not be used in production!");
45 } else {
46 allowedOrigins = Set.of(allowedOriginsStr.split(ALLOWED_ORIGINS_SEPARATOR));
47 log.info("Allowed origins: {}", allowedOrigins);
48 }
49 super.init(config);
50 }
51
52 @Override
53 protected void configure(JettyWebSocketServletFactory factory) {
54 factory.setMaxFrameSize(MAX_FRAME_SIZE);
55 factory.setIdleTimeout(IDLE_TIMEOUT);
56 factory.addMapping("/", this);
57 }
58
59 @Override
60 public Object createWebSocket(JettyServerUpgradeRequest req, JettyServerUpgradeResponse resp) {
61 if (allowedOrigins != null) {
62 var origin = req.getOrigin();
63 if (origin == null || !allowedOrigins.contains(origin.toLowerCase())) {
64 log.error("Connection from {} from forbidden origin {}", req.getRemoteSocketAddress(), origin);
65 try {
66 resp.sendForbidden("Origin not allowed");
67 } catch (IOException e) {
68 log.error("Cannot send forbidden origin error", e);
69 }
70 return null;
71 }
72 }
73 if (req.getSubProtocols().contains(XTEXT_SUBPROTOCOL_V1)) {
74 resp.setAcceptedSubProtocol(XTEXT_SUBPROTOCOL_V1);
75 } else {
76 log.error("None of the subprotocols {} offered by {} are supported", req.getSubProtocols(),
77 req.getRemoteSocketAddress());
78 resp.setAcceptedSubProtocol(null);
79 }
80 var session = new SimpleSession();
81 return new XtextWebSocket(session, IResourceServiceProvider.Registry.INSTANCE);
82 }
83}