diff options
author | 2021-10-20 01:49:14 +0200 | |
---|---|---|
committer | 2021-10-31 19:26:11 +0100 | |
commit | 7732fedb5933bdc699cd1ef22a766397d5a701d2 (patch) | |
tree | 691b0461ee763c0060f901f306a7fd99d756fcce /language-web/src/main/java/tools | |
parent | feat(web): batched xtext websocket prototype (diff) | |
download | refinery-7732fedb5933bdc699cd1ef22a766397d5a701d2.tar.gz refinery-7732fedb5933bdc699cd1ef22a766397d5a701d2.tar.zst refinery-7732fedb5933bdc699cd1ef22a766397d5a701d2.zip |
feat(web): push precomputed service results
Diffstat (limited to 'language-web/src/main/java/tools')
25 files changed, 719 insertions, 455 deletions
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 index 04f38414..ac8934ed 100644 --- a/language-web/src/main/java/tools/refinery/language/web/ProblemWebModule.java +++ b/language-web/src/main/java/tools/refinery/language/web/ProblemWebModule.java | |||
@@ -4,14 +4,26 @@ | |||
4 | package tools.refinery.language.web; | 4 | package tools.refinery.language.web; |
5 | 5 | ||
6 | import org.eclipse.xtext.web.server.XtextServiceDispatcher; | 6 | import org.eclipse.xtext.web.server.XtextServiceDispatcher; |
7 | import org.eclipse.xtext.web.server.model.IWebDocumentProvider; | ||
8 | import org.eclipse.xtext.web.server.model.XtextWebDocumentAccess; | ||
7 | 9 | ||
8 | import tools.refinery.language.web.xtext.server.NoPrecomputedServicesXtextServiceDispatcher; | 10 | import tools.refinery.language.web.xtext.server.push.PushServiceDispatcher; |
11 | import tools.refinery.language.web.xtext.server.push.PushWebDocumentAccess; | ||
12 | import tools.refinery.language.web.xtext.server.push.PushWebDocumentProvider; | ||
9 | 13 | ||
10 | /** | 14 | /** |
11 | * Use this class to register additional components to be used within the web application. | 15 | * Use this class to register additional components to be used within the web application. |
12 | */ | 16 | */ |
13 | public class ProblemWebModule extends AbstractProblemWebModule { | 17 | public class ProblemWebModule extends AbstractProblemWebModule { |
18 | public Class<? extends IWebDocumentProvider> bindIWebDocumentProvider() { | ||
19 | return PushWebDocumentProvider.class; | ||
20 | } | ||
21 | |||
22 | public Class<? extends XtextWebDocumentAccess> bindXtextWebDocumentAccess() { | ||
23 | return PushWebDocumentAccess.class; | ||
24 | } | ||
25 | |||
14 | public Class<? extends XtextServiceDispatcher> bindXtextServiceDispatcher() { | 26 | public Class<? extends XtextServiceDispatcher> bindXtextServiceDispatcher() { |
15 | return NoPrecomputedServicesXtextServiceDispatcher.class; | 27 | return PushServiceDispatcher.class; |
16 | } | 28 | } |
17 | } | 29 | } |
diff --git a/language-web/src/main/java/tools/refinery/language/web/xtext/server/NoPrecomputedServicesXtextServiceDispatcher.java b/language-web/src/main/java/tools/refinery/language/web/xtext/server/NoPrecomputedServicesXtextServiceDispatcher.java deleted file mode 100644 index 6660d6ac..00000000 --- a/language-web/src/main/java/tools/refinery/language/web/xtext/server/NoPrecomputedServicesXtextServiceDispatcher.java +++ /dev/null | |||
@@ -1,16 +0,0 @@ | |||
1 | package tools.refinery.language.web.xtext.server; | ||
2 | |||
3 | import org.eclipse.xtext.web.server.XtextServiceDispatcher; | ||
4 | import org.eclipse.xtext.web.server.model.PrecomputedServiceRegistry; | ||
5 | |||
6 | import com.google.inject.Singleton; | ||
7 | |||
8 | @Singleton | ||
9 | public class NoPrecomputedServicesXtextServiceDispatcher extends XtextServiceDispatcher { | ||
10 | @Override | ||
11 | protected void registerPreComputedServices(PrecomputedServiceRegistry registry) { | ||
12 | // Do not register any precomputed services, because we will always send | ||
13 | // requests for any pre-computation in the same websocket message as the | ||
14 | // document update request. | ||
15 | } | ||
16 | } | ||
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 new file mode 100644 index 00000000..2a85afe3 --- /dev/null +++ b/language-web/src/main/java/tools/refinery/language/web/xtext/server/ResponseHandler.java | |||
@@ -0,0 +1,8 @@ | |||
1 | package tools.refinery.language.web.xtext.server; | ||
2 | |||
3 | import tools.refinery.language.web.xtext.server.message.XtextWebResponse; | ||
4 | |||
5 | @FunctionalInterface | ||
6 | public 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 new file mode 100644 index 00000000..34fcb546 --- /dev/null +++ b/language-web/src/main/java/tools/refinery/language/web/xtext/server/ResponseHandlerException.java | |||
@@ -0,0 +1,14 @@ | |||
1 | package tools.refinery.language.web.xtext.server; | ||
2 | |||
3 | public 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 new file mode 100644 index 00000000..78e00a9e --- /dev/null +++ b/language-web/src/main/java/tools/refinery/language/web/xtext/server/SubscribingServiceContext.java | |||
@@ -0,0 +1,26 @@ | |||
1 | package tools.refinery.language.web.xtext.server; | ||
2 | |||
3 | import java.util.Set; | ||
4 | |||
5 | import org.eclipse.xtext.web.server.IServiceContext; | ||
6 | import org.eclipse.xtext.web.server.ISession; | ||
7 | |||
8 | import tools.refinery.language.web.xtext.server.push.PrecomputationListener; | ||
9 | |||
10 | public 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 new file mode 100644 index 00000000..f2f26d98 --- /dev/null +++ b/language-web/src/main/java/tools/refinery/language/web/xtext/server/TransactionExecutor.java | |||
@@ -0,0 +1,131 @@ | |||
1 | package tools.refinery.language.web.xtext.server; | ||
2 | |||
3 | import java.lang.ref.WeakReference; | ||
4 | import java.util.HashMap; | ||
5 | import java.util.Map; | ||
6 | |||
7 | import org.eclipse.emf.common.util.URI; | ||
8 | import org.eclipse.xtext.resource.IResourceServiceProvider; | ||
9 | import org.eclipse.xtext.util.IDisposable; | ||
10 | import org.eclipse.xtext.web.server.IServiceContext; | ||
11 | import org.eclipse.xtext.web.server.IServiceResult; | ||
12 | import org.eclipse.xtext.web.server.ISession; | ||
13 | import org.eclipse.xtext.web.server.InvalidRequestException; | ||
14 | import org.eclipse.xtext.web.server.InvalidRequestException.UnknownLanguageException; | ||
15 | import org.eclipse.xtext.web.server.XtextServiceDispatcher; | ||
16 | |||
17 | import com.google.common.base.Strings; | ||
18 | import com.google.inject.Injector; | ||
19 | |||
20 | import tools.refinery.language.web.xtext.server.message.XtextWebErrorKind; | ||
21 | import tools.refinery.language.web.xtext.server.message.XtextWebErrorResponse; | ||
22 | import tools.refinery.language.web.xtext.server.message.XtextWebOkResponse; | ||
23 | import tools.refinery.language.web.xtext.server.message.XtextWebPushMessage; | ||
24 | import tools.refinery.language.web.xtext.server.message.XtextWebRequest; | ||
25 | import tools.refinery.language.web.xtext.server.push.PrecomputationListener; | ||
26 | import tools.refinery.language.web.xtext.server.push.PushWebDocument; | ||
27 | import tools.refinery.language.web.xtext.servlet.SimpleServiceContext; | ||
28 | |||
29 | public class TransactionExecutor implements IDisposable, PrecomputationListener { | ||
30 | private final ISession session; | ||
31 | |||
32 | private final IResourceServiceProvider.Registry resourceServiceProviderRegistry; | ||
33 | |||
34 | private final Map<String, WeakReference<PushWebDocument>> subscriptions = new HashMap<>(); | ||
35 | |||
36 | private ResponseHandler responseHandler; | ||
37 | |||
38 | public TransactionExecutor(ISession session, IResourceServiceProvider.Registry resourceServiceProviderRegistry) { | ||
39 | this.session = session; | ||
40 | this.resourceServiceProviderRegistry = resourceServiceProviderRegistry; | ||
41 | } | ||
42 | |||
43 | public void setResponseHandler(ResponseHandler responseHandler) { | ||
44 | this.responseHandler = responseHandler; | ||
45 | } | ||
46 | |||
47 | public void handleRequest(XtextWebRequest request) throws ResponseHandlerException { | ||
48 | var serviceContext = new SimpleServiceContext(session, request.getRequestData()); | ||
49 | try { | ||
50 | var injector = getInjector(serviceContext); | ||
51 | var serviceDispatcher = injector.getInstance(XtextServiceDispatcher.class); | ||
52 | var service = serviceDispatcher.getService(new SubscribingServiceContext(serviceContext, this)); | ||
53 | var serviceResult = service.getService().apply(); | ||
54 | responseHandler.onResponse(new XtextWebOkResponse(request, serviceResult)); | ||
55 | } catch (InvalidRequestException e) { | ||
56 | responseHandler.onResponse(new XtextWebErrorResponse(request, XtextWebErrorKind.REQUEST_ERROR, e)); | ||
57 | } catch (RuntimeException e) { | ||
58 | responseHandler.onResponse(new XtextWebErrorResponse(request, XtextWebErrorKind.SERVER_ERROR, e)); | ||
59 | } | ||
60 | } | ||
61 | |||
62 | @Override | ||
63 | public void onPrecomputedServiceResult(String resourceId, String stateId, String serviceName, | ||
64 | IServiceResult serviceResult) throws ResponseHandlerException { | ||
65 | responseHandler.onResponse(new XtextWebPushMessage(resourceId, stateId, serviceName, serviceResult)); | ||
66 | } | ||
67 | |||
68 | @Override | ||
69 | public void onSubscribeToPrecomputationEvents(String resourceId, PushWebDocument document) { | ||
70 | PushWebDocument previousDocument = null; | ||
71 | var previousSubscription = subscriptions.get(resourceId); | ||
72 | if (previousSubscription != null) { | ||
73 | previousDocument = previousSubscription.get(); | ||
74 | } | ||
75 | if (previousDocument == document) { | ||
76 | return; | ||
77 | } | ||
78 | if (previousDocument != null) { | ||
79 | previousDocument.removePrecomputationListener(this); | ||
80 | } | ||
81 | subscriptions.put(resourceId, new WeakReference<>(document)); | ||
82 | } | ||
83 | |||
84 | /** | ||
85 | * Get the injector to satisfy the request in the {@code serviceContext}. | ||
86 | * | ||
87 | * Based on {@link org.eclipse.xtext.web.servlet.XtextServlet#getInjector}. | ||
88 | * | ||
89 | * @param serviceContext the Xtext service context of the request | ||
90 | * @return the injector for the Xtext language in the request | ||
91 | * @throws UnknownLanguageException if the Xtext language cannot be determined | ||
92 | */ | ||
93 | protected Injector getInjector(IServiceContext context) { | ||
94 | IResourceServiceProvider resourceServiceProvider = null; | ||
95 | var resourceName = context.getParameter("resource"); | ||
96 | if (resourceName == null) { | ||
97 | resourceName = ""; | ||
98 | } | ||
99 | var emfURI = URI.createURI(resourceName); | ||
100 | var contentType = context.getParameter("contentType"); | ||
101 | if (Strings.isNullOrEmpty(contentType)) { | ||
102 | resourceServiceProvider = resourceServiceProviderRegistry.getResourceServiceProvider(emfURI); | ||
103 | if (resourceServiceProvider == null) { | ||
104 | if (emfURI.toString().isEmpty()) { | ||
105 | throw new UnknownLanguageException( | ||
106 | "Unable to identify the Xtext language: missing parameter 'resource' or 'contentType'."); | ||
107 | } else { | ||
108 | throw new UnknownLanguageException( | ||
109 | "Unable to identify the Xtext language for resource " + emfURI + "."); | ||
110 | } | ||
111 | } | ||
112 | } else { | ||
113 | resourceServiceProvider = resourceServiceProviderRegistry.getResourceServiceProvider(emfURI, contentType); | ||
114 | if (resourceServiceProvider == null) { | ||
115 | throw new UnknownLanguageException( | ||
116 | "Unable to identify the Xtext language for contentType " + contentType + "."); | ||
117 | } | ||
118 | } | ||
119 | return resourceServiceProvider.get(Injector.class); | ||
120 | } | ||
121 | |||
122 | @Override | ||
123 | public void dispose() { | ||
124 | for (var subscription : subscriptions.values()) { | ||
125 | var document = subscription.get(); | ||
126 | if (document != null) { | ||
127 | document.removePrecomputationListener(this); | ||
128 | } | ||
129 | } | ||
130 | } | ||
131 | } | ||
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 new file mode 100644 index 00000000..f74bae74 --- /dev/null +++ b/language-web/src/main/java/tools/refinery/language/web/xtext/server/message/XtextWebErrorKind.java | |||
@@ -0,0 +1,11 @@ | |||
1 | package tools.refinery.language.web.xtext.server.message; | ||
2 | |||
3 | import com.google.gson.annotations.SerializedName; | ||
4 | |||
5 | public 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 new file mode 100644 index 00000000..01d78c31 --- /dev/null +++ b/language-web/src/main/java/tools/refinery/language/web/xtext/server/message/XtextWebErrorResponse.java | |||
@@ -0,0 +1,79 @@ | |||
1 | package tools.refinery.language.web.xtext.server.message; | ||
2 | |||
3 | import java.util.Objects; | ||
4 | |||
5 | import com.google.gson.annotations.SerializedName; | ||
6 | |||
7 | public 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/servlet/XtextWebSocketOkResponse.java b/language-web/src/main/java/tools/refinery/language/web/xtext/server/message/XtextWebOkResponse.java index 0a841895..8af27247 100644 --- a/language-web/src/main/java/tools/refinery/language/web/xtext/servlet/XtextWebSocketOkResponse.java +++ b/language-web/src/main/java/tools/refinery/language/web/xtext/server/message/XtextWebOkResponse.java | |||
@@ -1,4 +1,4 @@ | |||
1 | package tools.refinery.language.web.xtext.servlet; | 1 | package tools.refinery.language.web.xtext.server.message; |
2 | 2 | ||
3 | import java.util.Objects; | 3 | import java.util.Objects; |
4 | 4 | ||
@@ -7,23 +7,20 @@ import org.eclipse.xtext.web.server.IUnwrappableServiceResult; | |||
7 | 7 | ||
8 | import com.google.gson.annotations.SerializedName; | 8 | import com.google.gson.annotations.SerializedName; |
9 | 9 | ||
10 | public final class XtextWebSocketOkResponse implements XtextWebSocketResponse { | 10 | public final class XtextWebOkResponse implements XtextWebResponse { |
11 | private String id; | 11 | private String id; |
12 | 12 | ||
13 | private int index; | ||
14 | |||
15 | @SerializedName("response") | 13 | @SerializedName("response") |
16 | private Object responseData; | 14 | private Object responseData; |
17 | 15 | ||
18 | public XtextWebSocketOkResponse(String id, int index, Object responseData) { | 16 | public XtextWebOkResponse(String id, Object responseData) { |
19 | super(); | 17 | super(); |
20 | this.id = id; | 18 | this.id = id; |
21 | this.index = index; | ||
22 | this.responseData = responseData; | 19 | this.responseData = responseData; |
23 | } | 20 | } |
24 | 21 | ||
25 | public XtextWebSocketOkResponse(XtextWebSocketRequest request, int index, IServiceResult result) { | 22 | public XtextWebOkResponse(XtextWebRequest request, IServiceResult result) { |
26 | this(request.getId(), index, maybeUnwrap(result)); | 23 | this(request.getId(), maybeUnwrap(result)); |
27 | } | 24 | } |
28 | 25 | ||
29 | public String getId() { | 26 | public String getId() { |
@@ -34,14 +31,6 @@ public final class XtextWebSocketOkResponse implements XtextWebSocketResponse { | |||
34 | this.id = id; | 31 | this.id = id; |
35 | } | 32 | } |
36 | 33 | ||
37 | public int getIndex() { | ||
38 | return index; | ||
39 | } | ||
40 | |||
41 | public void setIndex(int index) { | ||
42 | this.index = index; | ||
43 | } | ||
44 | |||
45 | public Object getResponseData() { | 34 | public Object getResponseData() { |
46 | return responseData; | 35 | return responseData; |
47 | } | 36 | } |
@@ -52,7 +41,7 @@ public final class XtextWebSocketOkResponse implements XtextWebSocketResponse { | |||
52 | 41 | ||
53 | @Override | 42 | @Override |
54 | public int hashCode() { | 43 | public int hashCode() { |
55 | return Objects.hash(id, index, responseData); | 44 | return Objects.hash(id, responseData); |
56 | } | 45 | } |
57 | 46 | ||
58 | @Override | 47 | @Override |
@@ -63,13 +52,13 @@ public final class XtextWebSocketOkResponse implements XtextWebSocketResponse { | |||
63 | return false; | 52 | return false; |
64 | if (getClass() != obj.getClass()) | 53 | if (getClass() != obj.getClass()) |
65 | return false; | 54 | return false; |
66 | XtextWebSocketOkResponse other = (XtextWebSocketOkResponse) obj; | 55 | XtextWebOkResponse other = (XtextWebOkResponse) obj; |
67 | return Objects.equals(id, other.id) && index == other.index && Objects.equals(responseData, other.responseData); | 56 | return Objects.equals(id, other.id) && Objects.equals(responseData, other.responseData); |
68 | } | 57 | } |
69 | 58 | ||
70 | @Override | 59 | @Override |
71 | public String toString() { | 60 | public String toString() { |
72 | return "XtextWebSocketOkResponse [id=" + id + ", index=" + index + ", responseData=" + responseData + "]"; | 61 | return "XtextWebSocketOkResponse [id=" + id + ", responseData=" + responseData + "]"; |
73 | } | 62 | } |
74 | 63 | ||
75 | private static Object maybeUnwrap(IServiceResult result) { | 64 | private static Object maybeUnwrap(IServiceResult result) { |
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 new file mode 100644 index 00000000..c9432e1c --- /dev/null +++ b/language-web/src/main/java/tools/refinery/language/web/xtext/server/message/XtextWebPushMessage.java | |||
@@ -0,0 +1,81 @@ | |||
1 | package tools.refinery.language.web.xtext.server.message; | ||
2 | |||
3 | import java.util.Objects; | ||
4 | |||
5 | import com.google.gson.annotations.SerializedName; | ||
6 | |||
7 | public 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 new file mode 100644 index 00000000..959749f8 --- /dev/null +++ b/language-web/src/main/java/tools/refinery/language/web/xtext/server/message/XtextWebRequest.java | |||
@@ -0,0 +1,57 @@ | |||
1 | package tools.refinery.language.web.xtext.server.message; | ||
2 | |||
3 | import java.util.Map; | ||
4 | import java.util.Objects; | ||
5 | |||
6 | import com.google.gson.annotations.SerializedName; | ||
7 | |||
8 | public 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 new file mode 100644 index 00000000..3bd13047 --- /dev/null +++ b/language-web/src/main/java/tools/refinery/language/web/xtext/server/message/XtextWebResponse.java | |||
@@ -0,0 +1,4 @@ | |||
1 | package tools.refinery.language.web.xtext.server.message; | ||
2 | |||
3 | public 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 new file mode 100644 index 00000000..79a284db --- /dev/null +++ b/language-web/src/main/java/tools/refinery/language/web/xtext/server/push/PrecomputationListener.java | |||
@@ -0,0 +1,15 @@ | |||
1 | package tools.refinery.language.web.xtext.server.push; | ||
2 | |||
3 | import org.eclipse.xtext.web.server.IServiceResult; | ||
4 | |||
5 | import tools.refinery.language.web.xtext.server.ResponseHandlerException; | ||
6 | |||
7 | @FunctionalInterface | ||
8 | public 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 new file mode 100644 index 00000000..c7b8108d --- /dev/null +++ b/language-web/src/main/java/tools/refinery/language/web/xtext/server/push/PushServiceDispatcher.java | |||
@@ -0,0 +1,23 @@ | |||
1 | package tools.refinery.language.web.xtext.server.push; | ||
2 | |||
3 | import org.eclipse.xtext.web.server.IServiceContext; | ||
4 | import org.eclipse.xtext.web.server.XtextServiceDispatcher; | ||
5 | import org.eclipse.xtext.web.server.model.XtextWebDocument; | ||
6 | |||
7 | import com.google.inject.Singleton; | ||
8 | |||
9 | import tools.refinery.language.web.xtext.server.SubscribingServiceContext; | ||
10 | |||
11 | @Singleton | ||
12 | public 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 new file mode 100644 index 00000000..906b9e30 --- /dev/null +++ b/language-web/src/main/java/tools/refinery/language/web/xtext/server/push/PushWebDocument.java | |||
@@ -0,0 +1,89 @@ | |||
1 | package tools.refinery.language.web.xtext.server.push; | ||
2 | |||
3 | import java.util.ArrayList; | ||
4 | import java.util.HashMap; | ||
5 | import java.util.List; | ||
6 | import java.util.Map; | ||
7 | |||
8 | import org.eclipse.xtext.util.CancelIndicator; | ||
9 | import org.eclipse.xtext.web.server.IServiceResult; | ||
10 | import org.eclipse.xtext.web.server.model.AbstractCachedService; | ||
11 | import org.eclipse.xtext.web.server.model.DocumentSynchronizer; | ||
12 | import org.eclipse.xtext.web.server.model.XtextWebDocument; | ||
13 | import org.slf4j.Logger; | ||
14 | import org.slf4j.LoggerFactory; | ||
15 | |||
16 | import com.google.common.collect.ImmutableList; | ||
17 | |||
18 | import tools.refinery.language.web.xtext.server.ResponseHandlerException; | ||
19 | |||
20 | public 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 new file mode 100644 index 00000000..ff4bb035 --- /dev/null +++ b/language-web/src/main/java/tools/refinery/language/web/xtext/server/push/PushWebDocumentAccess.java | |||
@@ -0,0 +1,68 @@ | |||
1 | package tools.refinery.language.web.xtext.server.push; | ||
2 | |||
3 | import org.eclipse.xtext.service.OperationCanceledManager; | ||
4 | import org.eclipse.xtext.util.CancelIndicator; | ||
5 | import org.eclipse.xtext.util.concurrent.CancelableUnitOfWork; | ||
6 | import org.eclipse.xtext.web.server.IServiceResult; | ||
7 | import org.eclipse.xtext.web.server.model.AbstractCachedService; | ||
8 | import org.eclipse.xtext.web.server.model.IXtextWebDocument; | ||
9 | import org.eclipse.xtext.web.server.model.PrecomputedServiceRegistry; | ||
10 | import org.eclipse.xtext.web.server.model.XtextWebDocument; | ||
11 | import org.eclipse.xtext.web.server.model.XtextWebDocumentAccess; | ||
12 | import org.eclipse.xtext.web.server.syntaxcoloring.HighlightingService; | ||
13 | import org.eclipse.xtext.web.server.validation.ValidationService; | ||
14 | |||
15 | import com.google.inject.Inject; | ||
16 | |||
17 | public 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 "validation"; | ||
62 | } | ||
63 | if (service instanceof HighlightingService) { | ||
64 | return "highlighting"; | ||
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 new file mode 100644 index 00000000..fc45f74a --- /dev/null +++ b/language-web/src/main/java/tools/refinery/language/web/xtext/server/push/PushWebDocumentProvider.java | |||
@@ -0,0 +1,33 @@ | |||
1 | package tools.refinery.language.web.xtext.server.push; | ||
2 | |||
3 | import org.eclipse.xtext.web.server.IServiceContext; | ||
4 | import org.eclipse.xtext.web.server.model.DocumentSynchronizer; | ||
5 | import org.eclipse.xtext.web.server.model.IWebDocumentProvider; | ||
6 | import org.eclipse.xtext.web.server.model.XtextWebDocument; | ||
7 | |||
8 | import com.google.inject.Inject; | ||
9 | import com.google.inject.Provider; | ||
10 | import 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 | ||
19 | public 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/ResponseHandler.java b/language-web/src/main/java/tools/refinery/language/web/xtext/servlet/ResponseHandler.java deleted file mode 100644 index b1fcbc8b..00000000 --- a/language-web/src/main/java/tools/refinery/language/web/xtext/servlet/ResponseHandler.java +++ /dev/null | |||
@@ -1,8 +0,0 @@ | |||
1 | package tools.refinery.language.web.xtext.servlet; | ||
2 | |||
3 | import java.io.IOException; | ||
4 | |||
5 | @FunctionalInterface | ||
6 | public interface ResponseHandler { | ||
7 | void onResponse(XtextWebSocketResponse response) throws IOException; | ||
8 | } | ||
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 index 945d5db0..43e37160 100644 --- 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 | |||
@@ -5,19 +5,10 @@ import java.util.Set; | |||
5 | 5 | ||
6 | import org.eclipse.xtext.web.server.IServiceContext; | 6 | import org.eclipse.xtext.web.server.IServiceContext; |
7 | import org.eclipse.xtext.web.server.ISession; | 7 | import org.eclipse.xtext.web.server.ISession; |
8 | import org.eclipse.xtext.web.server.InvalidRequestException; | ||
9 | 8 | ||
10 | import com.google.common.collect.ImmutableMap; | ||
11 | import com.google.common.collect.ImmutableSet; | 9 | import com.google.common.collect.ImmutableSet; |
12 | 10 | ||
13 | record SimpleServiceContext(ISession session, Map<String, String> parameters) implements IServiceContext { | 11 | public record SimpleServiceContext(ISession session, Map<String, String> parameters) implements IServiceContext { |
14 | |||
15 | public static final String RESOURCE_NAME_PARAMETER = "resource"; | ||
16 | |||
17 | public static final String CONTENT_TYPE_PARAMETER = "contentType"; | ||
18 | |||
19 | public static final String STATE_ID_PARAMETER = "requiredStateId"; | ||
20 | |||
21 | @Override | 12 | @Override |
22 | public Set<String> getParameterKeys() { | 13 | public Set<String> getParameterKeys() { |
23 | return ImmutableSet.copyOf(parameters.keySet()); | 14 | return ImmutableSet.copyOf(parameters.keySet()); |
@@ -32,32 +23,4 @@ record SimpleServiceContext(ISession session, Map<String, String> parameters) im | |||
32 | public ISession getSession() { | 23 | public ISession getSession() { |
33 | return session; | 24 | return session; |
34 | } | 25 | } |
35 | |||
36 | public static IServiceContext ofTransaction(ISession session, XtextWebSocketRequest request, String stateId, | ||
37 | int index) { | ||
38 | var parameters = request.getRequestData().get(index); | ||
39 | checkParameters(parameters, RESOURCE_NAME_PARAMETER); | ||
40 | checkParameters(parameters, CONTENT_TYPE_PARAMETER); | ||
41 | checkParameters(parameters, STATE_ID_PARAMETER); | ||
42 | var builder = ImmutableMap.<String, String>builder(); | ||
43 | builder.putAll(parameters); | ||
44 | if (request.getResourceName() != null) { | ||
45 | builder.put(RESOURCE_NAME_PARAMETER, request.getResourceName()); | ||
46 | } | ||
47 | if (request.getContentType() != null) { | ||
48 | builder.put(CONTENT_TYPE_PARAMETER, request.getContentType()); | ||
49 | } | ||
50 | if (stateId != null) { | ||
51 | builder.put(STATE_ID_PARAMETER, stateId); | ||
52 | } | ||
53 | var allParameters = builder.build(); | ||
54 | return new SimpleServiceContext(session, allParameters); | ||
55 | } | ||
56 | |||
57 | private static void checkParameters(Map<String, String> parameters, String perTransactionParameter) { | ||
58 | if (parameters.containsKey(perTransactionParameter)) { | ||
59 | throw new InvalidRequestException.InvalidParametersException( | ||
60 | "Parameters map must not contain '" + perTransactionParameter + "' parameter."); | ||
61 | } | ||
62 | } | ||
63 | } | 26 | } |
diff --git a/language-web/src/main/java/tools/refinery/language/web/xtext/servlet/TransactionExecutor.java b/language-web/src/main/java/tools/refinery/language/web/xtext/servlet/TransactionExecutor.java deleted file mode 100644 index 08687097..00000000 --- a/language-web/src/main/java/tools/refinery/language/web/xtext/servlet/TransactionExecutor.java +++ /dev/null | |||
@@ -1,137 +0,0 @@ | |||
1 | package tools.refinery.language.web.xtext.servlet; | ||
2 | |||
3 | import java.io.IOException; | ||
4 | |||
5 | import org.eclipse.emf.common.util.URI; | ||
6 | import org.eclipse.xtext.resource.IResourceServiceProvider; | ||
7 | import org.eclipse.xtext.web.server.IServiceResult; | ||
8 | import org.eclipse.xtext.web.server.ISession; | ||
9 | import org.eclipse.xtext.web.server.InvalidRequestException; | ||
10 | import org.eclipse.xtext.web.server.InvalidRequestException.UnknownLanguageException; | ||
11 | import org.eclipse.xtext.web.server.ServiceConflictResult; | ||
12 | import org.eclipse.xtext.web.server.XtextServiceDispatcher; | ||
13 | import org.eclipse.xtext.web.server.contentassist.ContentAssistResult; | ||
14 | import org.eclipse.xtext.web.server.formatting.FormattingResult; | ||
15 | import org.eclipse.xtext.web.server.hover.HoverResult; | ||
16 | import org.eclipse.xtext.web.server.model.DocumentStateResult; | ||
17 | import org.eclipse.xtext.web.server.occurrences.OccurrencesResult; | ||
18 | import org.eclipse.xtext.web.server.persistence.ResourceContentResult; | ||
19 | |||
20 | import com.google.common.base.Strings; | ||
21 | import com.google.inject.Injector; | ||
22 | |||
23 | public class TransactionExecutor { | ||
24 | private final ISession session; | ||
25 | |||
26 | private final IResourceServiceProvider.Registry resourceServiceProviderRegistry; | ||
27 | |||
28 | public TransactionExecutor(ISession session, IResourceServiceProvider.Registry resourceServiceProviderRegistry) { | ||
29 | this.session = session; | ||
30 | this.resourceServiceProviderRegistry = resourceServiceProviderRegistry; | ||
31 | } | ||
32 | |||
33 | public void handleRequest(XtextWebSocketRequest request, ResponseHandler handler) throws IOException { | ||
34 | var requestData = request.getRequestData(); | ||
35 | if (requestData == null || requestData.isEmpty()) { | ||
36 | // Nothing to do. | ||
37 | return; | ||
38 | } | ||
39 | int nCalls = requestData.size(); | ||
40 | int lastCall = handleTransaction(request, handler); | ||
41 | for (int index = lastCall + 1; index < nCalls; index++) { | ||
42 | handler.onResponse( | ||
43 | new XtextWebSocketErrorResponse(request, index, XtextWebSocketErrorKind.TRANSACTION_CANCELLED)); | ||
44 | } | ||
45 | } | ||
46 | |||
47 | protected int handleTransaction(XtextWebSocketRequest request, ResponseHandler handler) throws IOException { | ||
48 | var requestData = request.getRequestData(); | ||
49 | var stateId = request.getRequiredStateId(); | ||
50 | int index = 0; | ||
51 | try { | ||
52 | var injector = getInjector(request); | ||
53 | var serviceDispatcher = injector.getInstance(XtextServiceDispatcher.class); | ||
54 | int nCalls = requestData.size(); | ||
55 | for (; index < nCalls; index++) { | ||
56 | var serviceContext = SimpleServiceContext.ofTransaction(session, request, stateId, index); | ||
57 | var service = serviceDispatcher.getService(serviceContext); | ||
58 | var serviceResult = service.getService().apply(); | ||
59 | handler.onResponse(new XtextWebSocketOkResponse(request, index, serviceResult)); | ||
60 | if (serviceResult instanceof ServiceConflictResult) { | ||
61 | break; | ||
62 | } | ||
63 | var nextStateId = getNextStateId(serviceResult); | ||
64 | if (nextStateId != null) { | ||
65 | stateId = nextStateId; | ||
66 | } | ||
67 | } | ||
68 | } catch (InvalidRequestException e) { | ||
69 | handler.onResponse( | ||
70 | new XtextWebSocketErrorResponse(request, index, XtextWebSocketErrorKind.REQUEST_ERROR, e)); | ||
71 | } catch (RuntimeException e) { | ||
72 | handler.onResponse( | ||
73 | new XtextWebSocketErrorResponse(request, index, XtextWebSocketErrorKind.SERVER_ERROR, e)); | ||
74 | } | ||
75 | return index; | ||
76 | } | ||
77 | |||
78 | /** | ||
79 | * Get the injector to satisfy the request in the {@code serviceContext}. | ||
80 | * | ||
81 | * Based on {@link org.eclipse.xtext.web.servlet.XtextServlet#getInjector}. | ||
82 | * | ||
83 | * @param serviceContext the Xtext service context of the request | ||
84 | * @return the injector for the Xtext language in the request | ||
85 | * @throws UnknownLanguageException if the Xtext language cannot be determined | ||
86 | */ | ||
87 | protected Injector getInjector(XtextWebSocketRequest request) { | ||
88 | IResourceServiceProvider resourceServiceProvider = null; | ||
89 | var resourceName = request.getResourceName(); | ||
90 | if (resourceName == null) { | ||
91 | resourceName = ""; | ||
92 | } | ||
93 | var emfURI = URI.createURI(resourceName); | ||
94 | var contentType = request.getContentType(); | ||
95 | if (Strings.isNullOrEmpty(contentType)) { | ||
96 | resourceServiceProvider = resourceServiceProviderRegistry.getResourceServiceProvider(emfURI); | ||
97 | if (resourceServiceProvider == null) { | ||
98 | if (emfURI.toString().isEmpty()) { | ||
99 | throw new UnknownLanguageException( | ||
100 | "Unable to identify the Xtext language: missing parameter 'resource' or 'contentType'."); | ||
101 | } else { | ||
102 | throw new UnknownLanguageException( | ||
103 | "Unable to identify the Xtext language for resource " + emfURI + "."); | ||
104 | } | ||
105 | } | ||
106 | } else { | ||
107 | resourceServiceProvider = resourceServiceProviderRegistry.getResourceServiceProvider(emfURI, contentType); | ||
108 | if (resourceServiceProvider == null) { | ||
109 | throw new UnknownLanguageException( | ||
110 | "Unable to identify the Xtext language for contentType " + contentType + "."); | ||
111 | } | ||
112 | } | ||
113 | return resourceServiceProvider.get(Injector.class); | ||
114 | } | ||
115 | |||
116 | protected String getNextStateId(IServiceResult serviceResult) { | ||
117 | if (serviceResult instanceof ContentAssistResult contentAssistResult) { | ||
118 | return contentAssistResult.getStateId(); | ||
119 | } | ||
120 | if (serviceResult instanceof DocumentStateResult documentStateResult) { | ||
121 | return documentStateResult.getStateId(); | ||
122 | } | ||
123 | if (serviceResult instanceof FormattingResult formattingResult) { | ||
124 | return formattingResult.getStateId(); | ||
125 | } | ||
126 | if (serviceResult instanceof HoverResult hoverResult) { | ||
127 | return hoverResult.getStateId(); | ||
128 | } | ||
129 | if (serviceResult instanceof OccurrencesResult occurrencesResult) { | ||
130 | return occurrencesResult.getStateId(); | ||
131 | } | ||
132 | if (serviceResult instanceof ResourceContentResult resourceContentResult) { | ||
133 | return resourceContentResult.getStateId(); | ||
134 | } | ||
135 | return null; | ||
136 | } | ||
137 | } | ||
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 index 25f67545..a8b4e123 100644 --- 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 | |||
@@ -20,16 +20,25 @@ import com.google.gson.Gson; | |||
20 | import com.google.gson.JsonIOException; | 20 | import com.google.gson.JsonIOException; |
21 | import com.google.gson.JsonParseException; | 21 | import com.google.gson.JsonParseException; |
22 | 22 | ||
23 | import tools.refinery.language.web.xtext.server.ResponseHandler; | ||
24 | import tools.refinery.language.web.xtext.server.ResponseHandlerException; | ||
25 | import tools.refinery.language.web.xtext.server.TransactionExecutor; | ||
26 | import tools.refinery.language.web.xtext.server.message.XtextWebRequest; | ||
27 | import tools.refinery.language.web.xtext.server.message.XtextWebResponse; | ||
28 | |||
23 | @WebSocket | 29 | @WebSocket |
24 | public class XtextWebSocket { | 30 | public class XtextWebSocket implements WriteCallback, ResponseHandler { |
25 | private static final Logger LOG = LoggerFactory.getLogger(XtextWebSocket.class); | 31 | private static final Logger LOG = LoggerFactory.getLogger(XtextWebSocket.class); |
26 | 32 | ||
27 | private final Gson gson = new Gson(); | 33 | private final Gson gson = new Gson(); |
28 | 34 | ||
29 | private final TransactionExecutor executor; | 35 | private final TransactionExecutor executor; |
30 | 36 | ||
37 | private Session webSocketSession; | ||
38 | |||
31 | public XtextWebSocket(TransactionExecutor executor) { | 39 | public XtextWebSocket(TransactionExecutor executor) { |
32 | this.executor = executor; | 40 | this.executor = executor; |
41 | executor.setResponseHandler(this); | ||
33 | } | 42 | } |
34 | 43 | ||
35 | public XtextWebSocket(ISession session, IResourceServiceProvider.Registry resourceServiceProviderRegistry) { | 44 | public XtextWebSocket(ISession session, IResourceServiceProvider.Registry resourceServiceProviderRegistry) { |
@@ -38,29 +47,46 @@ public class XtextWebSocket { | |||
38 | 47 | ||
39 | @OnWebSocketConnect | 48 | @OnWebSocketConnect |
40 | public void onConnect(Session webSocketSession) { | 49 | public void onConnect(Session webSocketSession) { |
50 | if (this.webSocketSession != null) { | ||
51 | LOG.error("Websocket session onConnect when already connected"); | ||
52 | return; | ||
53 | } | ||
41 | LOG.debug("New websocket connection from {}", webSocketSession.getRemoteAddress()); | 54 | LOG.debug("New websocket connection from {}", webSocketSession.getRemoteAddress()); |
55 | this.webSocketSession = webSocketSession; | ||
42 | } | 56 | } |
43 | 57 | ||
44 | @OnWebSocketClose | 58 | @OnWebSocketClose |
45 | public void onClose(Session webSocketSession, int statusCode, String reason) { | 59 | public void onClose(int statusCode, String reason) { |
60 | executor.dispose(); | ||
61 | if (webSocketSession == null) { | ||
62 | return; | ||
63 | } | ||
46 | if (statusCode == StatusCode.NORMAL) { | 64 | if (statusCode == StatusCode.NORMAL) { |
47 | LOG.debug("{} closed connection normally: {}", webSocketSession.getRemoteAddress(), reason); | 65 | LOG.debug("{} closed connection normally: {}", webSocketSession.getRemoteAddress(), reason); |
48 | } else { | 66 | } else { |
49 | LOG.warn("{} closed connection with status code {}: {}", webSocketSession.getRemoteAddress(), statusCode, | 67 | LOG.warn("{} closed connection with status code {}: {}", webSocketSession.getRemoteAddress(), statusCode, |
50 | reason); | 68 | reason); |
51 | } | 69 | } |
70 | webSocketSession = null; | ||
52 | } | 71 | } |
53 | 72 | ||
54 | @OnWebSocketError | 73 | @OnWebSocketError |
55 | public void onError(Session webSocketSession, Throwable error) { | 74 | public void onError(Throwable error) { |
75 | if (webSocketSession == null) { | ||
76 | return; | ||
77 | } | ||
56 | LOG.error("Internal websocket error in connection from" + webSocketSession.getRemoteAddress(), error); | 78 | LOG.error("Internal websocket error in connection from" + webSocketSession.getRemoteAddress(), error); |
57 | } | 79 | } |
58 | 80 | ||
59 | @OnWebSocketMessage | 81 | @OnWebSocketMessage |
60 | public void onMessage(Session webSocketSession, Reader reader) { | 82 | public void onMessage(Reader reader) { |
61 | XtextWebSocketRequest request; | 83 | if (webSocketSession == null) { |
84 | LOG.error("Trying to receive message when websocket is disconnected"); | ||
85 | return; | ||
86 | } | ||
87 | XtextWebRequest request; | ||
62 | try { | 88 | try { |
63 | request = gson.fromJson(reader, XtextWebSocketRequest.class); | 89 | request = gson.fromJson(reader, XtextWebRequest.class); |
64 | } catch (JsonIOException e) { | 90 | } catch (JsonIOException e) { |
65 | LOG.error("Cannot read from websocket from" + webSocketSession.getRemoteAddress(), e); | 91 | LOG.error("Cannot read from websocket from" + webSocketSession.getRemoteAddress(), e); |
66 | if (webSocketSession.isOpen()) { | 92 | if (webSocketSession.isOpen()) { |
@@ -73,22 +99,35 @@ public class XtextWebSocket { | |||
73 | return; | 99 | return; |
74 | } | 100 | } |
75 | try { | 101 | try { |
76 | executor.handleRequest(request, response -> sendResponse(webSocketSession, response)); | 102 | executor.handleRequest(request); |
77 | } catch (IOException e) { | 103 | } catch (ResponseHandlerException e) { |
78 | LOG.warn("Cannot initiaite async write to websocket " + webSocketSession.getRemoteAddress(), e); | 104 | LOG.warn("Cannot write websocket response", e); |
79 | if (webSocketSession.isOpen()) { | 105 | if (webSocketSession.isOpen()) { |
80 | webSocketSession.close(StatusCode.SERVER_ERROR, "Cannot write payload"); | 106 | webSocketSession.close(StatusCode.SERVER_ERROR, "Cannot write response"); |
81 | } | 107 | } |
82 | } | 108 | } |
83 | } | 109 | } |
84 | 110 | ||
85 | protected void sendResponse(Session webSocketSession, XtextWebSocketResponse response) throws IOException { | 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 | } | ||
86 | var responseString = gson.toJson(response); | 116 | var responseString = gson.toJson(response); |
87 | webSocketSession.getRemote().sendPartialString(responseString, true, new WriteCallback() { | 117 | try { |
88 | @Override | 118 | webSocketSession.getRemote().sendPartialString(responseString, true, this); |
89 | public void writeFailed(Throwable x) { | 119 | } catch (IOException e) { |
90 | LOG.warn("Cannot complete async write to websocket " + webSocketSession.getRemoteAddress(), x); | 120 | throw new ResponseHandlerException( |
91 | } | 121 | "Cannot initiaite async write to websocket " + webSocketSession.getRemoteAddress(), e); |
92 | }); | 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); | ||
93 | } | 132 | } |
94 | } | 133 | } |
diff --git a/language-web/src/main/java/tools/refinery/language/web/xtext/servlet/XtextWebSocketErrorKind.java b/language-web/src/main/java/tools/refinery/language/web/xtext/servlet/XtextWebSocketErrorKind.java deleted file mode 100644 index 66ea227f..00000000 --- a/language-web/src/main/java/tools/refinery/language/web/xtext/servlet/XtextWebSocketErrorKind.java +++ /dev/null | |||
@@ -1,14 +0,0 @@ | |||
1 | package tools.refinery.language.web.xtext.servlet; | ||
2 | |||
3 | import com.google.gson.annotations.SerializedName; | ||
4 | |||
5 | public enum XtextWebSocketErrorKind { | ||
6 | @SerializedName("request") | ||
7 | REQUEST_ERROR, | ||
8 | |||
9 | @SerializedName("server") | ||
10 | SERVER_ERROR, | ||
11 | |||
12 | @SerializedName("transaction") | ||
13 | TRANSACTION_CANCELLED, | ||
14 | } | ||
diff --git a/language-web/src/main/java/tools/refinery/language/web/xtext/servlet/XtextWebSocketErrorResponse.java b/language-web/src/main/java/tools/refinery/language/web/xtext/servlet/XtextWebSocketErrorResponse.java deleted file mode 100644 index 3be7df39..00000000 --- a/language-web/src/main/java/tools/refinery/language/web/xtext/servlet/XtextWebSocketErrorResponse.java +++ /dev/null | |||
@@ -1,95 +0,0 @@ | |||
1 | package tools.refinery.language.web.xtext.servlet; | ||
2 | |||
3 | import java.util.Objects; | ||
4 | |||
5 | import com.google.gson.annotations.SerializedName; | ||
6 | |||
7 | public final class XtextWebSocketErrorResponse implements XtextWebSocketResponse { | ||
8 | private String id; | ||
9 | |||
10 | private int index; | ||
11 | |||
12 | @SerializedName("error") | ||
13 | private XtextWebSocketErrorKind errorKind; | ||
14 | |||
15 | @SerializedName("message") | ||
16 | private String errorMessage; | ||
17 | |||
18 | public XtextWebSocketErrorResponse(String id, int index, XtextWebSocketErrorKind errorKind, String errorMessage) { | ||
19 | super(); | ||
20 | this.id = id; | ||
21 | this.index = index; | ||
22 | this.errorKind = errorKind; | ||
23 | this.errorMessage = errorMessage; | ||
24 | } | ||
25 | |||
26 | public XtextWebSocketErrorResponse(XtextWebSocketRequest request, int index, XtextWebSocketErrorKind errorKind, | ||
27 | String errorMessage) { | ||
28 | this(request.getId(), index, errorKind, errorMessage); | ||
29 | } | ||
30 | |||
31 | public XtextWebSocketErrorResponse(XtextWebSocketRequest request, int index, XtextWebSocketErrorKind errorKind) { | ||
32 | this(request, index, errorKind, (String) null); | ||
33 | } | ||
34 | |||
35 | public XtextWebSocketErrorResponse(XtextWebSocketRequest request, int index, XtextWebSocketErrorKind errorKind, | ||
36 | Throwable t) { | ||
37 | this(request, index, errorKind, t.getMessage()); | ||
38 | } | ||
39 | |||
40 | public String getId() { | ||
41 | return id; | ||
42 | } | ||
43 | |||
44 | public void setId(String id) { | ||
45 | this.id = id; | ||
46 | } | ||
47 | |||
48 | public int getIndex() { | ||
49 | return index; | ||
50 | } | ||
51 | |||
52 | public void setIndex(int index) { | ||
53 | this.index = index; | ||
54 | } | ||
55 | |||
56 | public XtextWebSocketErrorKind getErrorKind() { | ||
57 | return errorKind; | ||
58 | } | ||
59 | |||
60 | public void setErrorKind(XtextWebSocketErrorKind errorKind) { | ||
61 | this.errorKind = errorKind; | ||
62 | } | ||
63 | |||
64 | public String getErrorMessage() { | ||
65 | return errorMessage; | ||
66 | } | ||
67 | |||
68 | public void setErrorMessage(String errorMessage) { | ||
69 | this.errorMessage = errorMessage; | ||
70 | } | ||
71 | |||
72 | @Override | ||
73 | public int hashCode() { | ||
74 | return Objects.hash(errorKind, errorMessage, id, index); | ||
75 | } | ||
76 | |||
77 | @Override | ||
78 | public boolean equals(Object obj) { | ||
79 | if (this == obj) | ||
80 | return true; | ||
81 | if (obj == null) | ||
82 | return false; | ||
83 | if (getClass() != obj.getClass()) | ||
84 | return false; | ||
85 | XtextWebSocketErrorResponse other = (XtextWebSocketErrorResponse) obj; | ||
86 | return errorKind == other.errorKind && Objects.equals(errorMessage, other.errorMessage) | ||
87 | && Objects.equals(id, other.id) && index == other.index; | ||
88 | } | ||
89 | |||
90 | @Override | ||
91 | public String toString() { | ||
92 | return "XtextWebSocketErrorResponse [id=" + id + ", index=" + index + ", errorKind=" + errorKind | ||
93 | + ", errorMessage=" + errorMessage + "]"; | ||
94 | } | ||
95 | } | ||
diff --git a/language-web/src/main/java/tools/refinery/language/web/xtext/servlet/XtextWebSocketRequest.java b/language-web/src/main/java/tools/refinery/language/web/xtext/servlet/XtextWebSocketRequest.java deleted file mode 100644 index 8be67bc1..00000000 --- a/language-web/src/main/java/tools/refinery/language/web/xtext/servlet/XtextWebSocketRequest.java +++ /dev/null | |||
@@ -1,97 +0,0 @@ | |||
1 | package tools.refinery.language.web.xtext.servlet; | ||
2 | |||
3 | import java.util.List; | ||
4 | import java.util.Map; | ||
5 | import java.util.Objects; | ||
6 | |||
7 | import com.google.gson.annotations.SerializedName; | ||
8 | |||
9 | public class XtextWebSocketRequest { | ||
10 | private String id; | ||
11 | |||
12 | @SerializedName("resource") | ||
13 | private String resourceName; | ||
14 | |||
15 | private String contentType; | ||
16 | |||
17 | private String requiredStateId; | ||
18 | |||
19 | @SerializedName("request") | ||
20 | private List<Map<String, String>> requestData; | ||
21 | |||
22 | public XtextWebSocketRequest(String id, String resourceName, String contentType, String requiredStateId, | ||
23 | List<Map<String, String>> requestData) { | ||
24 | super(); | ||
25 | this.id = id; | ||
26 | this.resourceName = resourceName; | ||
27 | this.contentType = contentType; | ||
28 | this.requiredStateId = requiredStateId; | ||
29 | this.requestData = requestData; | ||
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 String getResourceName() { | ||
41 | return resourceName; | ||
42 | } | ||
43 | |||
44 | public void setResourceName(String resourceName) { | ||
45 | this.resourceName = resourceName; | ||
46 | } | ||
47 | |||
48 | public String getContentType() { | ||
49 | return contentType; | ||
50 | } | ||
51 | |||
52 | public void setContentType(String contentType) { | ||
53 | this.contentType = contentType; | ||
54 | } | ||
55 | |||
56 | public String getRequiredStateId() { | ||
57 | return requiredStateId; | ||
58 | } | ||
59 | |||
60 | public void setRequiredStateId(String requiredStateId) { | ||
61 | this.requiredStateId = requiredStateId; | ||
62 | } | ||
63 | |||
64 | public List<Map<String, String>> getRequestData() { | ||
65 | return requestData; | ||
66 | } | ||
67 | |||
68 | public void setRequestData(List<Map<String, String>> requestData) { | ||
69 | this.requestData = requestData; | ||
70 | } | ||
71 | |||
72 | @Override | ||
73 | public int hashCode() { | ||
74 | return Objects.hash(contentType, id, requestData, requiredStateId, resourceName); | ||
75 | } | ||
76 | |||
77 | @Override | ||
78 | public boolean equals(Object obj) { | ||
79 | if (this == obj) | ||
80 | return true; | ||
81 | if (obj == null) | ||
82 | return false; | ||
83 | if (getClass() != obj.getClass()) | ||
84 | return false; | ||
85 | XtextWebSocketRequest other = (XtextWebSocketRequest) obj; | ||
86 | return Objects.equals(contentType, other.contentType) && Objects.equals(id, other.id) | ||
87 | && Objects.equals(requestData, other.requestData) | ||
88 | && Objects.equals(requiredStateId, other.requiredStateId) | ||
89 | && Objects.equals(resourceName, other.resourceName); | ||
90 | } | ||
91 | |||
92 | @Override | ||
93 | public String toString() { | ||
94 | return "XtextWebSocketRequest [id=" + id + ", resourceName=" + resourceName + ", contentType=" + contentType | ||
95 | + ", requiredStateId=" + requiredStateId + ", requestData=" + requestData + "]"; | ||
96 | } | ||
97 | } | ||
diff --git a/language-web/src/main/java/tools/refinery/language/web/xtext/servlet/XtextWebSocketResponse.java b/language-web/src/main/java/tools/refinery/language/web/xtext/servlet/XtextWebSocketResponse.java deleted file mode 100644 index 2e7cfbbb..00000000 --- a/language-web/src/main/java/tools/refinery/language/web/xtext/servlet/XtextWebSocketResponse.java +++ /dev/null | |||
@@ -1,11 +0,0 @@ | |||
1 | package tools.refinery.language.web.xtext.servlet; | ||
2 | |||
3 | public sealed interface XtextWebSocketResponse permits XtextWebSocketOkResponse,XtextWebSocketErrorResponse { | ||
4 | public String getId(); | ||
5 | |||
6 | public void setId(String id); | ||
7 | |||
8 | public int getIndex(); | ||
9 | |||
10 | public void setIndex(int index); | ||
11 | } | ||