/*
* SPDX-FileCopyrightText: 2021-2023 The Refinery Authors
*
* SPDX-License-Identifier: EPL-2.0
*/
package tools.refinery.language.web.xtext.server;
import com.google.common.base.Strings;
import com.google.inject.Injector;
import org.eclipse.emf.common.util.URI;
import org.eclipse.xtext.resource.IResourceServiceProvider;
import org.eclipse.xtext.util.IDisposable;
import org.eclipse.xtext.web.server.*;
import org.eclipse.xtext.web.server.InvalidRequestException.UnknownLanguageException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import tools.refinery.language.web.xtext.server.message.*;
import tools.refinery.language.web.xtext.server.push.PrecomputationListener;
import tools.refinery.language.web.xtext.server.push.PushWebDocument;
import tools.refinery.language.web.xtext.servlet.SimpleServiceContext;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class TransactionExecutor implements IDisposable, PrecomputationListener {
private static final Logger LOG = LoggerFactory.getLogger(TransactionExecutor.class);
private final ISession session;
private final IResourceServiceProvider.Registry resourceServiceProviderRegistry;
private final Map> subscriptions = new HashMap<>();
private ResponseHandler responseHandler;
private final Object callPendingLock = new Object();
private boolean callPending;
private final List pendingPushMessages = new ArrayList<>();
private volatile boolean disposed;
public TransactionExecutor(ISession session, IResourceServiceProvider.Registry resourceServiceProviderRegistry) {
this.session = session;
this.resourceServiceProviderRegistry = resourceServiceProviderRegistry;
}
public void setResponseHandler(ResponseHandler responseHandler) {
this.responseHandler = responseHandler;
}
public void handleRequest(XtextWebRequest request) throws ResponseHandlerException {
if (disposed) {
return;
}
var serviceContext = new SimpleServiceContext(session, request.getRequestData());
var ping = serviceContext.getParameter("ping");
if (ping != null) {
onResponse(new XtextWebOkResponse(request, new PongResult(ping)));
return;
}
synchronized (callPendingLock) {
if (callPending) {
LOG.error("Reentrant request detected");
}
if (!pendingPushMessages.isEmpty()) {
LOG.error("{} push messages got stuck without a pending request", pendingPushMessages.size());
}
callPending = true;
}
try {
var injector = getInjector(serviceContext);
var serviceDispatcher = injector.getInstance(XtextServiceDispatcher.class);
var service = serviceDispatcher.getService(new SubscribingServiceContext(serviceContext, this));
var serviceResult = service.getService().apply();
onResponse(new XtextWebOkResponse(request, serviceResult));
} catch (InvalidRequestException e) {
onResponse(new XtextWebErrorResponse(request, XtextWebErrorKind.REQUEST_ERROR, e));
} catch (RuntimeException e) {
onResponse(new XtextWebErrorResponse(request, XtextWebErrorKind.SERVER_ERROR, e));
} finally {
flushPendingPushMessages();
}
}
private void onResponse(XtextWebResponse response) throws ResponseHandlerException {
if (!disposed) {
responseHandler.onResponse(response);
}
}
private void flushPendingPushMessages() {
synchronized (callPendingLock) {
for (var message : pendingPushMessages) {
if (disposed) {
return;
}
try {
responseHandler.onResponse(message);
} catch (ResponseHandlerException | RuntimeException e) {
LOG.error("Error while flushing push message", e);
}
}
pendingPushMessages.clear();
callPending = false;
}
}
@Override
public void onPrecomputedServiceResult(String resourceId, String stateId, String serviceName,
IServiceResult serviceResult) throws ResponseHandlerException {
var message = new XtextWebPushMessage(resourceId, stateId, serviceName, serviceResult);
synchronized (callPendingLock) {
// If we're currently responding to a call we must delay any push messages until
// the reply is sent, because push messages relating to the new state id must be
// sent after the response with the new state id so that the client knows about
// the new state when it receives the push message.
if (callPending) {
pendingPushMessages.add(message);
} else {
responseHandler.onResponse(message);
}
}
}
@Override
public void onSubscribeToPrecomputationEvents(String resourceId, PushWebDocument document) {
PushWebDocument previousDocument = null;
var previousSubscription = subscriptions.get(resourceId);
if (previousSubscription != null) {
previousDocument = previousSubscription.get();
}
if (previousDocument == document) {
return;
}
if (previousDocument != null) {
previousDocument.removePrecomputationListener(this);
}
subscriptions.put(resourceId, new WeakReference<>(document));
}
/**
* Get the injector to satisfy the request in the {@code serviceContext}.
* Based on {@link org.eclipse.xtext.web.servlet.XtextServlet#getInjector}.
*
* @param context the Xtext service context of the request
* @return the injector for the Xtext language in the request
* @throws UnknownLanguageException if the Xtext language cannot be determined
*/
protected Injector getInjector(IServiceContext context) {
IResourceServiceProvider resourceServiceProvider;
var resourceName = context.getParameter("resource");
if (resourceName == null) {
resourceName = "";
}
var emfURI = URI.createURI(resourceName);
var contentType = context.getParameter("contentType");
if (Strings.isNullOrEmpty(contentType)) {
resourceServiceProvider = resourceServiceProviderRegistry.getResourceServiceProvider(emfURI);
if (resourceServiceProvider == null) {
if (emfURI.toString().isEmpty()) {
throw new UnknownLanguageException(
"Unable to identify the Xtext language: missing parameter 'resource' or 'contentType'.");
} else {
throw new UnknownLanguageException(
"Unable to identify the Xtext language for resource " + emfURI + ".");
}
}
} else {
resourceServiceProvider = resourceServiceProviderRegistry.getResourceServiceProvider(emfURI, contentType);
if (resourceServiceProvider == null) {
throw new UnknownLanguageException(
"Unable to identify the Xtext language for contentType " + contentType + ".");
}
}
return resourceServiceProvider.get(Injector.class);
}
@Override
public void dispose() {
disposed = true;
for (var subscription : subscriptions.values()) {
var document = subscription.get();
if (document != null) {
document.removePrecomputationListener(this);
document.dispose();
}
}
}
}