VIVO-731 Implement the SPARQL query API.

Create a base class for API controller servlets.
This commit is contained in:
Jim Blake 2014-04-15 13:36:47 -04:00
parent de32d53791
commit 8892da74b0
5 changed files with 217 additions and 65 deletions

View file

@ -59,7 +59,7 @@ public class SimplePermission extends Permission {
public static final SimplePermission REFRESH_VISUALIZATION_CACHE = new SimplePermission( public static final SimplePermission REFRESH_VISUALIZATION_CACHE = new SimplePermission(
NAMESPACE + "RefreshVisualizationCache"); NAMESPACE + "RefreshVisualizationCache");
public static final SimplePermission SEE_CONFIGURATION = new SimplePermission( public static final SimplePermission SEE_CONFIGURATION = new SimplePermission(
NAMESPACE + "SeeConfiguration"); NAMESPACE + "SeeConfiguration");
public static final SimplePermission SEE_INDVIDUAL_EDITING_PANEL = new SimplePermission( public static final SimplePermission SEE_INDVIDUAL_EDITING_PANEL = new SimplePermission(
NAMESPACE + "SeeIndividualEditingPanel"); NAMESPACE + "SeeIndividualEditingPanel");
public static final SimplePermission SEE_REVISION_INFO = new SimplePermission( public static final SimplePermission SEE_REVISION_INFO = new SimplePermission(
@ -76,6 +76,8 @@ public class SimplePermission extends Permission {
NAMESPACE + "UseIndividualControlPanel"); NAMESPACE + "UseIndividualControlPanel");
public static final SimplePermission USE_SPARQL_QUERY_PAGE = new SimplePermission( public static final SimplePermission USE_SPARQL_QUERY_PAGE = new SimplePermission(
NAMESPACE + "UseSparqlQueryPage"); NAMESPACE + "UseSparqlQueryPage");
public static final SimplePermission USE_SPARQL_QUERY_API = new SimplePermission(
NAMESPACE + "UseSparqlQueryApi");
public static final SimplePermission USE_SPARQL_UPDATE_API = new SimplePermission( public static final SimplePermission USE_SPARQL_UPDATE_API = new SimplePermission(
NAMESPACE + "UseSparqlUpdateApi"); NAMESPACE + "UseSparqlUpdateApi");

View file

@ -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.");
}
}
}

View file

@ -8,11 +8,9 @@ import static javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
import static javax.servlet.http.HttpServletResponse.SC_OK; import static javax.servlet.http.HttpServletResponse.SC_OK;
import java.io.IOException; import java.io.IOException;
import java.io.PrintWriter;
import javax.servlet.ServletContext; import javax.servlet.ServletContext;
import javax.servlet.ServletException; import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
@ -28,24 +26,15 @@ import com.hp.hpl.jena.update.UpdateFactory;
import com.hp.hpl.jena.update.UpdateRequest; import com.hp.hpl.jena.update.UpdateRequest;
import edu.cornell.mannlib.vitro.webapp.auth.permissions.SimplePermission; 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.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.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.dao.jena.RDFServiceDataset;
import edu.cornell.mannlib.vitro.webapp.search.indexing.IndexBuilder; import edu.cornell.mannlib.vitro.webapp.search.indexing.IndexBuilder;
/** /**
* This extends HttpServlet instead of VitroHttpServlet because we want to have * Process SPARQL Updates, as an API.
* 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>
* *
* So these responses will be produced: * Supports only POST requests, not GET or HEAD. May produce these responses:
* *
* <pre> * <pre>
* 200 Success * 200 Success
@ -57,7 +46,7 @@ import edu.cornell.mannlib.vitro.webapp.search.indexing.IndexBuilder;
* 500 Unknown error * 500 Unknown error
* </pre> * </pre>
*/ */
public class SparqlUpdateApiController extends HttpServlet { public class SparqlUpdateApiController extends VitroApiServlet {
private static final Log log = LogFactory private static final Log log = LogFactory
.getLog(SparqlUpdateApiController.class); .getLog(SparqlUpdateApiController.class);
@ -67,7 +56,7 @@ public class SparqlUpdateApiController extends HttpServlet {
protected void doPost(HttpServletRequest req, HttpServletResponse resp) protected void doPost(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException { throws ServletException, IOException {
try { try {
checkAuthorization(req); confirmAuthorization(req, REQUIRED_ACTIONS);
UpdateRequest parsed = parseUpdateString(req); UpdateRequest parsed = parseUpdateString(req);
executeUpdate(req, parsed); executeUpdate(req, parsed);
do200response(resp); 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) private UpdateRequest parseUpdateString(HttpServletRequest req)
throws ParseException { throws ParseException {
String update = req.getParameter("update"); String update = req.getParameter("update");
@ -139,55 +107,32 @@ public class SparqlUpdateApiController extends HttpServlet {
} }
private void do200response(HttpServletResponse resp) throws IOException { 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) private void do403response(HttpServletResponse resp, AuthException e)
throws IOException { throws IOException {
doResponse(resp, SC_FORBIDDEN, e.getMessage()); sendShortResponse( SC_FORBIDDEN, e.getMessage(), resp);
} }
private void do400response(HttpServletResponse resp, ParseException e) private void do400response(HttpServletResponse resp, ParseException e)
throws IOException { throws IOException {
if (e.getCause() == null) { if (e.getCause() == null) {
doResponse(resp, SC_BAD_REQUEST, e.getMessage()); sendShortResponse( SC_BAD_REQUEST, e.getMessage(), resp);
} else { } 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) private void do500response(HttpServletResponse resp, Exception e)
throws IOException { throws IOException {
doResponse(resp, SC_INTERNAL_SERVER_ERROR, "Unknown error", e); sendShortResponse(SC_INTERNAL_SERVER_ERROR, "Unknown error", e, resp);
}
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>");
} }
// ---------------------------------------------------------------------- // ----------------------------------------------------------------------
// Helper classes // Helper classes
// ---------------------------------------------------------------------- // ----------------------------------------------------------------------
private static class AuthException extends Exception {
private AuthException(String message) {
super(message);
}
}
private static class ParseException extends Exception { private static class ParseException extends Exception {
private ParseException(String message) { private ParseException(String message) {
super(message); super(message);

View file

@ -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);
}
}
}

View file

@ -1034,6 +1034,16 @@
<url-pattern>/admin/sparqlquery</url-pattern> <url-pattern>/admin/sparqlquery</url-pattern>
</servlet-mapping> </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>
<servlet-name>SparqlUpdateApi</servlet-name> <servlet-name>SparqlUpdateApi</servlet-name>
<servlet-class>edu.cornell.mannlib.vitro.webapp.controller.api.SparqlUpdateApiController</servlet-class> <servlet-class>edu.cornell.mannlib.vitro.webapp.controller.api.SparqlUpdateApiController</servlet-class>