WorkbenchServlet.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 java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import javax.servlet.Servlet;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.eclipse.rdf4j.common.exception.ValidationException;
import org.eclipse.rdf4j.http.protocol.UnauthorizedException;
import org.eclipse.rdf4j.model.Model;
import org.eclipse.rdf4j.query.QueryResultHandlerException;
import org.eclipse.rdf4j.repository.Repository;
import org.eclipse.rdf4j.repository.RepositoryException;
import org.eclipse.rdf4j.repository.config.RepositoryConfigException;
import org.eclipse.rdf4j.repository.manager.LocalRepositoryManager;
import org.eclipse.rdf4j.repository.manager.RemoteRepositoryManager;
import org.eclipse.rdf4j.repository.manager.RepositoryManager;
import org.eclipse.rdf4j.rio.RDFFormat;
import org.eclipse.rdf4j.rio.Rio;
import org.eclipse.rdf4j.rio.WriterConfig;
import org.eclipse.rdf4j.rio.helpers.BasicWriterSettings;
import org.eclipse.rdf4j.workbench.base.AbstractServlet;
import org.eclipse.rdf4j.workbench.exceptions.BadRequestException;
import org.eclipse.rdf4j.workbench.exceptions.MissingInitParameterException;
import org.eclipse.rdf4j.workbench.util.BasicServletConfig;
import org.eclipse.rdf4j.workbench.util.DynamicHttpRequest;
import org.eclipse.rdf4j.workbench.util.TupleResultBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class WorkbenchServlet extends AbstractServlet {
private static final Logger LOGGER = LoggerFactory.getLogger(WorkbenchServlet.class);
private static final String DEFAULT_PATH = "default-path";
private static final String NO_REPOSITORY = "no-repository-id";
public static final String SERVER_PARAM = "server";
private RepositoryManager manager;
private final ConcurrentMap<String, ProxyRepositoryServlet> repositories = new ConcurrentHashMap<>();
@Override
public void init(final ServletConfig config) throws ServletException {
this.config = config;
if (config.getInitParameter(DEFAULT_PATH) == null) {
throw new MissingInitParameterException(DEFAULT_PATH);
}
final String param = config.getInitParameter(SERVER_PARAM);
if (param == null || param.trim().isEmpty()) {
throw new MissingInitParameterException(SERVER_PARAM);
}
try {
manager = createRepositoryManager(param);
} catch (IOException | RepositoryException e) {
throw new ServletException(e);
}
}
@Override
public void destroy() {
for (Servlet servlet : repositories.values()) {
servlet.destroy();
}
manager.shutDown();
}
public void resetCache() {
for (ProxyRepositoryServlet proxy : repositories.values()) {
// inform browser that server changed and cache is invalid
proxy.resetCache();
}
}
@Override
public void service(final HttpServletRequest req, final HttpServletResponse resp)
throws ServletException, IOException {
final String pathInfo = req.getPathInfo();
if (pathInfo == null) {
final String defaultPath = config.getInitParameter(DEFAULT_PATH);
resp.sendRedirect(req.getRequestURI() + defaultPath);
} else if ("/".equals(pathInfo)) {
final String defaultPath = config.getInitParameter(DEFAULT_PATH);
resp.sendRedirect(req.getRequestURI() + defaultPath.substring(1));
} else if ('/' == pathInfo.charAt(0)) {
try {
handleRequest(req, resp, pathInfo);
} catch (QueryResultHandlerException e) {
throw new IOException(e);
}
} else {
throw new BadRequestException("Request path must contain a repository ID");
}
}
/**
* @param req the servlet request
* @param resp the servlet response
* @param pathInfo the path info from the request
* @throws IOException
* @throws ServletException
* @throws QueryResultHandlerException
*/
private void handleRequest(final HttpServletRequest req, final HttpServletResponse resp, final String pathInfo)
throws IOException, ServletException, QueryResultHandlerException {
int idx = pathInfo.indexOf('/', 1);
if (idx < 0) {
idx = pathInfo.length();
}
final String repoID = pathInfo.substring(1, idx);
try {
service(repoID, req, resp);
} catch (UnauthorizedException e) {
handleUnauthorizedException(req, resp);
} catch (RepositoryConfigException | RepositoryException e) {
if (e.getCause() instanceof ValidationException) {
Model model = ((ValidationException) e.getCause()).validationReportAsModel();
resp.setStatus(HttpServletResponse.SC_CONFLICT);
resp.setContentType(TEXT_PLAIN);
PrintWriter writer = resp.getWriter();
writer.println("SHACL validation failed with the following report:\n");
WriterConfig writerConfig = new WriterConfig();
writerConfig.set(BasicWriterSettings.PRETTY_PRINT, true);
writerConfig.set(BasicWriterSettings.INLINE_BLANK_NODES, true);
Rio.write(model, writer, RDFFormat.TURTLE, writerConfig);
writer.println(
"\n" +
"THIS ERROR MESSAGE IS EXPERIMENTAL AND IS SUBJECT TO CHANGE - " +
"DO NOT TRY TO PARSE THIS ERROR MESSAGE");
} else {
throw new ServletException(e);
}
} catch (ServletException e) {
if (e.getCause() instanceof UnauthorizedException) {
handleUnauthorizedException(req, resp);
} else {
throw e;
}
}
}
/**
* @param req
* @param resp
* @throws IOException
* @throws QueryResultHandlerException
*/
private void handleUnauthorizedException(final HttpServletRequest req, final HttpServletResponse resp)
throws IOException, QueryResultHandlerException {
// Invalid credentials or insufficient authorization. Present
// entry form again with error message.
final TupleResultBuilder builder = getTupleResultBuilder(req, resp, resp.getOutputStream());
builder.transform(this.getTransformationUrl(req), "server.xsl");
builder.start("error-message");
builder.result(
"The entered credentials entered either failed to authenticate to the RDF4J server, or were unauthorized for the requested operation.");
builder.end();
}
private RepositoryManager createRepositoryManager(final String param) throws IOException, RepositoryException {
RepositoryManager manager;
if (param.startsWith("file:")) {
manager = new LocalRepositoryManager(asLocalFile(new URL(param)));
} else {
manager = new RemoteRepositoryManager(param);
}
manager.init();
return manager;
}
private File asLocalFile(final URL rdf) {
return new File(URLDecoder.decode(rdf.getFile(), StandardCharsets.UTF_8));
}
private void service(final String repoID, final HttpServletRequest req, final HttpServletResponse resp)
throws RepositoryConfigException, RepositoryException, ServletException, IOException {
LOGGER.info("Servicing repository: {}", repoID);
setCredentials(req, resp);
final DynamicHttpRequest http = new DynamicHttpRequest(req);
final String path = req.getPathInfo();
final int idx = path.indexOf(repoID) + repoID.length();
http.setServletPath(http.getServletPath() + path.substring(0, idx));
final String pathInfo = path.substring(idx);
http.setPathInfo(pathInfo.isEmpty() ? null : pathInfo);
if (repositories.containsKey(repoID)) {
repositories.get(repoID).service(http, resp);
} else {
final Repository repository = manager.getRepository(repoID);
if (repository == null) {
final String noId = config.getInitParameter(NO_REPOSITORY);
if (noId == null || !noId.equals(repoID)) {
resp.setHeader("Cache-Control", "no-cache, no-store");
throw new BadRequestException("No such repository: " + repoID);
}
}
final ProxyRepositoryServlet servlet = new ProxyRepositoryServlet();
servlet.setRepositoryManager(manager);
if (repository != null) {
servlet.setRepositoryInfo(manager.getRepositoryInfo(repoID));
servlet.setRepository(repository);
}
servlet.init(new BasicServletConfig(repoID, config));
repositories.putIfAbsent(repoID, servlet);
repositories.get(repoID).service(http, resp);
}
}
private String getTransformationUrl(final HttpServletRequest req) {
final String contextPath = req.getContextPath();
return contextPath + config.getInitParameter(WorkbenchGateway.TRANSFORMATIONS);
}
/**
* Set the username and password for all requests to the repository.
*
* @param req the servlet request
* @param resp the servlet response
* @throws MalformedURLException if the repository location is malformed
*/
private void setCredentials(final HttpServletRequest req, final HttpServletResponse resp)
throws MalformedURLException, RepositoryException {
if (manager instanceof RemoteRepositoryManager) {
final RemoteRepositoryManager rrm = (RemoteRepositoryManager) manager;
LOGGER.info("RemoteRepositoryManager URL: {}", rrm.getLocation());
final CookieHandler cookies = new CookieHandler(config);
final String user_password = cookies.getCookieNullIfEmpty(req, resp, WorkbenchGateway.SERVER_USER_PASSWORD);
if (user_password == null) {
rrm.setUsernameAndPassword(null, null);
} else {
String decoded;
try {
decoded = new String(Base64.getDecoder().decode(user_password));
} catch (IllegalArgumentException e) {
decoded = user_password; // older browsers
}
final String user = decoded.substring(0, decoded.indexOf(':'));
final String password = decoded.substring(decoded.indexOf(':') + 1);
LOGGER.info("Setting user '{}' and their password.", user);
rrm.setUsernameAndPassword(user, password);
}
// init() required to push credentials to internal HTTP
// client.
rrm.init();
}
}
}