diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/auth/permissions/SimplePermission.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/auth/permissions/SimplePermission.java index 2e5d40ca1..71e15f481 100644 --- a/webapp/src/edu/cornell/mannlib/vitro/webapp/auth/permissions/SimplePermission.java +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/auth/permissions/SimplePermission.java @@ -59,7 +59,7 @@ public class SimplePermission extends Permission { public static final SimplePermission REFRESH_VISUALIZATION_CACHE = new SimplePermission( NAMESPACE + "RefreshVisualizationCache"); public static final SimplePermission SEE_CONFIGURATION = new SimplePermission( - NAMESPACE + "SeeConfiguration"); + NAMESPACE + "SeeConfiguration"); public static final SimplePermission SEE_INDVIDUAL_EDITING_PANEL = new SimplePermission( NAMESPACE + "SeeIndividualEditingPanel"); public static final SimplePermission SEE_REVISION_INFO = new SimplePermission( @@ -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"); diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/controller/api/SparqlQueryApiController.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/controller/api/SparqlQueryApiController.java new file mode 100644 index 000000000..b241fc7f0 --- /dev/null +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/controller/api/SparqlQueryApiController.java @@ -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: + * + *
+ * 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
+ * 
+ */ +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."); + } + } + +} diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/controller/api/SparqlUpdateApiController.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/controller/api/SparqlUpdateApiController.java index 60a0e6af0..fc5bd6d04 100644 --- a/webapp/src/edu/cornell/mannlib/vitro/webapp/controller/api/SparqlUpdateApiController.java +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/controller/api/SparqlUpdateApiController.java @@ -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: - * + * Process SPARQL Updates, as an API. * - * So these responses will be produced: + * Supports only POST requests, not GET or HEAD. May produce these responses: * *
  * 200 Success
@@ -57,7 +46,7 @@ import edu.cornell.mannlib.vitro.webapp.search.indexing.IndexBuilder;
  * 500 Unknown error
  * 
*/ -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("

" + statusCode + " " + message + "

"); - } - - private void doResponse(HttpServletResponse resp, int statusCode, - String message, Throwable e) throws IOException { - resp.setStatus(statusCode); - PrintWriter writer = resp.getWriter(); - writer.println("

" + statusCode + " " + message + "

"); - writer.println("
");
-		e.printStackTrace(writer);
-		writer.println("
"); + 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); diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/controller/api/VitroApiServlet.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/controller/api/VitroApiServlet.java new file mode 100644 index 000000000..8786b4592 --- /dev/null +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/controller/api/VitroApiServlet.java @@ -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: + * + */ +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("

" + statusCode + " " + message + "

"); + } + + protected void sendShortResponse(int statusCode, String message, Throwable e, + HttpServletResponse resp) throws IOException { + sendShortResponse(statusCode, message, resp); + PrintWriter writer = resp.getWriter(); + writer.println("
");
+		e.printStackTrace(writer);
+		writer.println("
"); + } + + // ---------------------------------------------------------------------- + // 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); + } + } + +} diff --git a/webapp/web/WEB-INF/web.xml b/webapp/web/WEB-INF/web.xml index 8fb110bf1..a1aeaad87 100644 --- a/webapp/web/WEB-INF/web.xml +++ b/webapp/web/WEB-INF/web.xml @@ -1034,6 +1034,16 @@ /admin/sparqlquery + + SparqlQueryApi + edu.cornell.mannlib.vitro.webapp.controller.api.SparqlQueryApiController + + + + SparqlQueryApi + /api/sparqlQuery + + SparqlUpdateApi edu.cornell.mannlib.vitro.webapp.controller.api.SparqlUpdateApiController