From 86cb47fa2c86c27c2f82a70d77c3181e1ba43715 Mon Sep 17 00:00:00 2001 From: Kristóf Marussy Date: Wed, 13 Oct 2021 19:25:15 +0200 Subject: feat(web): batch operations for websocket protocol --- .../language/web/xtext/SimpleServiceContext.java | 41 +++++++- .../language/web/xtext/XtextWebSocket.java | 110 +++++++++++++++------ .../web/xtext/XtextWebSocketErrorKind.java | 14 +++ .../web/xtext/XtextWebSocketErrorResponse.java | 53 +++++++++- .../web/xtext/XtextWebSocketOkResponse.java | 41 +++++++- .../language/web/xtext/XtextWebSocketRequest.java | 50 ++++++++-- .../language/web/xtext/XtextWebSocketResponse.java | 6 +- .../language/web/xtext/XtextWebSocketServlet.java | 9 ++ language-web/src/main/js/logging.tsx | 2 +- 9 files changed, 273 insertions(+), 53 deletions(-) create mode 100644 language-web/src/main/java/tools/refinery/language/web/xtext/XtextWebSocketErrorKind.java (limited to 'language-web') 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 index 1ec5b235..8036b749 100644 --- 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 @@ -5,14 +5,25 @@ import java.util.Set; import org.eclipse.xtext.web.server.IServiceContext; import org.eclipse.xtext.web.server.ISession; +import org.eclipse.xtext.web.server.InvalidRequestException; -import com.google.common.collect.ImmutableSet; +import com.google.common.collect.ImmutableMap; record SimpleServiceContext(ISession session, Map parameters) implements IServiceContext { + public static final String RESOURCE_NAME_PARAMETER = "resource"; + + public static final String CONTENT_TYPE_PARAMETER = "contentType"; + + public static final String STATE_ID_PARAMETER = "requiredStateId"; + + public SimpleServiceContext(ISession session, XtextWebSocketRequest request, String stateId, int index) { + this(session, addPerTransactionData(request, stateId, request.getRequestData().get(index))); + } + @Override public Set getParameterKeys() { - return ImmutableSet.copyOf(parameters.keySet()); + return parameters.keySet(); } @Override @@ -24,4 +35,30 @@ record SimpleServiceContext(ISession session, Map parameters) im public ISession getSession() { return session; } + + private static Map addPerTransactionData(XtextWebSocketRequest request, String stateId, + Map parameters) { + checkParameters(parameters, RESOURCE_NAME_PARAMETER); + checkParameters(parameters, CONTENT_TYPE_PARAMETER); + checkParameters(parameters, STATE_ID_PARAMETER); + var builder = ImmutableMap.builder(); + builder.putAll(parameters); + if (request.getResourceName() != null) { + builder.put(RESOURCE_NAME_PARAMETER, request.getResourceName()); + } + if (request.getContentType() != null) { + builder.put(CONTENT_TYPE_PARAMETER, request.getContentType()); + } + if (stateId != null) { + builder.put(STATE_ID_PARAMETER, stateId); + } + return builder.build(); + } + + private static void checkParameters(Map parameters, String perTransactionParameter) { + if (parameters.containsKey(perTransactionParameter)) { + throw new InvalidRequestException.InvalidParametersException( + "Parameters map must not contain '" + perTransactionParameter + "' parameter."); + } + } } 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 index 4ad98b6e..0849ccb7 100644 --- 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 @@ -13,13 +13,18 @@ import org.eclipse.jetty.websocket.api.annotations.OnWebSocketError; import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage; import org.eclipse.jetty.websocket.api.annotations.WebSocket; import org.eclipse.xtext.resource.IResourceServiceProvider; -import org.eclipse.xtext.web.server.IServiceContext; import org.eclipse.xtext.web.server.IServiceResult; import org.eclipse.xtext.web.server.ISession; -import org.eclipse.xtext.web.server.IUnwrappableServiceResult; import org.eclipse.xtext.web.server.InvalidRequestException; import org.eclipse.xtext.web.server.InvalidRequestException.UnknownLanguageException; +import org.eclipse.xtext.web.server.ServiceConflictResult; import org.eclipse.xtext.web.server.XtextServiceDispatcher; +import org.eclipse.xtext.web.server.contentassist.ContentAssistResult; +import org.eclipse.xtext.web.server.formatting.FormattingResult; +import org.eclipse.xtext.web.server.hover.HoverResult; +import org.eclipse.xtext.web.server.model.DocumentStateResult; +import org.eclipse.xtext.web.server.occurrences.OccurrencesResult; +import org.eclipse.xtext.web.server.persistence.ResourceContentResult; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -80,17 +85,18 @@ public class XtextWebSocket { webSocketSession.close(XtextStatusCode.INVALID_JSON, "Invalid JSON payload"); return; } - var serviceContext = new SimpleServiceContext(session, request.getRequestData()); - var response = handleMessage(webSocketSession, serviceContext); - response.setId(request.getId()); - var responseString = gson.toJson(response); + var requestData = request.getRequestData(); + if (requestData == null || requestData.isEmpty()) { + // Nothing to do. + return; + } + int nCalls = requestData.size(); try { - webSocketSession.getRemote().sendPartialString(responseString, true, new WriteCallback() { - @Override - public void writeFailed(Throwable x) { - LOG.warn("Cannot complete async write to websocket " + webSocketSession.getRemoteAddress(), x); - } - }); + int lastCall = handleTransaction(webSocketSession, request); + for (int index = lastCall + 1; index < nCalls; index++) { + sendReply(webSocketSession, + new XtextWebSocketErrorResponse(request, index, XtextWebSocketErrorKind.TRANSACTION_CANCELLED)); + } } catch (IOException e) { LOG.warn("Cannot initiaite async write to websocket " + webSocketSession.getRemoteAddress(), e); if (webSocketSession.isOpen()) { @@ -99,27 +105,45 @@ public class XtextWebSocket { } } - protected XtextWebSocketResponse handleMessage(Session webSocketSession, IServiceContext serviceContext) { - IServiceResult serviceResult; + protected int handleTransaction(Session webSocketSession, XtextWebSocketRequest request) throws IOException { + var requestData = request.getRequestData(); + var stateId = request.getRequiredStateId(); + int index = 0; try { - var injector = getInjector(serviceContext); + var injector = getInjector(request); var serviceDispatcher = injector.getInstance(XtextServiceDispatcher.class); - var service = serviceDispatcher.getService(serviceContext); - serviceResult = service.getService().apply(); + int nCalls = requestData.size(); + for (; index < nCalls; index++) { + var serviceContext = new SimpleServiceContext(session, request, stateId, index); + var service = serviceDispatcher.getService(serviceContext); + var serviceResult = service.getService().apply(); + sendReply(webSocketSession, new XtextWebSocketOkResponse(request, index, serviceResult)); + if (serviceResult instanceof ServiceConflictResult) { + break; + } + var nextStateId = getNextStateId(serviceResult); + if (nextStateId != null) { + stateId = nextStateId; + } + } } catch (InvalidRequestException e) { - LOG.warn("Invalid request from websocket " + webSocketSession.getRemoteAddress(), e); - var error = new XtextWebSocketErrorResponse(); - error.setErrorMessage(e.getMessage()); - return error; + sendReply(webSocketSession, + new XtextWebSocketErrorResponse(request, index, XtextWebSocketErrorKind.REQUEST_ERROR, e)); + } catch (RuntimeException e) { + sendReply(webSocketSession, + new XtextWebSocketErrorResponse(request, index, XtextWebSocketErrorKind.SERVER_ERROR, e)); } - var response = new XtextWebSocketOkResponse(); - if (serviceResult instanceof IUnwrappableServiceResult unwrappableServiceResult - && unwrappableServiceResult.getContent() != null) { - response.setResponseData(unwrappableServiceResult.getContent()); - } else { - response.setResponseData(serviceResult); - } - return response; + return index; + } + + protected void sendReply(Session webSocketSession, XtextWebSocketResponse response) throws IOException { + var responseString = gson.toJson(response); + webSocketSession.getRemote().sendPartialString(responseString, true, new WriteCallback() { + @Override + public void writeFailed(Throwable x) { + LOG.warn("Cannot complete async write to websocket " + webSocketSession.getRemoteAddress(), x); + } + }); } /** @@ -131,14 +155,14 @@ public class XtextWebSocket { * @return the injector for the Xtext language in the request * @throws UnknownLanguageException if the Xtext language cannot be determined */ - protected Injector getInjector(IServiceContext serviceContext) { + protected Injector getInjector(XtextWebSocketRequest request) { IResourceServiceProvider resourceServiceProvider = null; - var resourceName = serviceContext.getParameter("resource"); + var resourceName = request.getResourceName(); if (resourceName == null) { resourceName = ""; } var emfURI = URI.createURI(resourceName); - var contentType = serviceContext.getParameter("contentType"); + var contentType = request.getContentType(); if (Strings.isNullOrEmpty(contentType)) { resourceServiceProvider = resourceServiceProviderRegistry.getResourceServiceProvider(emfURI); if (resourceServiceProvider == null) { @@ -159,4 +183,26 @@ public class XtextWebSocket { } return resourceServiceProvider.get(Injector.class); } + + protected String getNextStateId(IServiceResult serviceResult) { + if (serviceResult instanceof ContentAssistResult contentAssistResult) { + return contentAssistResult.getStateId(); + } + if (serviceResult instanceof DocumentStateResult documentStateResult) { + return documentStateResult.getStateId(); + } + if (serviceResult instanceof FormattingResult formattingResult) { + return formattingResult.getStateId(); + } + if (serviceResult instanceof HoverResult hoverResult) { + return hoverResult.getStateId(); + } + if (serviceResult instanceof OccurrencesResult occurrencesResult) { + return occurrencesResult.getStateId(); + } + if (serviceResult instanceof ResourceContentResult resourceContentResult) { + return resourceContentResult.getStateId(); + } + return null; + } } diff --git a/language-web/src/main/java/tools/refinery/language/web/xtext/XtextWebSocketErrorKind.java b/language-web/src/main/java/tools/refinery/language/web/xtext/XtextWebSocketErrorKind.java new file mode 100644 index 00000000..5759f39e --- /dev/null +++ b/language-web/src/main/java/tools/refinery/language/web/xtext/XtextWebSocketErrorKind.java @@ -0,0 +1,14 @@ +package tools.refinery.language.web.xtext; + +import com.google.gson.annotations.SerializedName; + +public enum XtextWebSocketErrorKind { + @SerializedName("request") + REQUEST_ERROR, + + @SerializedName("server") + SERVER_ERROR, + + @SerializedName("transaction") + TRANSACTION_CANCELLED, +} 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 index 87b300b4..1d2cf08a 100644 --- 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 @@ -7,19 +7,60 @@ import com.google.gson.annotations.SerializedName; public final class XtextWebSocketErrorResponse implements XtextWebSocketResponse { private String id; + private int index; + @SerializedName("error") + private XtextWebSocketErrorKind errorKind; + + @SerializedName("message") private String errorMessage; - @Override + public XtextWebSocketErrorResponse(String id, int index, XtextWebSocketErrorKind errorKind, String errorMessage) { + super(); + this.id = id; + this.index = index; + this.errorKind = errorKind; + this.errorMessage = errorMessage; + } + + public XtextWebSocketErrorResponse(XtextWebSocketRequest request, int index, XtextWebSocketErrorKind errorKind, + String errorMessage) { + this(request.getId(), index, errorKind, errorMessage); + } + + public XtextWebSocketErrorResponse(XtextWebSocketRequest request, int index, XtextWebSocketErrorKind errorKind) { + this(request, index, errorKind, (String) null); + } + + public XtextWebSocketErrorResponse(XtextWebSocketRequest request, int index, XtextWebSocketErrorKind errorKind, + Throwable t) { + this(request, index, errorKind, t.getMessage()); + } + public String getId() { return id; } - @Override public void setId(String id) { this.id = id; } + public int getIndex() { + return index; + } + + public void setIndex(int index) { + this.index = index; + } + + public XtextWebSocketErrorKind getErrorKind() { + return errorKind; + } + + public void setErrorKind(XtextWebSocketErrorKind errorKind) { + this.errorKind = errorKind; + } + public String getErrorMessage() { return errorMessage; } @@ -30,7 +71,7 @@ public final class XtextWebSocketErrorResponse implements XtextWebSocketResponse @Override public int hashCode() { - return Objects.hash(errorMessage, id); + return Objects.hash(errorKind, errorMessage, id, index); } @Override @@ -42,11 +83,13 @@ public final class XtextWebSocketErrorResponse implements XtextWebSocketResponse if (getClass() != obj.getClass()) return false; XtextWebSocketErrorResponse other = (XtextWebSocketErrorResponse) obj; - return Objects.equals(errorMessage, other.errorMessage) && Objects.equals(id, other.id); + return errorKind == other.errorKind && Objects.equals(errorMessage, other.errorMessage) + && Objects.equals(id, other.id) && index == other.index; } @Override public String toString() { - return "XtextWebSocketError [id=" + id + ", errorMessage=" + errorMessage + "]"; + return "XtextWebSocketErrorResponse [id=" + id + ", index=" + index + ", errorKind=" + errorKind + + ", errorMessage=" + errorMessage + "]"; } } 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 index 4ef1768b..aa453544 100644 --- 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 @@ -2,24 +2,46 @@ package tools.refinery.language.web.xtext; import java.util.Objects; +import org.eclipse.xtext.web.server.IServiceResult; +import org.eclipse.xtext.web.server.IUnwrappableServiceResult; + import com.google.gson.annotations.SerializedName; public final class XtextWebSocketOkResponse implements XtextWebSocketResponse { private String id; + private int index; + @SerializedName("response") private Object responseData; - @Override + public XtextWebSocketOkResponse(String id, int index, Object responseData) { + super(); + this.id = id; + this.index = index; + this.responseData = responseData; + } + + public XtextWebSocketOkResponse(XtextWebSocketRequest request, int index, IServiceResult result) { + this(request.getId(), index, maybeUnwrap(result)); + } + public String getId() { return id; } - @Override public void setId(String id) { this.id = id; } + public int getIndex() { + return index; + } + + public void setIndex(int index) { + this.index = index; + } + public Object getResponseData() { return responseData; } @@ -30,7 +52,7 @@ public final class XtextWebSocketOkResponse implements XtextWebSocketResponse { @Override public int hashCode() { - return Objects.hash(id, responseData); + return Objects.hash(id, index, responseData); } @Override @@ -42,11 +64,20 @@ public final class XtextWebSocketOkResponse implements XtextWebSocketResponse { if (getClass() != obj.getClass()) return false; XtextWebSocketOkResponse other = (XtextWebSocketOkResponse) obj; - return Objects.equals(id, other.id) && Objects.equals(responseData, other.responseData); + return Objects.equals(id, other.id) && index == other.index && Objects.equals(responseData, other.responseData); } @Override public String toString() { - return "XtextWebSocketResponse [id=" + id + ", responseData=" + responseData + "]"; + return "XtextWebSocketOkResponse [id=" + id + ", index=" + index + ", responseData=" + responseData + "]"; + } + + private static Object maybeUnwrap(IServiceResult result) { + if (result instanceof IUnwrappableServiceResult unwrappableServiceResult + && unwrappableServiceResult.getContent() != null) { + return unwrappableServiceResult.getContent(); + } else { + return result; + } } } 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 index e34bf73a..8aee70a1 100644 --- 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 @@ -1,5 +1,6 @@ package tools.refinery.language.web.xtext; +import java.util.List; import java.util.Map; import java.util.Objects; @@ -8,8 +9,15 @@ import com.google.gson.annotations.SerializedName; public class XtextWebSocketRequest { private String id; + @SerializedName("resource") + private String resourceName; + + private String contentType; + + private String requiredStateId; + @SerializedName("request") - private Map requestData; + private List> requestData; public String getId() { return id; @@ -19,17 +27,41 @@ public class XtextWebSocketRequest { this.id = id; } - public Map getRequestData() { + public String getResourceName() { + return resourceName; + } + + public void setResourceName(String resourceName) { + this.resourceName = resourceName; + } + + public String getContentType() { + return contentType; + } + + public void setContentType(String contentType) { + this.contentType = contentType; + } + + public String getRequiredStateId() { + return requiredStateId; + } + + public void setRequiredStateId(String requiredStateId) { + this.requiredStateId = requiredStateId; + } + + public List> getRequestData() { return requestData; } - public void setRequestData(Map request) { - this.requestData = request; + public void setRequestData(List> requestData) { + this.requestData = requestData; } @Override public int hashCode() { - return Objects.hash(id, requestData); + return Objects.hash(contentType, id, requestData, requiredStateId, resourceName); } @Override @@ -41,11 +73,15 @@ public class XtextWebSocketRequest { if (getClass() != obj.getClass()) return false; XtextWebSocketRequest other = (XtextWebSocketRequest) obj; - return Objects.equals(id, other.id) && Objects.equals(requestData, other.requestData); + return Objects.equals(contentType, other.contentType) && Objects.equals(id, other.id) + && Objects.equals(requestData, other.requestData) + && Objects.equals(requiredStateId, other.requiredStateId) + && Objects.equals(resourceName, other.resourceName); } @Override public String toString() { - return "XtextWebSocketRequest [id=" + id + ", requestData=" + requestData + "]"; + return "XtextWebSocketRequest [id=" + id + ", resourceName=" + resourceName + ", contentType=" + contentType + + ", requiredStateId=" + requiredStateId + ", requestData=" + requestData + "]"; } } 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 index df0c228e..9e15aa69 100644 --- 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 @@ -1,7 +1,11 @@ package tools.refinery.language.web.xtext; -public sealed interface XtextWebSocketResponse permits XtextWebSocketOkResponse, XtextWebSocketErrorResponse { +public sealed interface XtextWebSocketResponse permits XtextWebSocketOkResponse,XtextWebSocketErrorResponse { public String getId(); public void setId(String id); + + public int getIndex(); + + public void setIndex(int index); } 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 index 2db11325..5769e9e7 100644 --- 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 @@ -24,6 +24,8 @@ public abstract class XtextWebSocketServlet extends JettyWebSocketServlet implem public static final String ALLOWED_ORIGINS_INIT_PARAM = "tools.refinery.language.web.xtext.XtextWebSocketServlet.allowedOrigin"; + public static final String XTEXT_SUBPROTOCOL_V1 = "tools.refinery.language.web.xtext.v1"; + /** * Maximum message size should be large enough to upload a full model file. */ @@ -68,6 +70,13 @@ public abstract class XtextWebSocketServlet extends JettyWebSocketServlet implem return null; } } + if (req.getSubProtocols().contains(XTEXT_SUBPROTOCOL_V1)) { + resp.setAcceptedSubProtocol(XTEXT_SUBPROTOCOL_V1); + } else { + log.error("None of the subprotocols {} offered by {} are supported", req.getSubProtocols(), + req.getRemoteSocketAddress()); + resp.setAcceptedSubProtocol(null); + } var session = new SimpleSession(); return new XtextWebSocket(session, IResourceServiceProvider.Registry.INSTANCE); } diff --git a/language-web/src/main/js/logging.tsx b/language-web/src/main/js/logging.tsx index 25f50f19..306d122c 100644 --- a/language-web/src/main/js/logging.tsx +++ b/language-web/src/main/js/logging.tsx @@ -2,7 +2,7 @@ import styles, { CSPair } from 'ansi-styles'; import log from 'loglevel'; import * as prefix from 'loglevel-plugin-prefix'; -const colors: Record = { +const colors: Partial> = { TRACE: styles.magenta, DEBUG: styles.cyan, INFO: styles.blue, -- cgit v1.2.3-54-g00ecf