VIVO-731 Implement the SPARQL query API.
Create a base class for API controller servlets.
This commit is contained in:
parent
de32d53791
commit
8892da74b0
5 changed files with 217 additions and 65 deletions
|
@ -76,6 +76,8 @@ public class SimplePermission extends Permission {
|
|||
NAMESPACE + "UseIndividualControlPanel");
|
||||
public static final SimplePermission USE_SPARQL_QUERY_PAGE = new SimplePermission(
|
||||
NAMESPACE + "UseSparqlQueryPage");
|
||||
public static final SimplePermission USE_SPARQL_QUERY_API = new SimplePermission(
|
||||
NAMESPACE + "UseSparqlQueryApi");
|
||||
public static final SimplePermission USE_SPARQL_UPDATE_API = new SimplePermission(
|
||||
NAMESPACE + "UseSparqlUpdateApi");
|
||||
|
||||
|
|
|
@ -0,0 +1,104 @@
|
|||
/* $This file is distributed under the terms of the license in /doc/license.txt$ */
|
||||
|
||||
package edu.cornell.mannlib.vitro.webapp.controller.api;
|
||||
|
||||
import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST;
|
||||
import static javax.servlet.http.HttpServletResponse.SC_FORBIDDEN;
|
||||
import static javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
|
||||
import static javax.servlet.http.HttpServletResponse.SC_NOT_ACCEPTABLE;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import com.hp.hpl.jena.query.QueryParseException;
|
||||
|
||||
import edu.cornell.mannlib.vitro.webapp.auth.permissions.SimplePermission;
|
||||
import edu.cornell.mannlib.vitro.webapp.auth.requestedAction.Actions;
|
||||
import edu.cornell.mannlib.vitro.webapp.controller.api.sparqlquery.InvalidQueryTypeException;
|
||||
import edu.cornell.mannlib.vitro.webapp.controller.api.sparqlquery.SparqlQueryApiExecutor;
|
||||
import edu.cornell.mannlib.vitro.webapp.rdfservice.RDFService;
|
||||
import edu.cornell.mannlib.vitro.webapp.rdfservice.RDFServiceException;
|
||||
import edu.cornell.mannlib.vitro.webapp.rdfservice.impl.RDFServiceUtils;
|
||||
import edu.cornell.mannlib.vitro.webapp.utils.http.AcceptHeaderParsingException;
|
||||
import edu.cornell.mannlib.vitro.webapp.utils.http.NotAcceptableException;
|
||||
|
||||
/**
|
||||
* Process SPARQL queries as an API.
|
||||
*
|
||||
* Supports GET or POST requests. May produce these responses:
|
||||
*
|
||||
* <pre>
|
||||
* 200 Success
|
||||
* 400 Failed to parse SPARQL query
|
||||
* 400 SPARQL query type is not SELECT, ASK, CONSTRUCT, or DESCRIBE.
|
||||
* 403 username/password combination is not valid
|
||||
* 403 Account is not authorized
|
||||
* 406 Accept header does not include any available result formats
|
||||
* 500 Unknown error
|
||||
* </pre>
|
||||
*/
|
||||
public class SparqlQueryApiController extends VitroApiServlet {
|
||||
|
||||
private static final Actions REQUIRED_ACTIONS = SimplePermission.USE_SPARQL_QUERY_API.ACTIONS;
|
||||
|
||||
@Override
|
||||
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
|
||||
throws ServletException, IOException {
|
||||
doPost(req, resp);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doPost(HttpServletRequest req, HttpServletResponse resp)
|
||||
throws ServletException, IOException {
|
||||
RDFService rdfService = RDFServiceUtils.getRDFServiceFactory(
|
||||
getServletContext()).getRDFService();
|
||||
|
||||
String acceptHeader = req.getHeader("Accept");
|
||||
String queryString = req.getParameter("query");
|
||||
try {
|
||||
confirmAuthorization(req, REQUIRED_ACTIONS);
|
||||
confirmQueryIsPresent(queryString);
|
||||
|
||||
SparqlQueryApiExecutor core = SparqlQueryApiExecutor.instance(
|
||||
rdfService, queryString, acceptHeader);
|
||||
resp.setContentType(core.getMediaType());
|
||||
core.executeAndFormat(resp.getOutputStream());
|
||||
} catch (AuthException e) {
|
||||
sendShortResponse(SC_FORBIDDEN, e.getMessage(), resp);
|
||||
} catch (BadParameterException e) {
|
||||
sendShortResponse(SC_BAD_REQUEST, e.getMessage(), resp);
|
||||
} catch (InvalidQueryTypeException e) {
|
||||
sendShortResponse(SC_BAD_REQUEST,
|
||||
"Query type is not SELECT, ASK, CONSTRUCT, "
|
||||
+ "or DESCRIBE: '" + queryString + "'", resp);
|
||||
} catch (QueryParseException e) {
|
||||
sendShortResponse(SC_BAD_REQUEST, "Failed to parse query: '"
|
||||
+ queryString + "'", e, resp);
|
||||
} catch (NotAcceptableException | AcceptHeaderParsingException e) {
|
||||
sendShortResponse(SC_NOT_ACCEPTABLE,
|
||||
"The accept header does not include any "
|
||||
+ "available content type.", e, resp);
|
||||
} catch (RDFServiceException e) {
|
||||
sendShortResponse(SC_INTERNAL_SERVER_ERROR,
|
||||
"Problem executing the query.", e, resp);
|
||||
} catch (Exception e) {
|
||||
sendShortResponse(SC_INTERNAL_SERVER_ERROR, "Unrecognized error.",
|
||||
e, resp);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private void confirmQueryIsPresent(String queryString)
|
||||
throws BadParameterException {
|
||||
if (queryString == null) {
|
||||
throw new BadParameterException("Query string was not supplied.");
|
||||
}
|
||||
if (queryString.trim().isEmpty()) {
|
||||
throw new BadParameterException("Query string is empty.");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -8,11 +8,9 @@ import static javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
|
|||
import static javax.servlet.http.HttpServletResponse.SC_OK;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.PrintWriter;
|
||||
|
||||
import javax.servlet.ServletContext;
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServlet;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
|
@ -28,24 +26,15 @@ import com.hp.hpl.jena.update.UpdateFactory;
|
|||
import com.hp.hpl.jena.update.UpdateRequest;
|
||||
|
||||
import edu.cornell.mannlib.vitro.webapp.auth.permissions.SimplePermission;
|
||||
import edu.cornell.mannlib.vitro.webapp.auth.policy.PolicyHelper;
|
||||
import edu.cornell.mannlib.vitro.webapp.auth.requestedAction.Actions;
|
||||
import edu.cornell.mannlib.vitro.webapp.beans.UserAccount;
|
||||
import edu.cornell.mannlib.vitro.webapp.controller.VitroRequest;
|
||||
import edu.cornell.mannlib.vitro.webapp.controller.authenticate.Authenticator;
|
||||
import edu.cornell.mannlib.vitro.webapp.dao.jena.RDFServiceDataset;
|
||||
import edu.cornell.mannlib.vitro.webapp.search.indexing.IndexBuilder;
|
||||
|
||||
/**
|
||||
* This extends HttpServlet instead of VitroHttpServlet because we want to have
|
||||
* full control over the response:
|
||||
* <ul>
|
||||
* <li>No redirecting to the login page if not authorized</li>
|
||||
* <li>No redirecting to the home page on insufficient authorization</li>
|
||||
* <li>No support for GET or HEAD requests, only POST.</li>
|
||||
* </ul>
|
||||
* Process SPARQL Updates, as an API.
|
||||
*
|
||||
* So these responses will be produced:
|
||||
* Supports only POST requests, not GET or HEAD. May produce these responses:
|
||||
*
|
||||
* <pre>
|
||||
* 200 Success
|
||||
|
@ -57,7 +46,7 @@ import edu.cornell.mannlib.vitro.webapp.search.indexing.IndexBuilder;
|
|||
* 500 Unknown error
|
||||
* </pre>
|
||||
*/
|
||||
public class SparqlUpdateApiController extends HttpServlet {
|
||||
public class SparqlUpdateApiController extends VitroApiServlet {
|
||||
private static final Log log = LogFactory
|
||||
.getLog(SparqlUpdateApiController.class);
|
||||
|
||||
|
@ -67,7 +56,7 @@ public class SparqlUpdateApiController extends HttpServlet {
|
|||
protected void doPost(HttpServletRequest req, HttpServletResponse resp)
|
||||
throws ServletException, IOException {
|
||||
try {
|
||||
checkAuthorization(req);
|
||||
confirmAuthorization(req, REQUIRED_ACTIONS);
|
||||
UpdateRequest parsed = parseUpdateString(req);
|
||||
executeUpdate(req, parsed);
|
||||
do200response(resp);
|
||||
|
@ -80,27 +69,6 @@ public class SparqlUpdateApiController extends HttpServlet {
|
|||
}
|
||||
}
|
||||
|
||||
private void checkAuthorization(HttpServletRequest req)
|
||||
throws AuthException {
|
||||
String email = req.getParameter("email");
|
||||
String password = req.getParameter("password");
|
||||
|
||||
Authenticator auth = Authenticator.getInstance(req);
|
||||
UserAccount account = auth.getAccountForInternalAuth(email);
|
||||
if (!auth.isCurrentPassword(account, password)) {
|
||||
log.debug("Invalid: '" + email + "'/'" + password + "'");
|
||||
throw new AuthException("email/password combination is not valid");
|
||||
}
|
||||
|
||||
if (!PolicyHelper.isAuthorizedForActions(req, email, password,
|
||||
REQUIRED_ACTIONS)) {
|
||||
log.debug("Not authorized: '" + email + "'");
|
||||
throw new AuthException("Account is not authorized");
|
||||
}
|
||||
|
||||
log.debug("Authorized for '" + email + "'");
|
||||
}
|
||||
|
||||
private UpdateRequest parseUpdateString(HttpServletRequest req)
|
||||
throws ParseException {
|
||||
String update = req.getParameter("update");
|
||||
|
@ -139,55 +107,32 @@ public class SparqlUpdateApiController extends HttpServlet {
|
|||
}
|
||||
|
||||
private void do200response(HttpServletResponse resp) throws IOException {
|
||||
doResponse(resp, SC_OK, "SPARQL update accepted.");
|
||||
sendShortResponse(SC_OK, "SPARQL update accepted.", resp);
|
||||
}
|
||||
|
||||
private void do403response(HttpServletResponse resp, AuthException e)
|
||||
throws IOException {
|
||||
doResponse(resp, SC_FORBIDDEN, e.getMessage());
|
||||
sendShortResponse( SC_FORBIDDEN, e.getMessage(), resp);
|
||||
}
|
||||
|
||||
private void do400response(HttpServletResponse resp, ParseException e)
|
||||
throws IOException {
|
||||
if (e.getCause() == null) {
|
||||
doResponse(resp, SC_BAD_REQUEST, e.getMessage());
|
||||
sendShortResponse( SC_BAD_REQUEST, e.getMessage(), resp);
|
||||
} else {
|
||||
doResponse(resp, SC_BAD_REQUEST, e.getMessage(), e.getCause());
|
||||
sendShortResponse( SC_BAD_REQUEST, e.getMessage(), e.getCause(), resp);
|
||||
}
|
||||
}
|
||||
|
||||
private void do500response(HttpServletResponse resp, Exception e)
|
||||
throws IOException {
|
||||
doResponse(resp, SC_INTERNAL_SERVER_ERROR, "Unknown error", e);
|
||||
}
|
||||
|
||||
private void doResponse(HttpServletResponse resp, int statusCode,
|
||||
String message) throws IOException {
|
||||
resp.setStatus(statusCode);
|
||||
PrintWriter writer = resp.getWriter();
|
||||
writer.println("<H1>" + statusCode + " " + message + "</H1>");
|
||||
}
|
||||
|
||||
private void doResponse(HttpServletResponse resp, int statusCode,
|
||||
String message, Throwable e) throws IOException {
|
||||
resp.setStatus(statusCode);
|
||||
PrintWriter writer = resp.getWriter();
|
||||
writer.println("<H1>" + statusCode + " " + message + "</H1>");
|
||||
writer.println("<pre>");
|
||||
e.printStackTrace(writer);
|
||||
writer.println("</pre>");
|
||||
sendShortResponse(SC_INTERNAL_SERVER_ERROR, "Unknown error", e, resp);
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
// Helper classes
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
private static class AuthException extends Exception {
|
||||
private AuthException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
|
||||
private static class ParseException extends Exception {
|
||||
private ParseException(String message) {
|
||||
super(message);
|
||||
|
|
|
@ -0,0 +1,91 @@
|
|||
/* $This file is distributed under the terms of the license in /doc/license.txt$ */
|
||||
|
||||
package edu.cornell.mannlib.vitro.webapp.controller.api;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.PrintWriter;
|
||||
|
||||
import javax.servlet.http.HttpServlet;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import edu.cornell.mannlib.vitro.webapp.auth.policy.PolicyHelper;
|
||||
import edu.cornell.mannlib.vitro.webapp.auth.requestedAction.Actions;
|
||||
import edu.cornell.mannlib.vitro.webapp.beans.UserAccount;
|
||||
import edu.cornell.mannlib.vitro.webapp.controller.authenticate.Authenticator;
|
||||
|
||||
/**
|
||||
* The base class for Vitro servlets that implement the API.
|
||||
*
|
||||
* We don't want the API servlets to extend VitroHttpServlet, because we want
|
||||
* the following behavior:
|
||||
* <ul>
|
||||
* <li>No redirecting to the login page if not authorized</li>
|
||||
* <li>No redirecting to the home page on insufficient authorization</li>
|
||||
* <li>GET and POST requests are not necessarily equivalent.</li>
|
||||
* </ul>
|
||||
*/
|
||||
public class VitroApiServlet extends HttpServlet {
|
||||
private static final Log log = LogFactory.getLog(VitroApiServlet.class);
|
||||
|
||||
/**
|
||||
* If they have not provided an email/password combo that will authorize
|
||||
* them for this action, throw an AuthException.
|
||||
*/
|
||||
protected void confirmAuthorization(HttpServletRequest req,
|
||||
Actions requiredActions) throws AuthException {
|
||||
String email = req.getParameter("email");
|
||||
String password = req.getParameter("password");
|
||||
|
||||
Authenticator auth = Authenticator.getInstance(req);
|
||||
UserAccount account = auth.getAccountForInternalAuth(email);
|
||||
if (!auth.isCurrentPassword(account, password)) {
|
||||
log.debug("Invalid: '" + email + "'/'" + password + "'");
|
||||
throw new AuthException("email/password combination is not valid");
|
||||
}
|
||||
|
||||
if (!PolicyHelper.isAuthorizedForActions(req, email, password,
|
||||
requiredActions)) {
|
||||
log.debug("Not authorized: '" + email + "'");
|
||||
throw new AuthException("Account is not authorized");
|
||||
}
|
||||
|
||||
log.debug("Authorized for '" + email + "'");
|
||||
}
|
||||
|
||||
protected void sendShortResponse(int statusCode, String message,
|
||||
HttpServletResponse resp) throws IOException {
|
||||
resp.setStatus(statusCode);
|
||||
PrintWriter writer = resp.getWriter();
|
||||
writer.println("<H1>" + statusCode + " " + message + "</H1>");
|
||||
}
|
||||
|
||||
protected void sendShortResponse(int statusCode, String message, Throwable e,
|
||||
HttpServletResponse resp) throws IOException {
|
||||
sendShortResponse(statusCode, message, resp);
|
||||
PrintWriter writer = resp.getWriter();
|
||||
writer.println("<pre>");
|
||||
e.printStackTrace(writer);
|
||||
writer.println("</pre>");
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
// Helper classes
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
protected static class AuthException extends Exception {
|
||||
protected AuthException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
|
||||
protected static class BadParameterException extends Exception {
|
||||
protected BadParameterException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -1034,6 +1034,16 @@
|
|||
<url-pattern>/admin/sparqlquery</url-pattern>
|
||||
</servlet-mapping>
|
||||
|
||||
<servlet>
|
||||
<servlet-name>SparqlQueryApi</servlet-name>
|
||||
<servlet-class>edu.cornell.mannlib.vitro.webapp.controller.api.SparqlQueryApiController</servlet-class>
|
||||
</servlet>
|
||||
|
||||
<servlet-mapping>
|
||||
<servlet-name>SparqlQueryApi</servlet-name>
|
||||
<url-pattern>/api/sparqlQuery</url-pattern>
|
||||
</servlet-mapping>
|
||||
|
||||
<servlet>
|
||||
<servlet-name>SparqlUpdateApi</servlet-name>
|
||||
<servlet-class>edu.cornell.mannlib.vitro.webapp.controller.api.SparqlUpdateApiController</servlet-class>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue