AbstractQueryRequestHandler.java
/*******************************************************************************
* Copyright (c) 2022 Eclipse RDF4J contributors.
*
* 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.http.server.repository.handler;
import static javax.servlet.http.HttpServletResponse.SC_SERVICE_UNAVAILABLE;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.eclipse.rdf4j.common.lang.FileFormat;
import org.eclipse.rdf4j.common.lang.service.FileFormatServiceRegistry;
import org.eclipse.rdf4j.http.protocol.Protocol;
import org.eclipse.rdf4j.http.server.ClientHTTPException;
import org.eclipse.rdf4j.http.server.HTTPException;
import org.eclipse.rdf4j.http.server.ProtocolUtil;
import org.eclipse.rdf4j.http.server.ServerHTTPException;
import org.eclipse.rdf4j.http.server.repository.QueryResultView;
import org.eclipse.rdf4j.http.server.repository.resolver.RepositoryResolver;
import org.eclipse.rdf4j.query.Query;
import org.eclipse.rdf4j.query.QueryEvaluationException;
import org.eclipse.rdf4j.query.QueryInterruptedException;
import org.eclipse.rdf4j.repository.Repository;
import org.eclipse.rdf4j.repository.RepositoryConnection;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.View;
/**
* A base implementation to handle an HTTP query request.
*/
public abstract class AbstractQueryRequestHandler implements QueryRequestHandler {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
private final RepositoryResolver repositoryResolver;
public AbstractQueryRequestHandler(RepositoryResolver repositoryResolver) {
this.repositoryResolver = repositoryResolver;
}
@Override
public ModelAndView handleQueryRequest(HttpServletRequest request, RequestMethod requestMethod,
HttpServletResponse response) throws HTTPException, IOException {
RepositoryConnection repositoryCon = null;
Object queryResponse = null;
try {
Repository repository = repositoryResolver.getRepository(request);
repositoryCon = repositoryResolver.getRepositoryConnection(request, repository);
String queryString = getQueryString(request, requestMethod);
logQuery(requestMethod, queryString);
Query query = getQuery(request, repositoryCon, queryString);
boolean headersOnly = requestMethod == RequestMethod.HEAD;
long limit = getLimit(request);
long offset = getOffset(request);
boolean distinct = isDistinct(request);
try {
if (headersOnly) {
queryResponse = null;
} else {
queryResponse = evaluateQuery(query, limit, offset, distinct);
}
FileFormatServiceRegistry<? extends FileFormat, ?> registry = getResultWriterFor(query);
if (registry == null) {
throw new UnsupportedOperationException(
"Unknown result writer for query of type: " + query.getClass().getName());
}
View view = getViewFor(query);
if (view == null) {
throw new UnsupportedOperationException(
"Unknown view for query of type: " + query.getClass().getName());
}
return getModelAndView(request, response, headersOnly, repositoryCon, view, queryResponse, registry);
} catch (QueryInterruptedException e) {
logger.info("Query interrupted", e);
throw new ServerHTTPException(SC_SERVICE_UNAVAILABLE, "Query evaluation took too long");
} catch (QueryEvaluationException e) {
logger.info("Query evaluation error", e);
if (e.getCause() != null && e.getCause() instanceof HTTPException) {
// custom signal from the backend, throw as HTTPException
// directly (see SES-1016).
throw (HTTPException) e.getCause();
} else {
throw new ServerHTTPException("Query evaluation error: " + e.getMessage());
}
}
} catch (Exception e) {
// only close the response & connection when an exception occurs. Otherwise, the QueryResultView will take
// care of closing it.
try {
if (queryResponse instanceof AutoCloseable) {
((AutoCloseable) queryResponse).close();
}
} catch (Exception qre) {
logger.warn("Query response closing error", qre);
} finally {
try {
if (repositoryCon != null) {
repositoryCon.close();
}
} catch (Exception qre) {
logger.warn("Connection closing error", qre);
}
}
throw e;
}
}
abstract protected Object evaluateQuery(Query query, long limit, long offset, boolean distinct)
throws ClientHTTPException;
abstract protected View getViewFor(Query query);
abstract protected FileFormatServiceRegistry<? extends FileFormat, ?> getResultWriterFor(Query query);
abstract protected String getQueryString(HttpServletRequest request, RequestMethod requestMethod)
throws HTTPException;
abstract protected Query getQuery(HttpServletRequest request, RepositoryConnection repositoryCon,
String queryString) throws IOException, HTTPException;
protected ModelAndView getModelAndView(HttpServletRequest request, HttpServletResponse response,
boolean headersOnly, RepositoryConnection repositoryCon, View view, Object queryResult,
FileFormatServiceRegistry<? extends FileFormat, ?> registry) throws ClientHTTPException {
Map<String, Object> model = new HashMap<>();
model.put(QueryResultView.FILENAME_HINT_KEY, "query-result");
model.put(QueryResultView.QUERY_RESULT_KEY, queryResult);
model.put(QueryResultView.FACTORY_KEY, ProtocolUtil.getAcceptableService(request, response, registry));
model.put(QueryResultView.HEADERS_ONLY, headersOnly);
model.put(QueryResultView.CONNECTION_KEY, repositoryCon);
return new ModelAndView(view, model);
}
protected boolean isDistinct(HttpServletRequest request) throws ClientHTTPException {
return getParam(request, Protocol.DISTINCT_PARAM_NAME, false, Boolean.TYPE);
}
protected long getOffset(HttpServletRequest request) throws ClientHTTPException {
return getParam(request, Protocol.OFFSET_PARAM_NAME, 0L, Long.TYPE);
}
protected long getLimit(HttpServletRequest request) throws ClientHTTPException {
return getParam(request, Protocol.LIMIT_PARAM_NAME, 0L, Long.TYPE);
}
<T> T getParam(HttpServletRequest request, String distinctParamName, T defaultValue, Class<T> clazz)
throws ClientHTTPException {
if (clazz == Boolean.TYPE) {
return (T) (Boolean) ProtocolUtil.parseBooleanParam(request, distinctParamName, (Boolean) defaultValue);
}
if (clazz == Long.TYPE) {
return (T) (Long) ProtocolUtil.parseLongParam(request, distinctParamName, (Long) defaultValue);
}
throw new UnsupportedOperationException("Class not supported: " + clazz);
}
private void logQuery(RequestMethod requestMethod, String queryString) {
if (logger.isInfoEnabled() || logger.isDebugEnabled()) {
int queryHashCode = queryString.hashCode();
switch (requestMethod) {
case GET:
logger.info("GET query {}", queryHashCode);
break;
case HEAD:
logger.info("HEAD query {}", queryHashCode);
break;
case POST:
logger.info("POST query {}", queryHashCode);
break;
}
logger.debug("query {} = {}", queryHashCode, queryString);
}
}
}