From 814295eecc10b7a87694b25c347d0821e3502c92 Mon Sep 17 00:00:00 2001 From: Kristóf Marussy Date: Thu, 7 Oct 2021 16:26:05 +0200 Subject: chore(web): bump Jetty version Requred vendoring org.eclipse.xtext.web.servlet as tools.refinery.language.web.xtext due to the javax -> jakarta package name change: https://www.eclipse.org/lists/jetty-announce/msg00149.html --- language-web/build.gradle | 4 +- .../refinery/language/web/CacheControlFilter.java | 16 +- .../refinery/language/web/ProblemServlet.java | 22 ++- .../refinery/language/web/ServerLauncher.java | 24 +-- .../language/web/xtext/HttpServiceContext.java | 107 +++++++++++ .../language/web/xtext/HttpSessionWrapper.java | 53 ++++++ .../refinery/language/web/xtext/XtextServlet.java | 196 +++++++++++++++++++++ 7 files changed, 385 insertions(+), 37 deletions(-) create mode 100644 language-web/src/main/java/tools/refinery/language/web/xtext/HttpServiceContext.java create mode 100644 language-web/src/main/java/tools/refinery/language/web/xtext/HttpSessionWrapper.java create mode 100644 language-web/src/main/java/tools/refinery/language/web/xtext/XtextServlet.java (limited to 'language-web') diff --git a/language-web/build.gradle b/language-web/build.gradle index 15d743b9..c467c019 100644 --- a/language-web/build.gradle +++ b/language-web/build.gradle @@ -4,12 +4,12 @@ apply from: "${rootDir}/gradle/xtext-common.gradle" dependencies { implementation project(':refinery-language') implementation project(':refinery-language-ide') - implementation "org.eclipse.xtext:org.eclipse.xtext.xbase.web:${xtextVersion}" - implementation "org.eclipse.xtext:org.eclipse.xtext.web.servlet:${xtextVersion}" + implementation "org.eclipse.xtext:org.eclipse.xtext.web:${xtextVersion}" implementation "org.eclipse.xtend:org.eclipse.xtend.lib:${xtextVersion}" implementation "org.eclipse.jetty:jetty-server:${jettyVersion}" implementation "org.eclipse.jetty:jetty-servlet:${jettyVersion}" implementation "org.slf4j:slf4j-simple:${slf4JVersion}" + implementation "org.slf4j:log4j-over-slf4j:${slf4JVersion}" } def generateXtextLanguage = project(':refinery-language').tasks.named('generateXtextLanguage') diff --git a/language-web/src/main/java/tools/refinery/language/web/CacheControlFilter.java b/language-web/src/main/java/tools/refinery/language/web/CacheControlFilter.java index e39ce54c..cf4c00fa 100644 --- a/language-web/src/main/java/tools/refinery/language/web/CacheControlFilter.java +++ b/language-web/src/main/java/tools/refinery/language/web/CacheControlFilter.java @@ -3,14 +3,14 @@ package tools.refinery.language.web; import java.io.IOException; import java.util.regex.Pattern; -import javax.servlet.Filter; -import javax.servlet.FilterChain; -import javax.servlet.FilterConfig; -import javax.servlet.ServletException; -import javax.servlet.ServletRequest; -import javax.servlet.ServletResponse; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; +import jakarta.servlet.Filter; +import jakarta.servlet.FilterChain; +import jakarta.servlet.FilterConfig; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; public class CacheControlFilter implements Filter { diff --git a/language-web/src/main/java/tools/refinery/language/web/ProblemServlet.java b/language-web/src/main/java/tools/refinery/language/web/ProblemServlet.java index d249525f..49457002 100644 --- a/language-web/src/main/java/tools/refinery/language/web/ProblemServlet.java +++ b/language-web/src/main/java/tools/refinery/language/web/ProblemServlet.java @@ -3,29 +3,27 @@ */ package tools.refinery.language.web; -import javax.servlet.ServletException; - import org.eclipse.xtext.util.DisposableRegistry; -import org.eclipse.xtext.web.servlet.XtextServlet; + +import jakarta.servlet.ServletException; +import tools.refinery.language.web.xtext.XtextServlet; /** * Deploy this class into a servlet container to enable DSL-specific services. */ public class ProblemServlet extends XtextServlet { - - private static final long serialVersionUID = 1L; - - // Xtext requires a mutable servlet instance field. - @SuppressWarnings("squid:S2226") - private DisposableRegistry disposableRegistry; - + + private static final long serialVersionUID = -9204695886561362912L; + + private transient DisposableRegistry disposableRegistry; + @Override public void init() throws ServletException { super.init(); var injector = new ProblemWebSetup().createInjectorAndDoEMFRegistration(); this.disposableRegistry = injector.getInstance(DisposableRegistry.class); } - + @Override public void destroy() { if (disposableRegistry != null) { @@ -34,5 +32,5 @@ public class ProblemServlet extends XtextServlet { } super.destroy(); } - + } diff --git a/language-web/src/main/java/tools/refinery/language/web/ServerLauncher.java b/language-web/src/main/java/tools/refinery/language/web/ServerLauncher.java index c6ee94dc..c253422b 100644 --- a/language-web/src/main/java/tools/refinery/language/web/ServerLauncher.java +++ b/language-web/src/main/java/tools/refinery/language/web/ServerLauncher.java @@ -11,16 +11,17 @@ import java.net.URISyntaxException; import java.util.EnumSet; import java.util.Set; -import javax.servlet.DispatcherType; -import javax.servlet.SessionTrackingMode; - import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.session.SessionHandler; import org.eclipse.jetty.servlet.DefaultServlet; import org.eclipse.jetty.servlet.ServletContextHandler; import org.eclipse.jetty.servlet.ServletHolder; -import org.eclipse.jetty.util.log.Slf4jLog; import org.eclipse.jetty.util.resource.Resource; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import jakarta.servlet.DispatcherType; +import jakarta.servlet.SessionTrackingMode; public class ServerLauncher { public static final String DEFAULT_LISTEN_ADDRESS = "localhost"; @@ -30,7 +31,7 @@ public class ServerLauncher { // Use this cookie name for load balancing. public static final String SESSION_COOKIE_NAME = "JSESSIONID"; - private static final Slf4jLog LOG = new Slf4jLog(ServerLauncher.class.getName()); + private static final Logger LOG = LoggerFactory.getLogger(ServerLauncher.class); private final Server server; @@ -71,15 +72,8 @@ public class ServerLauncher { public void start() throws Exception { server.start(); - LOG.info("Server started " + server.getURI() + "..."); - LOG.info("Press enter to stop the server..."); - int key = System.in.read(); - if (key != -1) { - server.stop(); - } else { - LOG.warn("Console input is not available. " - + "In order to stop the server, you need to cancel process manually."); - } + LOG.info("Server started on {}", server.getURI()); + server.join(); } public static void main(String[] args) { @@ -89,7 +83,7 @@ public class ServerLauncher { var serverLauncher = new ServerLauncher(bindAddress, baseResource); serverLauncher.start(); } catch (Exception exception) { - LOG.warn(exception); + LOG.error("Fatal server error", exception); System.exit(1); } } diff --git a/language-web/src/main/java/tools/refinery/language/web/xtext/HttpServiceContext.java b/language-web/src/main/java/tools/refinery/language/web/xtext/HttpServiceContext.java new file mode 100644 index 00000000..d0ba6a2d --- /dev/null +++ b/language-web/src/main/java/tools/refinery/language/web/xtext/HttpServiceContext.java @@ -0,0 +1,107 @@ +/** + * Copyright (c) 2015, 2020 itemis AG (http://www.itemis.eu) and others. + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.language.web.xtext; + +import java.io.IOException; +import java.net.URLDecoder; +import java.nio.charset.Charset; +import java.util.Collections; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +import org.eclipse.xtext.web.server.IServiceContext; +import org.eclipse.xtext.web.server.ISession; + +import com.google.common.io.CharStreams; + +import jakarta.servlet.http.HttpServletRequest; + +/** + * Provides the parameters and metadata of an {@link HttpServletRequest}. + */ +class HttpServiceContext implements IServiceContext { + private final HttpServletRequest request; + + private final Map parameters = new HashMap<>(); + + private HttpSessionWrapper sessionWrapper; + + public HttpServiceContext(HttpServletRequest request) throws IOException { + this.request = request; + initializeParameters(); + } + + private void initializeParameters() throws IOException { + initializeUrlEncodedParameters(); + initializeRequestParameters(); + if (!parameters.containsKey(IServiceContext.SERVICE_TYPE)) { + String substring = null; + if (request.getPathInfo() != null) { + substring = request.getPathInfo().substring(1); + } + parameters.put(IServiceContext.SERVICE_TYPE, substring); + } + } + + private void initializeUrlEncodedParameters() throws IOException { + String[] contentType = null; + if (request.getContentType() != null) { + contentType = request.getContentType().split(";(\\s*)"); + } + if (contentType != null && "application/x-www-form-urlencoded".equals(contentType[0])) { + String charset = null; + if (contentType.length >= 2 && contentType[1].startsWith("charset=")) { + charset = (contentType[1]).substring("charset=".length()); + } else { + charset = Charset.defaultCharset().toString(); + } + String[] encodedParams = CharStreams.toString(request.getReader()).split("&"); + for (String param : encodedParams) { + int nameEnd = param.indexOf("="); + if (nameEnd > 0) { + String key = param.substring(0, nameEnd); + String value = URLDecoder.decode(param.substring(nameEnd + 1), charset); + parameters.put(key, value); + } + } + } + } + + private void initializeRequestParameters() { + Enumeration paramNames = request.getParameterNames(); + while (paramNames.hasMoreElements()) { + String name = paramNames.nextElement(); + parameters.put(name, request.getParameter(name)); + } + } + + @Override + public Set getParameterKeys() { + return Collections.unmodifiableSet(parameters.keySet()); + } + + @Override + public String getParameter(String key) { + return parameters.get(key); + } + + @Override + public ISession getSession() { + if (sessionWrapper == null) { + sessionWrapper = new HttpSessionWrapper(request.getSession(true)); + } + return sessionWrapper; + } + + public HttpServletRequest getRequest() { + return request; + } +} diff --git a/language-web/src/main/java/tools/refinery/language/web/xtext/HttpSessionWrapper.java b/language-web/src/main/java/tools/refinery/language/web/xtext/HttpSessionWrapper.java new file mode 100644 index 00000000..8a5e19ba --- /dev/null +++ b/language-web/src/main/java/tools/refinery/language/web/xtext/HttpSessionWrapper.java @@ -0,0 +1,53 @@ +/** + * Copyright (c) 2015, 2020 itemis AG (http://www.itemis.eu) and others. + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.language.web.xtext; + +import org.eclipse.xtext.web.server.ISession; +import org.eclipse.xtext.xbase.lib.Functions.Function0; + +import jakarta.servlet.http.HttpSession; + +/** + * Provides access to the information stored in a {@link HttpSession}. + */ +record HttpSessionWrapper(HttpSession session) implements ISession { + @SuppressWarnings("unchecked") + @Override + public T get(Object key) { + return (T) session.getAttribute(key.toString()); + } + + @Override + public T get(Object key, Function0 factory) { + synchronized (session) { + T sessionValue = get(key); + if (sessionValue != null) { + return sessionValue; + } else { + T factoryValue = factory.apply(); + put(key, factoryValue); + return factoryValue; + } + } + } + + @Override + public void put(Object key, Object value) { + session.setAttribute(key.toString(), value); + } + + @Override + public void remove(Object key) { + session.removeAttribute(key.toString()); + } + + public HttpSession getSession() { + return session; + } +} diff --git a/language-web/src/main/java/tools/refinery/language/web/xtext/XtextServlet.java b/language-web/src/main/java/tools/refinery/language/web/xtext/XtextServlet.java new file mode 100644 index 00000000..f39bec12 --- /dev/null +++ b/language-web/src/main/java/tools/refinery/language/web/xtext/XtextServlet.java @@ -0,0 +1,196 @@ +/** + * Copyright (c) 2015, 2020 itemis AG (http://www.itemis.eu) and others. + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.language.web.xtext; + +import java.io.IOException; +import java.util.Set; + +import org.eclipse.emf.common.util.URI; +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.IUnwrappableServiceResult; +import org.eclipse.xtext.web.server.InvalidRequestException; +import org.eclipse.xtext.web.server.XtextServiceDispatcher; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.common.base.Objects; +import com.google.common.base.Strings; +import com.google.gson.Gson; +import com.google.inject.Injector; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +/** + * An HTTP servlet for publishing the Xtext services. Include this into your web + * server by creating a subclass that executes the standalone setups of your + * languages in its {@link #init()} method: + * + *
+ * @WebServlet(name = "Xtext Services", urlPatterns = "/xtext-service/*")
+ * class MyXtextServlet extends XtextServlet {
+ * 	override init() {
+ * 		super.init();
+ * 		MyDslWebSetup.doSetup();
+ * 	}
+ * }
+ * 
+ * + * Use the {@code WebServlet} annotation to register your servlet. The default + * URL pattern for Xtext services is {@code "/xtext-service/*"}. + */ +public class XtextServlet extends HttpServlet { + + private static final long serialVersionUID = 7784324070547781918L; + + private static final IResourceServiceProvider.Registry SERVICE_PROVIDER_REGISTRY = IResourceServiceProvider.Registry.INSTANCE; + + private static final String ENCODING = "UTF-8"; + + private static final String INVALID_REQUEST_MESSAGE = "Invalid request ({}): {}"; + + private final transient Logger log = LoggerFactory.getLogger(this.getClass()); + + private final transient Gson gson = new Gson(); + + @Override + protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + try { + super.service(req, resp); + } catch (InvalidRequestException.ResourceNotFoundException exception) { + log.trace(INVALID_REQUEST_MESSAGE, req.getRequestURI(), exception.getMessage()); + resp.sendError(HttpServletResponse.SC_NOT_FOUND, exception.getMessage()); + } catch (InvalidRequestException.InvalidDocumentStateException exception) { + log.trace(INVALID_REQUEST_MESSAGE, req.getRequestURI(), exception.getMessage()); + resp.sendError(HttpServletResponse.SC_CONFLICT, exception.getMessage()); + } catch (InvalidRequestException.PermissionDeniedException exception) { + log.trace(INVALID_REQUEST_MESSAGE, req.getRequestURI(), exception.getMessage()); + resp.sendError(HttpServletResponse.SC_FORBIDDEN, exception.getMessage()); + } catch (InvalidRequestException exception) { + log.trace(INVALID_REQUEST_MESSAGE, req.getRequestURI(), exception.getMessage()); + resp.sendError(HttpServletResponse.SC_BAD_REQUEST, exception.getMessage()); + } + } + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + XtextServiceDispatcher.ServiceDescriptor service = getService(req); + if (!service.isHasConflict() && (service.isHasSideEffects() || hasTextInput(service))) { + super.doGet(req, resp); + } else { + doService(service, resp); + } + } + + @Override + protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + XtextServiceDispatcher.ServiceDescriptor service = getService(req); + String type = service.getContext().getParameter(IServiceContext.SERVICE_TYPE); + if (!service.isHasConflict() && !Objects.equal(type, "update")) { + super.doPut(req, resp); + } else { + doService(service, resp); + } + } + + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + XtextServiceDispatcher.ServiceDescriptor service = getService(req); + String type = service.getContext().getParameter(IServiceContext.SERVICE_TYPE); + if (!service.isHasConflict() + && (!service.isHasSideEffects() && !hasTextInput(service) || Objects.equal(type, "update"))) { + super.doPost(req, resp); + } else { + doService(service, resp); + } + } + + protected boolean hasTextInput(XtextServiceDispatcher.ServiceDescriptor service) { + Set parameterKeys = service.getContext().getParameterKeys(); + return parameterKeys.contains("fullText") || parameterKeys.contains("deltaText"); + } + + /** + * Retrieve the service metadata for the given request. This involves resolving + * the Guice injector for the respective language, querying the + * {@link XtextServiceDispatcher}, and checking the permission to invoke the + * service. + */ + protected XtextServiceDispatcher.ServiceDescriptor getService(HttpServletRequest request) throws IOException { + HttpServiceContext serviceContext = new HttpServiceContext(request); + Injector injector = getInjector(serviceContext); + XtextServiceDispatcher serviceDispatcher = injector.getInstance(XtextServiceDispatcher.class); + return serviceDispatcher.getService(serviceContext); + } + + /** + * Invoke the service function of the given service descriptor and write its + * result to the servlet response in Json format. An exception is made for + * {@link IUnwrappableServiceResult}: here the document itself is written into + * the response instead of wrapping it into a Json object. + */ + protected void doService(XtextServiceDispatcher.ServiceDescriptor service, HttpServletResponse response) + throws IOException { + IServiceResult result = service.getService().apply(); + response.setStatus(HttpServletResponse.SC_OK); + response.setCharacterEncoding(ENCODING); + response.setHeader("Cache-Control", "no-cache"); + if (result instanceof IUnwrappableServiceResult unwrapResult && unwrapResult.getContent() != null) { + String contentType = null; + if (unwrapResult.getContentType() != null) { + contentType = unwrapResult.getContentType(); + } else { + contentType = "text/plain"; + } + response.setContentType(contentType); + response.getWriter().write(unwrapResult.getContent()); + } else { + response.setContentType("text/x-json"); + gson.toJson(result, response.getWriter()); + } + } + + /** + * Resolve the Guice injector for the language associated with the given + * context. + */ + protected Injector getInjector(HttpServiceContext serviceContext) + throws InvalidRequestException.UnknownLanguageException { + IResourceServiceProvider resourceServiceProvider = null; + String parameter = serviceContext.getParameter("resource"); + if (parameter == null) { + parameter = ""; + } + URI emfURI = URI.createURI(parameter); + String contentType = serviceContext.getParameter("contentType"); + if (Strings.isNullOrEmpty(contentType)) { + resourceServiceProvider = SERVICE_PROVIDER_REGISTRY.getResourceServiceProvider(emfURI); + if (resourceServiceProvider == null) { + if (emfURI.toString().isEmpty()) { + throw new InvalidRequestException.UnknownLanguageException( + "Unable to identify the Xtext language: missing parameter 'resource' or 'contentType'."); + } else { + throw new InvalidRequestException.UnknownLanguageException( + "Unable to identify the Xtext language for resource " + emfURI + "."); + } + } + } else { + resourceServiceProvider = SERVICE_PROVIDER_REGISTRY.getResourceServiceProvider(emfURI, contentType); + if (resourceServiceProvider == null) { + throw new InvalidRequestException.UnknownLanguageException( + "Unable to identify the Xtext language for contentType " + contentType + "."); + } + } + return resourceServiceProvider.get(Injector.class); + } +} \ No newline at end of file -- cgit v1.2.3-54-g00ecf