aboutsummaryrefslogtreecommitdiffstats
path: root/language-web
diff options
context:
space:
mode:
authorLibravatar Kristóf Marussy <kristof@marussy.com>2021-10-11 20:53:54 +0200
committerLibravatar Kristóf Marussy <kristof@marussy.com>2021-10-31 19:26:10 +0100
commit7e9289a754e1829ebb04efcce0a4a2d52a22b122 (patch)
tree9445ffd0f67f7e4d804acd1a7c17a67b9016d461 /language-web
parentfeat(web): simplify contextual parsing (diff)
downloadrefinery-7e9289a754e1829ebb04efcce0a4a2d52a22b122.tar.gz
refinery-7e9289a754e1829ebb04efcce0a4a2d52a22b122.tar.zst
refinery-7e9289a754e1829ebb04efcce0a4a2d52a22b122.zip
feat(web): add websocket server
Diffstat (limited to 'language-web')
-rw-r--r--language-web/build.gradle1
-rw-r--r--language-web/src/main/java/tools/refinery/language/web/CacheControlFilter.java21
-rw-r--r--language-web/src/main/java/tools/refinery/language/web/ProblemWebSocketServlet.java (renamed from language-web/src/main/java/tools/refinery/language/web/ProblemServlet.java)13
-rw-r--r--language-web/src/main/java/tools/refinery/language/web/ServerLauncher.java69
-rw-r--r--language-web/src/main/java/tools/refinery/language/web/xtext/HttpServiceContext.java107
-rw-r--r--language-web/src/main/java/tools/refinery/language/web/xtext/HttpSessionWrapper.java53
-rw-r--r--language-web/src/main/java/tools/refinery/language/web/xtext/SimpleServiceContext.java27
-rw-r--r--language-web/src/main/java/tools/refinery/language/web/xtext/SimpleSession.java35
-rw-r--r--language-web/src/main/java/tools/refinery/language/web/xtext/XtextServlet.java196
-rw-r--r--language-web/src/main/java/tools/refinery/language/web/xtext/XtextStatusCode.java9
-rw-r--r--language-web/src/main/java/tools/refinery/language/web/xtext/XtextWebSocket.java137
-rw-r--r--language-web/src/main/java/tools/refinery/language/web/xtext/XtextWebSocketErrorResponse.java52
-rw-r--r--language-web/src/main/java/tools/refinery/language/web/xtext/XtextWebSocketOkResponse.java52
-rw-r--r--language-web/src/main/java/tools/refinery/language/web/xtext/XtextWebSocketRequest.java51
-rw-r--r--language-web/src/main/java/tools/refinery/language/web/xtext/XtextWebSocketResponse.java7
-rw-r--r--language-web/src/main/java/tools/refinery/language/web/xtext/XtextWebSocketServlet.java75
16 files changed, 520 insertions, 385 deletions
diff --git a/language-web/build.gradle b/language-web/build.gradle
index f2b9b43f..df23bc91 100644
--- a/language-web/build.gradle
+++ b/language-web/build.gradle
@@ -8,6 +8,7 @@ dependencies {
8 implementation "org.eclipse.xtend:org.eclipse.xtend.lib:${xtextVersion}" 8 implementation "org.eclipse.xtend:org.eclipse.xtend.lib:${xtextVersion}"
9 implementation "org.eclipse.jetty:jetty-server:${jettyVersion}" 9 implementation "org.eclipse.jetty:jetty-server:${jettyVersion}"
10 implementation "org.eclipse.jetty:jetty-servlet:${jettyVersion}" 10 implementation "org.eclipse.jetty:jetty-servlet:${jettyVersion}"
11 implementation "org.eclipse.jetty.websocket:websocket-jetty-server:${jettyVersion}"
11 implementation "org.slf4j:slf4j-simple:${slf4JVersion}" 12 implementation "org.slf4j:slf4j-simple:${slf4JVersion}"
12 implementation "org.slf4j:log4j-over-slf4j:${slf4JVersion}" 13 implementation "org.slf4j:log4j-over-slf4j:${slf4JVersion}"
13} 14}
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
index cf4c00fa..b13ae95d 100644
--- a/language-web/src/main/java/tools/refinery/language/web/CacheControlFilter.java
+++ b/language-web/src/main/java/tools/refinery/language/web/CacheControlFilter.java
@@ -1,8 +1,11 @@
1package tools.refinery.language.web; 1package tools.refinery.language.web;
2 2
3import java.io.IOException; 3import java.io.IOException;
4import java.time.Duration;
4import java.util.regex.Pattern; 5import java.util.regex.Pattern;
5 6
7import org.eclipse.jetty.http.HttpHeader;
8
6import jakarta.servlet.Filter; 9import jakarta.servlet.Filter;
7import jakarta.servlet.FilterChain; 10import jakarta.servlet.FilterChain;
8import jakarta.servlet.FilterConfig; 11import jakarta.servlet.FilterConfig;
@@ -13,16 +16,11 @@ import jakarta.servlet.http.HttpServletRequest;
13import jakarta.servlet.http.HttpServletResponse; 16import jakarta.servlet.http.HttpServletResponse;
14 17
15public class CacheControlFilter implements Filter { 18public class CacheControlFilter implements Filter {
16
17 private static final String CACHE_CONTROL_HEADER = "Cache-Control";
18
19 private static final String EXPIRES_HEADER = "Expires";
20
21 private static final Pattern CACHE_URI_PATTERN = Pattern.compile(".*\\.(css|gif|js|map|png|svg|woff2)"); 19 private static final Pattern CACHE_URI_PATTERN = Pattern.compile(".*\\.(css|gif|js|map|png|svg|woff2)");
22 20
23 private static final long EXPIRY = 31536000; 21 private static final Duration EXPIRY = Duration.ofDays(365);
24 22
25 private static final String CACHE_CONTROL_CACHE_VALUE = "public, max-age: " + EXPIRY + ", immutable"; 23 private static final String CACHE_CONTROL_CACHE_VALUE = "public, max-age: " + EXPIRY.toSeconds() + ", immutable";
26 24
27 private static final String CACHE_CONTROL_NO_CACHE_VALUE = "no-cache, no-store, max-age: 0, must-revalidate"; 25 private static final String CACHE_CONTROL_NO_CACHE_VALUE = "no-cache, no-store, max-age: 0, must-revalidate";
28 26
@@ -36,11 +34,12 @@ public class CacheControlFilter implements Filter {
36 throws IOException, ServletException { 34 throws IOException, ServletException {
37 if (request instanceof HttpServletRequest httpRequest && response instanceof HttpServletResponse httpResponse) { 35 if (request instanceof HttpServletRequest httpRequest && response instanceof HttpServletResponse httpResponse) {
38 if (CACHE_URI_PATTERN.matcher(httpRequest.getRequestURI()).matches()) { 36 if (CACHE_URI_PATTERN.matcher(httpRequest.getRequestURI()).matches()) {
39 httpResponse.setHeader(CACHE_CONTROL_HEADER, CACHE_CONTROL_CACHE_VALUE); 37 httpResponse.setHeader(HttpHeader.CACHE_CONTROL.asString(), CACHE_CONTROL_CACHE_VALUE);
40 httpResponse.setDateHeader(EXPIRES_HEADER, System.currentTimeMillis() + EXPIRY * 1000L); 38 httpResponse.setDateHeader(HttpHeader.EXPIRES.asString(),
39 System.currentTimeMillis() + EXPIRY.toMillis());
41 } else { 40 } else {
42 httpResponse.setHeader(CACHE_CONTROL_HEADER, CACHE_CONTROL_NO_CACHE_VALUE); 41 httpResponse.setHeader(HttpHeader.CACHE_CONTROL.asString(), CACHE_CONTROL_NO_CACHE_VALUE);
43 httpResponse.setDateHeader(EXPIRES_HEADER, 0); 42 httpResponse.setDateHeader(HttpHeader.EXPIRES.asString(), 0);
44 } 43 }
45 } 44 }
46 chain.doFilter(request, response); 45 chain.doFilter(request, response);
diff --git a/language-web/src/main/java/tools/refinery/language/web/ProblemServlet.java b/language-web/src/main/java/tools/refinery/language/web/ProblemWebSocketServlet.java
index 49457002..9ffd6557 100644
--- a/language-web/src/main/java/tools/refinery/language/web/ProblemServlet.java
+++ b/language-web/src/main/java/tools/refinery/language/web/ProblemWebSocketServlet.java
@@ -1,19 +1,13 @@
1/*
2 * generated by Xtext 2.25.0
3 */
4package tools.refinery.language.web; 1package tools.refinery.language.web;
5 2
6import org.eclipse.xtext.util.DisposableRegistry; 3import org.eclipse.xtext.util.DisposableRegistry;
7 4
8import jakarta.servlet.ServletException; 5import jakarta.servlet.ServletException;
9import tools.refinery.language.web.xtext.XtextServlet; 6import tools.refinery.language.web.xtext.XtextWebSocketServlet;
10 7
11/** 8public class ProblemWebSocketServlet extends XtextWebSocketServlet {
12 * Deploy this class into a servlet container to enable DSL-specific services.
13 */
14public class ProblemServlet extends XtextServlet {
15 9
16 private static final long serialVersionUID = -9204695886561362912L; 10 private static final long serialVersionUID = -7040955470384797008L;
17 11
18 private transient DisposableRegistry disposableRegistry; 12 private transient DisposableRegistry disposableRegistry;
19 13
@@ -32,5 +26,4 @@ public class ProblemServlet extends XtextServlet {
32 } 26 }
33 super.destroy(); 27 super.destroy();
34 } 28 }
35
36} 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
index c253422b..0942b680 100644
--- a/language-web/src/main/java/tools/refinery/language/web/ServerLauncher.java
+++ b/language-web/src/main/java/tools/refinery/language/web/ServerLauncher.java
@@ -9,6 +9,7 @@ import java.net.InetSocketAddress;
9import java.net.URI; 9import java.net.URI;
10import java.net.URISyntaxException; 10import java.net.URISyntaxException;
11import java.util.EnumSet; 11import java.util.EnumSet;
12import java.util.Optional;
12import java.util.Set; 13import java.util.Set;
13 14
14import org.eclipse.jetty.server.Server; 15import org.eclipse.jetty.server.Server;
@@ -22,24 +23,28 @@ import org.slf4j.LoggerFactory;
22 23
23import jakarta.servlet.DispatcherType; 24import jakarta.servlet.DispatcherType;
24import jakarta.servlet.SessionTrackingMode; 25import jakarta.servlet.SessionTrackingMode;
26import tools.refinery.language.web.xtext.XtextWebSocketServlet;
25 27
26public class ServerLauncher { 28public class ServerLauncher {
27 public static final String DEFAULT_LISTEN_ADDRESS = "localhost"; 29 public static final String DEFAULT_LISTEN_ADDRESS = "localhost";
28 30
29 public static final int DEFAULT_LISTEN_PORT = 1312; 31 public static final int DEFAULT_LISTEN_PORT = 1312;
30 32
31 // Use this cookie name for load balancing. 33 public static final int DEFAULT_PUBLIC_PORT = 443;
32 public static final String SESSION_COOKIE_NAME = "JSESSIONID"; 34
35 public static final int HTTP_DEFAULT_PORT = 80;
36
37 public static final int HTTPS_DEFAULT_PORT = 443;
33 38
34 private static final Logger LOG = LoggerFactory.getLogger(ServerLauncher.class); 39 private static final Logger LOG = LoggerFactory.getLogger(ServerLauncher.class);
35 40
36 private final Server server; 41 private final Server server;
37 42
38 public ServerLauncher(InetSocketAddress bindAddress, Resource baseResource) { 43 public ServerLauncher(InetSocketAddress bindAddress, Resource baseResource, Optional<String[]> allowedOrigins) {
39 server = new Server(bindAddress); 44 server = new Server(bindAddress);
40 var handler = new ServletContextHandler(); 45 var handler = new ServletContextHandler();
41 addSessionHandler(handler); 46 addSessionHandler(handler);
42 addProblemServlet(handler); 47 addProblemServlet(handler, allowedOrigins);
43 if (baseResource != null) { 48 if (baseResource != null) {
44 handler.setBaseResource(baseResource); 49 handler.setBaseResource(baseResource);
45 handler.setWelcomeFiles(new String[] { "index.html" }); 50 handler.setWelcomeFiles(new String[] { "index.html" });
@@ -52,12 +57,20 @@ public class ServerLauncher {
52 private void addSessionHandler(ServletContextHandler handler) { 57 private void addSessionHandler(ServletContextHandler handler) {
53 var sessionHandler = new SessionHandler(); 58 var sessionHandler = new SessionHandler();
54 sessionHandler.setSessionTrackingModes(Set.of(SessionTrackingMode.COOKIE)); 59 sessionHandler.setSessionTrackingModes(Set.of(SessionTrackingMode.COOKIE));
55 sessionHandler.setSessionCookie(SESSION_COOKIE_NAME);
56 handler.setSessionHandler(sessionHandler); 60 handler.setSessionHandler(sessionHandler);
57 } 61 }
58 62
59 private void addProblemServlet(ServletContextHandler handler) { 63 private void addProblemServlet(ServletContextHandler handler, Optional<String[]> allowedOrigins) {
60 handler.addServlet(ProblemServlet.class, "/xtext-service/*"); 64 var problemServletHolder = new ServletHolder(ProblemWebSocketServlet.class);
65 if (allowedOrigins.isEmpty()) {
66 LOG.warn("All WebSocket origins are allowed! This setting should not be used in production!");
67 } else {
68 var allowedOriginsString = String.join(XtextWebSocketServlet.ALLOWED_ORIGINS_SEPARATOR,
69 allowedOrigins.get());
70 problemServletHolder.setInitParameter(XtextWebSocketServlet.ALLOWED_ORIGINS_INIT_PARAM,
71 allowedOriginsString);
72 }
73 handler.addServlet(problemServletHolder, "/xtext-service/*");
61 } 74 }
62 75
63 private void addDefaultServlet(ServletContextHandler handler) { 76 private void addDefaultServlet(ServletContextHandler handler) {
@@ -80,7 +93,8 @@ public class ServerLauncher {
80 try { 93 try {
81 var bindAddress = getBindAddress(); 94 var bindAddress = getBindAddress();
82 var baseResource = getBaseResource(); 95 var baseResource = getBaseResource();
83 var serverLauncher = new ServerLauncher(bindAddress, baseResource); 96 var allowedOrigins = getAllowedOrigins();
97 var serverLauncher = new ServerLauncher(bindAddress, baseResource, allowedOrigins);
84 serverLauncher.start(); 98 serverLauncher.start();
85 } catch (Exception exception) { 99 } catch (Exception exception) {
86 LOG.error("Fatal server error", exception); 100 LOG.error("Fatal server error", exception);
@@ -132,4 +146,43 @@ public class ServerLauncher {
132 // Fall back to just serving a 404. 146 // Fall back to just serving a 404.
133 return null; 147 return null;
134 } 148 }
149
150 private static String getPublicHost() {
151 var publicHost = System.getenv("PUBLIC_HOST");
152 if (publicHost != null) {
153 return publicHost.toLowerCase();
154 }
155 return null;
156 }
157
158 private static int getPublicPort() {
159 var portStr = System.getenv("PUBLIC_PORT");
160 if (portStr != null) {
161 return Integer.parseInt(portStr);
162 }
163 return DEFAULT_LISTEN_PORT;
164 }
165
166 private static Optional<String[]> getAllowedOrigins() {
167 var allowedOrigins = System.getenv("ALLOWED_ORIGINS");
168 if (allowedOrigins != null) {
169 return Optional.of(allowedOrigins.split(XtextWebSocketServlet.ALLOWED_ORIGINS_SEPARATOR));
170 }
171 return getAllowedOriginsFromPublicHostAndPort();
172 }
173
174 private static Optional<String[]> getAllowedOriginsFromPublicHostAndPort() {
175 var publicHost = getPublicHost();
176 if (publicHost == null) {
177 return Optional.empty();
178 }
179 int publicPort = getPublicPort();
180 var scheme = publicPort == HTTPS_DEFAULT_PORT ? "https" : "http";
181 var urlWithPort = String.format("%s://%s:%d", scheme, publicHost, publicPort);
182 if (publicPort == HTTPS_DEFAULT_PORT || publicPort == HTTP_DEFAULT_PORT) {
183 var urlWithoutPort = String.format("%s://%s", scheme, publicHost);
184 return Optional.of(new String[] { urlWithPort, urlWithoutPort });
185 }
186 return Optional.of(new String[] { urlWithPort });
187 }
135} 188}
diff --git a/language-web/src/main/java/tools/refinery/language/web/xtext/HttpServiceContext.java b/language-web/src/main/java/tools/refinery/language/web/xtext/HttpServiceContext.java
deleted file mode 100644
index d0ba6a2d..00000000
--- a/language-web/src/main/java/tools/refinery/language/web/xtext/HttpServiceContext.java
+++ /dev/null
@@ -1,107 +0,0 @@
1/**
2 * Copyright (c) 2015, 2020 itemis AG (http://www.itemis.eu) and others.
3 * This program and the accompanying materials are made available under the
4 * terms of the Eclipse Public License 2.0 which is available at
5 * http://www.eclipse.org/legal/epl-2.0.
6 *
7 * SPDX-License-Identifier: EPL-2.0
8 */
9package tools.refinery.language.web.xtext;
10
11import java.io.IOException;
12import java.net.URLDecoder;
13import java.nio.charset.Charset;
14import java.util.Collections;
15import java.util.Enumeration;
16import java.util.HashMap;
17import java.util.Map;
18import java.util.Set;
19
20import org.eclipse.xtext.web.server.IServiceContext;
21import org.eclipse.xtext.web.server.ISession;
22
23import com.google.common.io.CharStreams;
24
25import jakarta.servlet.http.HttpServletRequest;
26
27/**
28 * Provides the parameters and metadata of an {@link HttpServletRequest}.
29 */
30class HttpServiceContext implements IServiceContext {
31 private final HttpServletRequest request;
32
33 private final Map<String, String> parameters = new HashMap<>();
34
35 private HttpSessionWrapper sessionWrapper;
36
37 public HttpServiceContext(HttpServletRequest request) throws IOException {
38 this.request = request;
39 initializeParameters();
40 }
41
42 private void initializeParameters() throws IOException {
43 initializeUrlEncodedParameters();
44 initializeRequestParameters();
45 if (!parameters.containsKey(IServiceContext.SERVICE_TYPE)) {
46 String substring = null;
47 if (request.getPathInfo() != null) {
48 substring = request.getPathInfo().substring(1);
49 }
50 parameters.put(IServiceContext.SERVICE_TYPE, substring);
51 }
52 }
53
54 private void initializeUrlEncodedParameters() throws IOException {
55 String[] contentType = null;
56 if (request.getContentType() != null) {
57 contentType = request.getContentType().split(";(\\s*)");
58 }
59 if (contentType != null && "application/x-www-form-urlencoded".equals(contentType[0])) {
60 String charset = null;
61 if (contentType.length >= 2 && contentType[1].startsWith("charset=")) {
62 charset = (contentType[1]).substring("charset=".length());
63 } else {
64 charset = Charset.defaultCharset().toString();
65 }
66 String[] encodedParams = CharStreams.toString(request.getReader()).split("&");
67 for (String param : encodedParams) {
68 int nameEnd = param.indexOf("=");
69 if (nameEnd > 0) {
70 String key = param.substring(0, nameEnd);
71 String value = URLDecoder.decode(param.substring(nameEnd + 1), charset);
72 parameters.put(key, value);
73 }
74 }
75 }
76 }
77
78 private void initializeRequestParameters() {
79 Enumeration<String> paramNames = request.getParameterNames();
80 while (paramNames.hasMoreElements()) {
81 String name = paramNames.nextElement();
82 parameters.put(name, request.getParameter(name));
83 }
84 }
85
86 @Override
87 public Set<String> getParameterKeys() {
88 return Collections.unmodifiableSet(parameters.keySet());
89 }
90
91 @Override
92 public String getParameter(String key) {
93 return parameters.get(key);
94 }
95
96 @Override
97 public ISession getSession() {
98 if (sessionWrapper == null) {
99 sessionWrapper = new HttpSessionWrapper(request.getSession(true));
100 }
101 return sessionWrapper;
102 }
103
104 public HttpServletRequest getRequest() {
105 return request;
106 }
107}
diff --git a/language-web/src/main/java/tools/refinery/language/web/xtext/HttpSessionWrapper.java b/language-web/src/main/java/tools/refinery/language/web/xtext/HttpSessionWrapper.java
deleted file mode 100644
index 8a5e19ba..00000000
--- a/language-web/src/main/java/tools/refinery/language/web/xtext/HttpSessionWrapper.java
+++ /dev/null
@@ -1,53 +0,0 @@
1/**
2 * Copyright (c) 2015, 2020 itemis AG (http://www.itemis.eu) and others.
3 * This program and the accompanying materials are made available under the
4 * terms of the Eclipse Public License 2.0 which is available at
5 * http://www.eclipse.org/legal/epl-2.0.
6 *
7 * SPDX-License-Identifier: EPL-2.0
8 */
9package tools.refinery.language.web.xtext;
10
11import org.eclipse.xtext.web.server.ISession;
12import org.eclipse.xtext.xbase.lib.Functions.Function0;
13
14import jakarta.servlet.http.HttpSession;
15
16/**
17 * Provides access to the information stored in a {@link HttpSession}.
18 */
19record HttpSessionWrapper(HttpSession session) implements ISession {
20 @SuppressWarnings("unchecked")
21 @Override
22 public <T> T get(Object key) {
23 return (T) session.getAttribute(key.toString());
24 }
25
26 @Override
27 public <T> T get(Object key, Function0<? extends T> factory) {
28 synchronized (session) {
29 T sessionValue = get(key);
30 if (sessionValue != null) {
31 return sessionValue;
32 } else {
33 T factoryValue = factory.apply();
34 put(key, factoryValue);
35 return factoryValue;
36 }
37 }
38 }
39
40 @Override
41 public void put(Object key, Object value) {
42 session.setAttribute(key.toString(), value);
43 }
44
45 @Override
46 public void remove(Object key) {
47 session.removeAttribute(key.toString());
48 }
49
50 public HttpSession getSession() {
51 return session;
52 }
53}
diff --git a/language-web/src/main/java/tools/refinery/language/web/xtext/SimpleServiceContext.java b/language-web/src/main/java/tools/refinery/language/web/xtext/SimpleServiceContext.java
new file mode 100644
index 00000000..1ec5b235
--- /dev/null
+++ b/language-web/src/main/java/tools/refinery/language/web/xtext/SimpleServiceContext.java
@@ -0,0 +1,27 @@
1package tools.refinery.language.web.xtext;
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
11record SimpleServiceContext(ISession session, Map<String, String> parameters) implements IServiceContext {
12
13 @Override
14 public Set<String> getParameterKeys() {
15 return ImmutableSet.copyOf(parameters.keySet());
16 }
17
18 @Override
19 public String getParameter(String key) {
20 return parameters.get(key);
21 }
22
23 @Override
24 public ISession getSession() {
25 return session;
26 }
27}
diff --git a/language-web/src/main/java/tools/refinery/language/web/xtext/SimpleSession.java b/language-web/src/main/java/tools/refinery/language/web/xtext/SimpleSession.java
new file mode 100644
index 00000000..691077d0
--- /dev/null
+++ b/language-web/src/main/java/tools/refinery/language/web/xtext/SimpleSession.java
@@ -0,0 +1,35 @@
1package tools.refinery.language.web.xtext;
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/XtextServlet.java b/language-web/src/main/java/tools/refinery/language/web/xtext/XtextServlet.java
deleted file mode 100644
index f39bec12..00000000
--- a/language-web/src/main/java/tools/refinery/language/web/xtext/XtextServlet.java
+++ /dev/null
@@ -1,196 +0,0 @@
1/**
2 * Copyright (c) 2015, 2020 itemis AG (http://www.itemis.eu) and others.
3 * This program and the accompanying materials are made available under the
4 * terms of the Eclipse Public License 2.0 which is available at
5 * http://www.eclipse.org/legal/epl-2.0.
6 *
7 * SPDX-License-Identifier: EPL-2.0
8 */
9package tools.refinery.language.web.xtext;
10
11import java.io.IOException;
12import java.util.Set;
13
14import org.eclipse.emf.common.util.URI;
15import org.eclipse.xtext.resource.IResourceServiceProvider;
16import org.eclipse.xtext.web.server.IServiceContext;
17import org.eclipse.xtext.web.server.IServiceResult;
18import org.eclipse.xtext.web.server.IUnwrappableServiceResult;
19import org.eclipse.xtext.web.server.InvalidRequestException;
20import org.eclipse.xtext.web.server.XtextServiceDispatcher;
21import org.slf4j.Logger;
22import org.slf4j.LoggerFactory;
23
24import com.google.common.base.Objects;
25import com.google.common.base.Strings;
26import com.google.gson.Gson;
27import com.google.inject.Injector;
28
29import jakarta.servlet.ServletException;
30import jakarta.servlet.http.HttpServlet;
31import jakarta.servlet.http.HttpServletRequest;
32import jakarta.servlet.http.HttpServletResponse;
33
34/**
35 * An HTTP servlet for publishing the Xtext services. Include this into your web
36 * server by creating a subclass that executes the standalone setups of your
37 * languages in its {@link #init()} method:
38 *
39 * <pre>
40 * &#64;WebServlet(name = "Xtext Services", urlPatterns = "/xtext-service/*")
41 * class MyXtextServlet extends XtextServlet {
42 * override init() {
43 * super.init();
44 * MyDslWebSetup.doSetup();
45 * }
46 * }
47 * </pre>
48 *
49 * Use the {@code WebServlet} annotation to register your servlet. The default
50 * URL pattern for Xtext services is {@code "/xtext-service/*"}.
51 */
52public class XtextServlet extends HttpServlet {
53
54 private static final long serialVersionUID = 7784324070547781918L;
55
56 private static final IResourceServiceProvider.Registry SERVICE_PROVIDER_REGISTRY = IResourceServiceProvider.Registry.INSTANCE;
57
58 private static final String ENCODING = "UTF-8";
59
60 private static final String INVALID_REQUEST_MESSAGE = "Invalid request ({}): {}";
61
62 private final transient Logger log = LoggerFactory.getLogger(this.getClass());
63
64 private final transient Gson gson = new Gson();
65
66 @Override
67 protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
68 try {
69 super.service(req, resp);
70 } catch (InvalidRequestException.ResourceNotFoundException exception) {
71 log.trace(INVALID_REQUEST_MESSAGE, req.getRequestURI(), exception.getMessage());
72 resp.sendError(HttpServletResponse.SC_NOT_FOUND, exception.getMessage());
73 } catch (InvalidRequestException.InvalidDocumentStateException exception) {
74 log.trace(INVALID_REQUEST_MESSAGE, req.getRequestURI(), exception.getMessage());
75 resp.sendError(HttpServletResponse.SC_CONFLICT, exception.getMessage());
76 } catch (InvalidRequestException.PermissionDeniedException exception) {
77 log.trace(INVALID_REQUEST_MESSAGE, req.getRequestURI(), exception.getMessage());
78 resp.sendError(HttpServletResponse.SC_FORBIDDEN, exception.getMessage());
79 } catch (InvalidRequestException exception) {
80 log.trace(INVALID_REQUEST_MESSAGE, req.getRequestURI(), exception.getMessage());
81 resp.sendError(HttpServletResponse.SC_BAD_REQUEST, exception.getMessage());
82 }
83 }
84
85 @Override
86 protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
87 XtextServiceDispatcher.ServiceDescriptor service = getService(req);
88 if (!service.isHasConflict() && (service.isHasSideEffects() || hasTextInput(service))) {
89 super.doGet(req, resp);
90 } else {
91 doService(service, resp);
92 }
93 }
94
95 @Override
96 protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
97 XtextServiceDispatcher.ServiceDescriptor service = getService(req);
98 String type = service.getContext().getParameter(IServiceContext.SERVICE_TYPE);
99 if (!service.isHasConflict() && !Objects.equal(type, "update")) {
100 super.doPut(req, resp);
101 } else {
102 doService(service, resp);
103 }
104 }
105
106 @Override
107 protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
108 XtextServiceDispatcher.ServiceDescriptor service = getService(req);
109 String type = service.getContext().getParameter(IServiceContext.SERVICE_TYPE);
110 if (!service.isHasConflict()
111 && (!service.isHasSideEffects() && !hasTextInput(service) || Objects.equal(type, "update"))) {
112 super.doPost(req, resp);
113 } else {
114 doService(service, resp);
115 }
116 }
117
118 protected boolean hasTextInput(XtextServiceDispatcher.ServiceDescriptor service) {
119 Set<String> parameterKeys = service.getContext().getParameterKeys();
120 return parameterKeys.contains("fullText") || parameterKeys.contains("deltaText");
121 }
122
123 /**
124 * Retrieve the service metadata for the given request. This involves resolving
125 * the Guice injector for the respective language, querying the
126 * {@link XtextServiceDispatcher}, and checking the permission to invoke the
127 * service.
128 */
129 protected XtextServiceDispatcher.ServiceDescriptor getService(HttpServletRequest request) throws IOException {
130 HttpServiceContext serviceContext = new HttpServiceContext(request);
131 Injector injector = getInjector(serviceContext);
132 XtextServiceDispatcher serviceDispatcher = injector.getInstance(XtextServiceDispatcher.class);
133 return serviceDispatcher.getService(serviceContext);
134 }
135
136 /**
137 * Invoke the service function of the given service descriptor and write its
138 * result to the servlet response in Json format. An exception is made for
139 * {@link IUnwrappableServiceResult}: here the document itself is written into
140 * the response instead of wrapping it into a Json object.
141 */
142 protected void doService(XtextServiceDispatcher.ServiceDescriptor service, HttpServletResponse response)
143 throws IOException {
144 IServiceResult result = service.getService().apply();
145 response.setStatus(HttpServletResponse.SC_OK);
146 response.setCharacterEncoding(ENCODING);
147 response.setHeader("Cache-Control", "no-cache");
148 if (result instanceof IUnwrappableServiceResult unwrapResult && unwrapResult.getContent() != null) {
149 String contentType = null;
150 if (unwrapResult.getContentType() != null) {
151 contentType = unwrapResult.getContentType();
152 } else {
153 contentType = "text/plain";
154 }
155 response.setContentType(contentType);
156 response.getWriter().write(unwrapResult.getContent());
157 } else {
158 response.setContentType("text/x-json");
159 gson.toJson(result, response.getWriter());
160 }
161 }
162
163 /**
164 * Resolve the Guice injector for the language associated with the given
165 * context.
166 */
167 protected Injector getInjector(HttpServiceContext serviceContext)
168 throws InvalidRequestException.UnknownLanguageException {
169 IResourceServiceProvider resourceServiceProvider = null;
170 String parameter = serviceContext.getParameter("resource");
171 if (parameter == null) {
172 parameter = "";
173 }
174 URI emfURI = URI.createURI(parameter);
175 String contentType = serviceContext.getParameter("contentType");
176 if (Strings.isNullOrEmpty(contentType)) {
177 resourceServiceProvider = SERVICE_PROVIDER_REGISTRY.getResourceServiceProvider(emfURI);
178 if (resourceServiceProvider == null) {
179 if (emfURI.toString().isEmpty()) {
180 throw new InvalidRequestException.UnknownLanguageException(
181 "Unable to identify the Xtext language: missing parameter 'resource' or 'contentType'.");
182 } else {
183 throw new InvalidRequestException.UnknownLanguageException(
184 "Unable to identify the Xtext language for resource " + emfURI + ".");
185 }
186 }
187 } else {
188 resourceServiceProvider = SERVICE_PROVIDER_REGISTRY.getResourceServiceProvider(emfURI, contentType);
189 if (resourceServiceProvider == null) {
190 throw new InvalidRequestException.UnknownLanguageException(
191 "Unable to identify the Xtext language for contentType " + contentType + ".");
192 }
193 }
194 return resourceServiceProvider.get(Injector.class);
195 }
196} \ No newline at end of file
diff --git a/language-web/src/main/java/tools/refinery/language/web/xtext/XtextStatusCode.java b/language-web/src/main/java/tools/refinery/language/web/xtext/XtextStatusCode.java
new file mode 100644
index 00000000..8ef60108
--- /dev/null
+++ b/language-web/src/main/java/tools/refinery/language/web/xtext/XtextStatusCode.java
@@ -0,0 +1,9 @@
1package tools.refinery.language.web.xtext;
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/XtextWebSocket.java b/language-web/src/main/java/tools/refinery/language/web/xtext/XtextWebSocket.java
new file mode 100644
index 00000000..c76ef12f
--- /dev/null
+++ b/language-web/src/main/java/tools/refinery/language/web/xtext/XtextWebSocket.java
@@ -0,0 +1,137 @@
1package tools.refinery.language.web.xtext;
2
3import java.io.IOException;
4import java.io.Reader;
5
6import org.eclipse.emf.common.util.URI;
7import org.eclipse.jetty.websocket.api.Session;
8import org.eclipse.jetty.websocket.api.StatusCode;
9import org.eclipse.jetty.websocket.api.WriteCallback;
10import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage;
11import org.eclipse.jetty.websocket.api.annotations.WebSocket;
12import org.eclipse.xtext.resource.IResourceServiceProvider;
13import org.eclipse.xtext.web.server.IServiceContext;
14import org.eclipse.xtext.web.server.ISession;
15import org.eclipse.xtext.web.server.IUnwrappableServiceResult;
16import org.eclipse.xtext.web.server.InvalidRequestException;
17import org.eclipse.xtext.web.server.InvalidRequestException.UnknownLanguageException;
18import org.eclipse.xtext.web.server.XtextServiceDispatcher;
19import org.slf4j.Logger;
20import org.slf4j.LoggerFactory;
21
22import com.google.common.base.Strings;
23import com.google.gson.Gson;
24import com.google.gson.JsonIOException;
25import com.google.gson.JsonParseException;
26import com.google.inject.Injector;
27
28@WebSocket
29public class XtextWebSocket implements WriteCallback {
30 private static final Logger LOG = LoggerFactory.getLogger(XtextWebSocket.class);
31
32 private final Gson gson = new Gson();
33
34 private final ISession session;
35
36 private final IResourceServiceProvider.Registry resourceServiceProviderRegistry;
37
38 public XtextWebSocket(ISession session, IResourceServiceProvider.Registry resourceServiceProviderRegistry) {
39 this.session = session;
40 this.resourceServiceProviderRegistry = resourceServiceProviderRegistry;
41 }
42
43 @OnWebSocketMessage
44 public void onMessage(Session webSocketSession, Reader reader) {
45 XtextWebSocketRequest request;
46 try {
47 request = gson.fromJson(reader, XtextWebSocketRequest.class);
48 } catch (JsonIOException e) {
49 LOG.error("Cannot read from websocket " + webSocketSession.getRemoteAddress(), e);
50 if (webSocketSession.isOpen()) {
51 webSocketSession.close(StatusCode.SERVER_ERROR, "Cannot read payload");
52 }
53 return;
54 } catch (JsonParseException e) {
55 LOG.warn("Malformed websocket request from " + webSocketSession.getRemoteAddress(), e);
56 webSocketSession.close(XtextStatusCode.INVALID_JSON, "Invalid JSON payload");
57 return;
58 }
59 var serviceContext = new SimpleServiceContext(session, request.getRequestData());
60 var response = handleMessage(serviceContext);
61 response.setId(request.getId());
62 var responseString = gson.toJson(response);
63 try {
64 webSocketSession.getRemote().sendPartialString(responseString, true, this);
65 } catch (IOException e) {
66 LOG.warn("Cannot initiaite async write to websocket to " + webSocketSession.getRemoteAddress(), e);
67 if (webSocketSession.isOpen()) {
68 webSocketSession.close(StatusCode.SERVER_ERROR, "Cannot write payload");
69 }
70 }
71 }
72
73 @Override
74 public void writeFailed(Throwable x) {
75 LOG.warn("Cannot complete async write to websocket", x);
76 }
77
78 protected XtextWebSocketResponse handleMessage(IServiceContext serviceContext) {
79 try {
80 var injector = getInjector(serviceContext);
81 var serviceDispatcher = injector.getInstance(XtextServiceDispatcher.class);
82 var service = serviceDispatcher.getService(serviceContext);
83 var serviceResult = service.getService().apply();
84 var response = new XtextWebSocketOkResponse();
85 if (serviceResult instanceof IUnwrappableServiceResult unwrappableServiceResult
86 && unwrappableServiceResult.getContent() != null) {
87 response.setResponseData(unwrappableServiceResult.getContent());
88 } else {
89 response.setResponseData(serviceResult);
90 }
91 return response;
92 } catch (InvalidRequestException e) {
93 LOG.warn("Invalid request", e);
94 var error = new XtextWebSocketErrorResponse();
95 error.setErrorMessage(e.getMessage());
96 return error;
97 }
98 }
99
100 /**
101 * Get the injector to satisfy the request in the {@code serviceContext}.
102 *
103 * Based on {@link org.eclipse.xtext.web.servlet.XtextServlet#getInjector}.
104 *
105 * @param serviceContext the Xtext service context of the request
106 * @return the injector for the Xtext language in the request
107 * @throws UnknownLanguageException if the Xtext language cannot be determined
108 */
109 protected Injector getInjector(IServiceContext serviceContext) {
110 IResourceServiceProvider resourceServiceProvider = null;
111 var resourceName = serviceContext.getParameter("resource");
112 if (resourceName == null) {
113 resourceName = "";
114 }
115 var emfURI = URI.createURI(resourceName);
116 var contentType = serviceContext.getParameter("contentType");
117 if (Strings.isNullOrEmpty(contentType)) {
118 resourceServiceProvider = resourceServiceProviderRegistry.getResourceServiceProvider(emfURI);
119 if (resourceServiceProvider == null) {
120 if (emfURI.toString().isEmpty()) {
121 throw new UnknownLanguageException(
122 "Unable to identify the Xtext language: missing parameter 'resource' or 'contentType'.");
123 } else {
124 throw new UnknownLanguageException(
125 "Unable to identify the Xtext language for resource " + emfURI + ".");
126 }
127 }
128 } else {
129 resourceServiceProvider = resourceServiceProviderRegistry.getResourceServiceProvider(emfURI, contentType);
130 if (resourceServiceProvider == null) {
131 throw new UnknownLanguageException(
132 "Unable to identify the Xtext language for contentType " + contentType + ".");
133 }
134 }
135 return resourceServiceProvider.get(Injector.class);
136 }
137}
diff --git a/language-web/src/main/java/tools/refinery/language/web/xtext/XtextWebSocketErrorResponse.java b/language-web/src/main/java/tools/refinery/language/web/xtext/XtextWebSocketErrorResponse.java
new file mode 100644
index 00000000..87b300b4
--- /dev/null
+++ b/language-web/src/main/java/tools/refinery/language/web/xtext/XtextWebSocketErrorResponse.java
@@ -0,0 +1,52 @@
1package tools.refinery.language.web.xtext;
2
3import java.util.Objects;
4
5import com.google.gson.annotations.SerializedName;
6
7public final class XtextWebSocketErrorResponse implements XtextWebSocketResponse {
8 private String id;
9
10 @SerializedName("error")
11 private String errorMessage;
12
13 @Override
14 public String getId() {
15 return id;
16 }
17
18 @Override
19 public void setId(String id) {
20 this.id = id;
21 }
22
23 public String getErrorMessage() {
24 return errorMessage;
25 }
26
27 public void setErrorMessage(String errorMessage) {
28 this.errorMessage = errorMessage;
29 }
30
31 @Override
32 public int hashCode() {
33 return Objects.hash(errorMessage, id);
34 }
35
36 @Override
37 public boolean equals(Object obj) {
38 if (this == obj)
39 return true;
40 if (obj == null)
41 return false;
42 if (getClass() != obj.getClass())
43 return false;
44 XtextWebSocketErrorResponse other = (XtextWebSocketErrorResponse) obj;
45 return Objects.equals(errorMessage, other.errorMessage) && Objects.equals(id, other.id);
46 }
47
48 @Override
49 public String toString() {
50 return "XtextWebSocketError [id=" + id + ", errorMessage=" + errorMessage + "]";
51 }
52}
diff --git a/language-web/src/main/java/tools/refinery/language/web/xtext/XtextWebSocketOkResponse.java b/language-web/src/main/java/tools/refinery/language/web/xtext/XtextWebSocketOkResponse.java
new file mode 100644
index 00000000..4ef1768b
--- /dev/null
+++ b/language-web/src/main/java/tools/refinery/language/web/xtext/XtextWebSocketOkResponse.java
@@ -0,0 +1,52 @@
1package tools.refinery.language.web.xtext;
2
3import java.util.Objects;
4
5import com.google.gson.annotations.SerializedName;
6
7public final class XtextWebSocketOkResponse implements XtextWebSocketResponse {
8 private String id;
9
10 @SerializedName("response")
11 private Object responseData;
12
13 @Override
14 public String getId() {
15 return id;
16 }
17
18 @Override
19 public void setId(String id) {
20 this.id = id;
21 }
22
23 public Object getResponseData() {
24 return responseData;
25 }
26
27 public void setResponseData(Object responseData) {
28 this.responseData = responseData;
29 }
30
31 @Override
32 public int hashCode() {
33 return Objects.hash(id, responseData);
34 }
35
36 @Override
37 public boolean equals(Object obj) {
38 if (this == obj)
39 return true;
40 if (obj == null)
41 return false;
42 if (getClass() != obj.getClass())
43 return false;
44 XtextWebSocketOkResponse other = (XtextWebSocketOkResponse) obj;
45 return Objects.equals(id, other.id) && Objects.equals(responseData, other.responseData);
46 }
47
48 @Override
49 public String toString() {
50 return "XtextWebSocketResponse [id=" + id + ", responseData=" + responseData + "]";
51 }
52}
diff --git a/language-web/src/main/java/tools/refinery/language/web/xtext/XtextWebSocketRequest.java b/language-web/src/main/java/tools/refinery/language/web/xtext/XtextWebSocketRequest.java
new file mode 100644
index 00000000..e34bf73a
--- /dev/null
+++ b/language-web/src/main/java/tools/refinery/language/web/xtext/XtextWebSocketRequest.java
@@ -0,0 +1,51 @@
1package tools.refinery.language.web.xtext;
2
3import java.util.Map;
4import java.util.Objects;
5
6import com.google.gson.annotations.SerializedName;
7
8public class XtextWebSocketRequest {
9 private String id;
10
11 @SerializedName("request")
12 private Map<String, String> requestData;
13
14 public String getId() {
15 return id;
16 }
17
18 public void setId(String id) {
19 this.id = id;
20 }
21
22 public Map<String, String> getRequestData() {
23 return requestData;
24 }
25
26 public void setRequestData(Map<String, String> request) {
27 this.requestData = request;
28 }
29
30 @Override
31 public int hashCode() {
32 return Objects.hash(id, requestData);
33 }
34
35 @Override
36 public boolean equals(Object obj) {
37 if (this == obj)
38 return true;
39 if (obj == null)
40 return false;
41 if (getClass() != obj.getClass())
42 return false;
43 XtextWebSocketRequest other = (XtextWebSocketRequest) obj;
44 return Objects.equals(id, other.id) && Objects.equals(requestData, other.requestData);
45 }
46
47 @Override
48 public String toString() {
49 return "XtextWebSocketRequest [id=" + id + ", requestData=" + requestData + "]";
50 }
51}
diff --git a/language-web/src/main/java/tools/refinery/language/web/xtext/XtextWebSocketResponse.java b/language-web/src/main/java/tools/refinery/language/web/xtext/XtextWebSocketResponse.java
new file mode 100644
index 00000000..df0c228e
--- /dev/null
+++ b/language-web/src/main/java/tools/refinery/language/web/xtext/XtextWebSocketResponse.java
@@ -0,0 +1,7 @@
1package tools.refinery.language.web.xtext;
2
3public sealed interface XtextWebSocketResponse permits XtextWebSocketOkResponse, XtextWebSocketErrorResponse {
4 public String getId();
5
6 public void setId(String id);
7}
diff --git a/language-web/src/main/java/tools/refinery/language/web/xtext/XtextWebSocketServlet.java b/language-web/src/main/java/tools/refinery/language/web/xtext/XtextWebSocketServlet.java
new file mode 100644
index 00000000..0de6c358
--- /dev/null
+++ b/language-web/src/main/java/tools/refinery/language/web/xtext/XtextWebSocketServlet.java
@@ -0,0 +1,75 @@
1package tools.refinery.language.web.xtext;
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 /**
28 * Maximum message size should be large enough to upload a full model file.
29 */
30 private static final long MAX_FRAME_SIZE = 4L * 1024L * 1024L;
31
32 private static final Duration IDLE_TIMEOUT = Duration.ofMinutes(10);
33
34 private transient Logger log = LoggerFactory.getLogger(getClass());
35
36 private transient Set<String> allowedOrigins = null;
37
38 @Override
39 public void init(ServletConfig config) throws ServletException {
40 var allowedOriginsStr = config.getInitParameter(ALLOWED_ORIGINS_INIT_PARAM);
41 if (allowedOriginsStr != null) {
42 allowedOrigins = Set.of(allowedOriginsStr.split(ALLOWED_ORIGINS_SEPARATOR));
43 log.info("Allowed origins: {}", allowedOrigins);
44 } else {
45 log.warn("All WebSocket origins are allowed! This setting should not be used in production!");
46 }
47 super.init(config);
48 }
49
50 @Override
51 protected void configure(JettyWebSocketServletFactory factory) {
52 factory.setMaxFrameSize(MAX_FRAME_SIZE);
53 factory.setIdleTimeout(IDLE_TIMEOUT);
54 factory.addMapping("/", this);
55 }
56
57 @Override
58 public Object createWebSocket(JettyServerUpgradeRequest req, JettyServerUpgradeResponse resp) {
59 if (allowedOrigins != null) {
60 var origin = req.getOrigin();
61 if (origin != null && !allowedOrigins.contains(origin.toLowerCase())) {
62 log.error("Connection from {} from forbidden origin {}", req.getRemoteSocketAddress(), origin);
63 try {
64 resp.sendForbidden("Origin not allowed");
65 } catch (IOException e) {
66 log.error("Cannot send forbidden origin error", e);
67 }
68 return null;
69 }
70 }
71 log.debug("New connection from {}", req.getRemoteSocketAddress());
72 var session = new SimpleSession();
73 return new XtextWebSocket(session, IResourceServiceProvider.Registry.INSTANCE);
74 }
75}