aboutsummaryrefslogtreecommitdiffstats
path: root/language-web/src/main/java/tools/refinery/language/web/xtext/XtextServlet.java
blob: f39bec12479aec451c36408cf785283c8792b0c6 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
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:
 * 
 * <pre>
 * &#64;WebServlet(name = "Xtext Services", urlPatterns = "/xtext-service/*")
 * class MyXtextServlet extends XtextServlet {
 * 	override init() {
 * 		super.init();
 * 		MyDslWebSetup.doSetup();
 * 	}
 * }
 * </pre>
 * 
 * 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<String> 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);
	}
}