WorkbenchGateway.java
/*******************************************************************************
* Copyright (c) 2015 Eclipse RDF4J contributors, Aduna, and others.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Distribution License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/org/documents/edl-v10.php.
*
* SPDX-License-Identifier: BSD-3-Clause
*******************************************************************************/
package org.eclipse.rdf4j.workbench.proxy;
import static org.eclipse.rdf4j.workbench.proxy.WorkbenchServlet.SERVER_PARAM;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.annotation.MultipartConfig;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.eclipse.rdf4j.query.QueryResultHandlerException;
import org.eclipse.rdf4j.workbench.base.AbstractServlet;
import org.eclipse.rdf4j.workbench.exceptions.MissingInitParameterException;
import org.eclipse.rdf4j.workbench.util.BasicServletConfig;
import org.eclipse.rdf4j.workbench.util.TupleResultBuilder;
/**
* All requests are serviced by this Servlet, though it usually delegates to other Servlets.
*/
@MultipartConfig
public class WorkbenchGateway extends AbstractServlet {
private static final String DEFAULT_SERVER = "default-server";
private static final String CHANGE_SERVER = "change-server-path";
private static final String SERVER_COOKIE = "workbench-server";
protected static final String TRANSFORMATIONS = "transformations";
/**
* Thread-safe map of server paths to their WorkbenchServlet instances.
*/
private final Map<String, WorkbenchServlet> servlets = new ConcurrentHashMap<>();
private CookieHandler cookies;
private ServerValidator serverValidator;
@Override
public void init(final ServletConfig config) throws ServletException {
super.init(config);
if (getDefaultServerPath() == null) {
throw new MissingInitParameterException(DEFAULT_SERVER);
}
if (config.getInitParameter(TRANSFORMATIONS) == null) {
throw new MissingInitParameterException(TRANSFORMATIONS);
}
this.cookies = new CookieHandler(config);
this.serverValidator = new ServerValidator(config);
}
@Override
public void destroy() {
for (WorkbenchServlet servlet : servlets.values()) {
servlet.destroy();
}
}
public String getChangeServerPath() {
return config.getInitParameter(CHANGE_SERVER);
}
/**
* Returns the value of the default-server configuration variable. Often, this is simply a relative path on the same
* HTTP server.
*
* @return the path to the default Sesame server instance
*/
public String getDefaultServerPath() {
return config.getInitParameter(DEFAULT_SERVER);
}
/**
* Whether the server path is fixed, which is when the change-server-path configuration value is not set.
*
* @return true, if the change-server-path configuration variable is not set, meaning that changing the server is
* blocked
*/
public boolean isServerFixed() {
return getChangeServerPath() == null;
}
@Override
public void service(final HttpServletRequest req, final HttpServletResponse resp)
throws ServletException, IOException {
final String change = getChangeServerPath();
if (change != null && change.equals(req.getPathInfo())) {
try {
changeServer(req, resp);
} catch (QueryResultHandlerException e) {
throw new IOException(e);
}
} else {
final WorkbenchServlet servlet = findWorkbenchServlet(req, resp);
if (servlet == null) {
// Redirect to change-server-path
final StringBuilder uri = new StringBuilder(req.getRequestURI());
if (req.getPathInfo() != null) {
uri.setLength(uri.length() - req.getPathInfo().length());
}
resp.sendRedirect(uri.append(getChangeServerPath()).toString());
} else {
servlet.service(req, resp);
}
}
}
private void resetCache() {
for (WorkbenchServlet servlet : servlets.values()) {
// inform browser that server changed and cache is invalid
servlet.resetCache();
}
}
/**
* Handles requests to the "change server" page.
*
* @param req the servlet request object
* @param resp the servlet response object
* @throws IOException if an issue occurs writing to the response
* @throws QueryResultHandlerException
*/
private void changeServer(final HttpServletRequest req, final HttpServletResponse resp)
throws IOException, QueryResultHandlerException {
String server = req.getParameter(SERVER_COOKIE);
if (server == null) {
// Server parameter was not present, so present entry form.
final TupleResultBuilder builder = getTupleResultBuilder(req, resp, resp.getOutputStream());
builder.transform(getTransformationUrl(req), "server.xsl");
builder.start("server");
// see if server url was still present in cookie, if so use that
// value as prefilled value in the form
String currentServer = this.cookies.getCookie(req, resp, SERVER_COOKIE);
if (currentServer == null) {
// otherwise use the default
currentServer = getDefaultServer(req);
}
builder.result(currentServer);
builder.end();
return;
}
server = server.trim();
if (!this.serverValidator.isValidServer(server)) {
// Invalid server was submitted by form. Present entry form again
// with error message.
final TupleResultBuilder builder = getTupleResultBuilder(req, resp, resp.getOutputStream());
builder.transform(getTransformationUrl(req), "server.xsl");
builder.start("error-message", "server");
builder.result("Invalid Server URL", server);
builder.end();
return;
}
// Valid server was submitted by form. Set cookie and redirect to
// repository selection page.
this.cookies.addNewCookie(req, resp, SERVER_COOKIE, server);
final String user_password = getOptionalParameter(req, SERVER_USER_PASSWORD);
this.cookies.addNewCookie(req, resp, SERVER_USER_PASSWORD, user_password);
final StringBuilder uri = new StringBuilder(req.getRequestURI());
uri.setLength(uri.length() - req.getPathInfo().length());
resetCache();
resp.sendRedirect(uri.toString());
}
/**
* @param req the servlet request
* @param name the name of the optional parameter
* @return the value of the parameter, or an empty String if it is not present.
*/
private String getOptionalParameter(final HttpServletRequest req, final String name) {
String value = req.getParameter(name);
if (value == null) {
value = "";
}
return value;
}
/**
* Returns the user requested server, if valid, or the default server.
*
* @param req the request
* @param resp the response
* @return the user's requested server, if valid, or the default server
*/
private String findServer(final HttpServletRequest req, final HttpServletResponse resp) {
final StringBuilder value = new StringBuilder();
if (isServerFixed()) {
value.append(getDefaultServer(req));
} else {
value.append(cookies.getCookie(req, resp, SERVER_COOKIE));
if (0 == value.length()) {
value.append(getDefaultServer(req));
} else if (!this.serverValidator.isValidServer(value.toString())) {
value.replace(0, value.length(), getDefaultServer(req));
}
}
return value.toString();
}
/**
* Returns a WorkbenchServlet instance allocated for the requested server.
*
* @param req the current request
* @param resp the current response
* @return a WorkbenchServlet instance allocated for the requested server
* @throws ServletException if a problem occurs initializing a new servlet
*/
private WorkbenchServlet findWorkbenchServlet(final HttpServletRequest req, final HttpServletResponse resp)
throws ServletException {
WorkbenchServlet servlet = null;
final String server = findServer(req, resp);
if (servlets.containsKey(server)) {
servlet = servlets.get(server);
} else {
if (isServerFixed() || this.serverValidator.isValidServer(server)) {
synchronized (servlets) {
// Even though the map is thread-safe, we only wish one
// thread to be in this block at a time, to avoid abandoning
// a WorkbenchServlet instance to the garbage collector.
if (servlets.containsKey(server)) {
servlet = servlets.get(server);
} else {
final Map<String, String> params = new HashMap<>(3);
params.put(SERVER_PARAM, server);
params.put(CookieHandler.COOKIE_AGE_PARAM, this.cookies.getMaxAge());
params.put(TRANSFORMATIONS, this.config.getInitParameter(TRANSFORMATIONS));
final ServletConfig cfg = new BasicServletConfig(server, config, params);
servlet = new WorkbenchServlet();
servlet.init(cfg);
servlets.put(server, servlet);
}
}
}
}
return servlet;
}
/**
* Returns the full URL to the default server on the same server as the given request.
*
* @param req the request to find the default server relative to
* @return the full URL to the default server on the same server as the given request
*/
private String getDefaultServer(final HttpServletRequest req) {
String server = getDefaultServerPath();
if ('/' == server.charAt(0)) {
final StringBuffer url = req.getRequestURL();
final StringBuilder path = getServerPath(req);
url.setLength(url.indexOf(path.toString()));
server = url.append(server).toString();
}
return server;
}
/**
* Returns the full path for the given request.
*
* @param req the request for which the path is sought
* @return the full path for the given request
*/
private StringBuilder getServerPath(final HttpServletRequest req) {
final StringBuilder path = new StringBuilder();
if (req.getContextPath() != null) {
path.append(req.getContextPath());
}
if (req.getServletPath() != null) {
path.append(req.getServletPath());
}
if (req.getPathInfo() != null) {
path.append(req.getPathInfo());
}
return path;
}
private String getTransformationUrl(final HttpServletRequest req) {
final String contextPath = req.getContextPath();
return contextPath + config.getInitParameter(TRANSFORMATIONS);
}
}