package edu.kit.aifb.cumulus.webapp; import static edu.kit.aifb.cumulus.framework.util.Strings.isNotNullOrEmptyString; import static edu.kit.aifb.cumulus.framework.util.Strings.isNullOrEmptyString; import static edu.kit.aifb.cumulus.util.Util.ALL_CONSTANTS; import static edu.kit.aifb.cumulus.util.Util.ALL_VARS; import static edu.kit.aifb.cumulus.util.Util.CONTAINS_VAR; import static edu.kit.aifb.cumulus.util.Util.singletonIterator; import static edu.kit.aifb.cumulus.util.Util.toResultIterator; import static edu.kit.aifb.cumulus.webapp.HttpProtocol.*; import static edu.kit.aifb.cumulus.webapp.writer.HTMLWriter.HTML_FORMAT; import java.io.IOException; import java.util.Iterator; import javax.servlet.ServletContext; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.openrdf.model.Resource; import org.openrdf.model.Statement; import org.openrdf.model.URI; import org.openrdf.model.Value; import org.openrdf.model.ValueFactory; import org.openrdf.model.impl.ValueFactoryImpl; import org.openrdf.repository.RepositoryResult; import org.openrdf.repository.sail.SailRepository; import org.openrdf.repository.sail.SailRepositoryConnection; import org.openrdf.rio.RDFFormat; import org.openrdf.rio.RDFHandler; import org.openrdf.rio.RDFHandlerException; import org.openrdf.rio.RDFParseException; import org.openrdf.rio.RDFParser; import org.openrdf.rio.RDFWriter; import org.openrdf.rio.Rio; import org.openrdf.rio.ntriples.NTriplesUtil; import org.slf4j.LoggerFactory; import com.google.common.collect.Iterators; import edu.kit.aifb.cumulus.framework.Environment; import edu.kit.aifb.cumulus.framework.Environment.ConfigParams; import edu.kit.aifb.cumulus.framework.Environment.ConfigValues; import edu.kit.aifb.cumulus.log.Log; import edu.kit.aifb.cumulus.log.MessageCatalog; import edu.kit.aifb.cumulus.store.CumulusStoreException; import edu.kit.aifb.cumulus.store.Store; /** * <p> * * Implements the <a href="http://en.wikipedia.org/wiki/Create,_read,_update_and_delete">CRUD</a> operations using HTTP as operation selector. Parameters * must be encoded in N-Triples syntax. * * <ul> * <li><b>DELETE</b>: delete a triple or pattern. Parameters: s,p,o (c) (if omitted a * parameter will be treated as variable).</li> * <li><b>GET</b>: Describes a requested URI. Parameters: uri.</li> * <li><b>POST</b>: Insert data into the store. All common RDF serializations are supported.</li> * <li><b>PUT</b>: Updates a triple. Parameters for the "original" triple: s,p,o (c). * Parameters for the "new" triple: and s2,p2,o2 (c2).</li> * </ul> * </p> * * <p> * Note, if parameter "c" (i.e., context) is specified, the storage layout must * be a "quad". * </p> * * @see <a href="http://code.google.com/p/cumulusrdf/wiki/Webapps">http://code.google.com/p/cumulusrdf/wiki/Webapps</a> * @see <a href="http://www.w3.org/TR/n-triples/">http://www.w3.org/TR/n-triples/</a> * @see {@link edu.kit.aifb.cumulus.webapp.HttpProtocol.Parameters} * * @author Andreas Harth * @author Andreas Wagner * @author Andrea Gazzarini * * @since 1.0 */ public class CRUDServlet extends AbstractCumulusServlet { private static final long serialVersionUID = -2672280063418774760L; private Log _log = new Log(LoggerFactory.getLogger(CRUDServlet.class)); private ValueFactory _valueFactory; /** * <p>To delete a triple send a HTTP DELETE request. * All parameters must be encoded in <a href="http://www.w3.org/TR/n-triples/">N-Triples</a> syntax.</p> * * <p>The request takes the following parameters:</p> * * <ul> * <li> * <b>s, p, o, c</b>: for the subject, predicate and object of the triple to delete. * One or two of the parameters can be left out, which deletes all triples with the given parameters. * For example, if no subject is given, all triples with the given predicate and object are deleted. * </li> * <li> * <b>uri</b>: a specific resource. If this is given, all triples having this resource as subject or object are deleted. * </li> * </ul> * * @param req the HTTP request. * @param resp the HTTP response. * * @throws IOException in case of I/O failure. * @throws ServletException in case of application failure. * * @see <a href="http://www.w3.org/TR/n-triples/">http://www.w3.org/TR/n-triples/</a> * @see {@link edu.kit.aifb.cumulus.webapp.HttpProtocol.Parameters} */ @Override public void doDelete(final HttpServletRequest req, final HttpServletResponse resp) throws IOException, ServletException { final ServletContext ctx = getServletContext(); final String uri = getParameterValue(req, Parameters.URI); final String s = getParameterValue(req, Parameters.S), p = getParameterValue(req, Parameters.P), o = getParameterValue(req, Parameters.O), c = getParameterValue(req, Parameters.C); // Sanity check: at least one parameter must be valid. if (isNullOrEmptyString(uri) && isNullOrEmptyString(s) && isNullOrEmptyString(p) && isNullOrEmptyString(o) && isNullOrEmptyString(c)) { _log.debug(MessageCatalog._00024_MISSING_REQUIRED_PARAM, req.getQueryString()); sendError(req, resp, HttpServletResponse.SC_BAD_REQUEST, MessageCatalog._00035_MISSING_URI_OR_PATTERN); return; } final Store crdf = (Store) ctx.getAttribute(ConfigParams.STORE); if ((crdf == null) || !crdf.isOpen()) { _log.error(MessageCatalog._00025_CUMULUS_SYSTEM_INTERNAL_FAILURE_MSG + " Store was null or not initialized."); sendError( req, resp, HttpServletResponse.SC_INTERNAL_SERVER_ERROR, MessageCatalog._00025_CUMULUS_SYSTEM_INTERNAL_FAILURE_MSG); return; } try { Iterator<Statement> result = null; if (isNotNullOrEmptyString(uri)) { result = crdf.describe(NTriplesUtil.parseResource(uri, _valueFactory), false); } else { final String layout = (String) ctx.getAttribute(ConfigParams.LAYOUT); final Value node_s = (s != null ? NTriplesUtil.parseResource(s, _valueFactory) : null); final Value node_p = (p != null ? NTriplesUtil.parseURI(p, _valueFactory) : null); final Value node_o = (o != null ? NTriplesUtil.parseValue(o, _valueFactory) : null); if (ConfigValues.STORE_LAYOUT_TRIPLE.equals(layout)) { final Value[] pattern = new Value[] { node_s, node_p, node_o }; result = ALL_CONSTANTS.apply(pattern) ? singletonIterator(_valueFactory.createStatement((Resource) node_s, (URI) node_p, node_o)) : crdf.query(pattern); } else { final Value node_c = (c != null ? _valueFactory.createURI(c) : null); if (node_c == null) { final Value[] pattern = new Value[] { node_s, node_p, node_o }; if (ALL_CONSTANTS.apply(pattern)) { result = singletonIterator(_valueFactory.createStatement((Resource) node_s, (URI) node_p, node_o)); } else { result = crdf.query(pattern); } } else { result = crdf.query(new Value[] { node_s, node_p, node_o, node_c }); } } } if ((result != null) && result.hasNext()) { crdf.removeData(result); } else { sendError(req, resp, HttpServletResponse.SC_NOT_FOUND, MessageCatalog._00033_RESOURCE_NOT_FOUND_MSG); return; } } catch (final CumulusStoreException exception) { _log.error(MessageCatalog._00025_CUMULUS_SYSTEM_INTERNAL_FAILURE, exception); sendError( req, resp, HttpServletResponse.SC_INTERNAL_SERVER_ERROR, MessageCatalog._00025_CUMULUS_SYSTEM_INTERNAL_FAILURE_MSG, exception); } catch (final Exception exception) { _log.error(MessageCatalog._00026_NWS_SYSTEM_INTERNAL_FAILURE, exception); sendError( req, resp, HttpServletResponse.SC_INTERNAL_SERVER_ERROR, MessageCatalog._00025_CUMULUS_SYSTEM_INTERNAL_FAILURE_MSG, exception); } } /** * <p>To request triples, send a HTTP GET request. This method returns triples that have the given URL as subject or object. * The service accepts:</p> * * <ul> * <li> * an <b>uri</b> parameter (must be encoded in N-Triples syntax) for the URL that should be subject or object of the returned triples. * </li> * <li> * an <b>accept</b> header for specifying a given output format (default: RDF/XML). * </li> * </ul> * * @param req the HTTP request. * @param resp the HTTP response. * * @see {@link edu.kit.aifb.cumulus.webapp.HttpProtocol.Parameters} * * @throws IOException in case of I/O failure. * @throws ServletException in case of application failure. */ @Override public void doGet(final HttpServletRequest req, final HttpServletResponse resp) throws IOException, ServletException { final String uri = getParameterValue(req, Parameters.URI); if (isNullOrEmptyString(uri)) { _log.debug(MessageCatalog._00023_MISSING_URI); sendError(req, resp, HttpServletResponse.SC_BAD_REQUEST, MessageCatalog._00024_MISSING_REQUIRED_PARAM + " Missing URI parameter."); return; } final RDFFormat format = Rio.getWriterFormatForMIMEType(parseAcceptHeader(req)); if (format == null) { _log.debug(MessageCatalog._00115_WEB_MODULE_REQUEST_NOT_VALID + " Mime-type '" + parseAcceptHeader(req) + "' not supported."); sendError(req, resp, HttpServletResponse.SC_BAD_REQUEST, MessageCatalog._00115_WEB_MODULE_REQUEST_NOT_VALID + " Mime-type '" + parseAcceptHeader(req) + "' not supported."); return; } SailRepositoryConnection connection = null; RepositoryResult<Statement> repo_res1 = null, repo_res2 = null; try { SailRepository repository = (SailRepository) getServletContext().getAttribute(ConfigParams.SESAME_REPO); if ((repository == null) || !repository.isInitialized()) { _log.error(MessageCatalog._00025_CUMULUS_SYSTEM_INTERNAL_FAILURE_MSG + " Repository was null or not initialized."); sendError( req, resp, HttpServletResponse.SC_INTERNAL_SERVER_ERROR, MessageCatalog._00025_CUMULUS_SYSTEM_INTERNAL_FAILURE_MSG); return; } connection = repository.getConnection(); Resource entity = NTriplesUtil.parseResource(uri, _valueFactory); final Iterator<Statement> res1 = toResultIterator(repo_res1 = connection.getStatements(entity, null, null, false)); final Iterator<Statement> res2 = toResultIterator(repo_res2 = connection.getStatements(null, null, entity, false)); if (!res1.hasNext() && !res2.hasNext()) { sendError(req, resp, HttpServletResponse.SC_NOT_FOUND, MessageCatalog._00033_RESOURCE_NOT_FOUND_MSG); } else { if (format.equals(HTML_FORMAT)) { req.setAttribute("uri", entity.stringValue()); req.setAttribute("result", Iterators.concat(res1, res2)); forwardTo(req, resp, "pattern-query-result.vm"); } else { resp.setCharacterEncoding(Environment.CHARSET_UTF8.name()); resp.setContentType(format.getDefaultMIMEType()); resp.setStatus(HttpServletResponse.SC_OK); RDFWriter writer = Rio.createWriter(format, resp.getOutputStream()); Rio.write(new Iterable<Statement>() { @Override public Iterator<Statement> iterator() { return Iterators.concat(res1, res2); } }, writer); } } } catch (Exception exception) { _log.error(MessageCatalog._00025_CUMULUS_SYSTEM_INTERNAL_FAILURE, exception); sendError(req, resp, HttpServletResponse.SC_INTERNAL_SERVER_ERROR, MessageCatalog._00025_CUMULUS_SYSTEM_INTERNAL_FAILURE_MSG, exception); } finally { try { if (repo_res1 != null) { repo_res1.close(); } if (repo_res2 != null) { repo_res2.close(); } if (connection != null) { connection.close(); } } catch (Exception exception) { _log.error(MessageCatalog._00025_CUMULUS_SYSTEM_INTERNAL_FAILURE, exception); sendError(req, resp, HttpServletResponse.SC_INTERNAL_SERVER_ERROR, MessageCatalog._00025_CUMULUS_SYSTEM_INTERNAL_FAILURE_MSG, exception); } } } /** * To insert triples send a HTTP POST request. * The content-type header must match the RDF serialization used. All major RDF serializations are supported. * A base URI can be specified in the HTTP header field 'base-uri'. * * @param req the HTTP request. * @param resp the HTTP response. * @see {@link edu.kit.aifb.cumulus.webapp.HttpProtocol.Parameters} * @throws IOException in case of I/O failure. * @throws ServletException in case of application failure. */ @Override public void doPost(final HttpServletRequest req, final HttpServletResponse resp) throws IOException, ServletException { final ServletContext ctx = getServletContext(); final String contentType = parseContentTypeHeader(req), baseURI = parseBaseURI(req); RDFFormat format = Rio.getParserFormatForMIMEType(contentType); if (format == null) { _log.debug(MessageCatalog._00115_WEB_MODULE_REQUEST_NOT_VALID + " Content-type '" + contentType + "' not supported."); sendError(req, resp, HttpServletResponse.SC_BAD_REQUEST, MessageCatalog._00115_WEB_MODULE_REQUEST_NOT_VALID + " Content-type '" + contentType + "' not supported."); return; } final Store crdf = (Store) ctx.getAttribute(ConfigParams.STORE); if ((crdf == null) || !crdf.isOpen()) { _log.error(MessageCatalog._00025_CUMULUS_SYSTEM_INTERNAL_FAILURE_MSG + " Store was null or not initialized."); sendError( req, resp, HttpServletResponse.SC_INTERNAL_SERVER_ERROR, MessageCatalog._00025_CUMULUS_SYSTEM_INTERNAL_FAILURE_MSG); return; } try { RDFParser parser = Rio.createParser(format); parser.setRDFHandler(new RDFHandler() { @Override public void endRDF() throws RDFHandlerException { } @Override public void handleComment(String comment) throws RDFHandlerException { } @Override public void handleNamespace(String prefix, String uri) throws RDFHandlerException { } @Override public void handleStatement(Statement st) throws RDFHandlerException { try { crdf.addData(st); } catch (CumulusStoreException e) { _log.error(MessageCatalog._00025_CUMULUS_SYSTEM_INTERNAL_FAILURE, e); } } @Override public void startRDF() throws RDFHandlerException { } }); /* * this allows proper handling of RuntimeExceptions ... */ try { parser.parse(req.getInputStream(), baseURI); } catch (RuntimeException exception) { throw new RDFParseException(exception); } resp.setStatus(HttpServletResponse.SC_CREATED); } catch (RDFParseException exception) { _log.debug(MessageCatalog._00029_RDF_PARSE_FAILURE, exception); sendError(req, resp, HttpServletResponse.SC_BAD_REQUEST, MessageCatalog._00029_RDF_PARSE_FAILURE, exception); } catch (RDFHandlerException exception) { _log.debug(MessageCatalog._00029_RDF_PARSE_FAILURE, exception); sendError(req, resp, HttpServletResponse.SC_BAD_REQUEST, MessageCatalog._00029_RDF_PARSE_FAILURE, exception); } catch (IOException exception) { _log.debug(MessageCatalog._00029_RDF_PARSE_FAILURE, exception); sendError(req, resp, HttpServletResponse.SC_BAD_REQUEST, MessageCatalog._00029_RDF_PARSE_FAILURE, exception); } catch (Exception exception) { _log.error(MessageCatalog._00025_CUMULUS_SYSTEM_INTERNAL_FAILURE, exception); sendError(req, resp, HttpServletResponse.SC_INTERNAL_SERVER_ERROR, MessageCatalog._00025_CUMULUS_SYSTEM_INTERNAL_FAILURE_MSG, exception); } } /** * <p>To update a triple send a HTTP PUT request. * The parameters can be specified as URL parameters or in the request body using N-Triples syntax. For instance: </p> * * <p>s=<http://example.org/id/s1></p> * <p>p=<http://example.org/id/p1></p> * *<p> The parameters are: </p> * * <ul> * <li>s, p, o, c: for the subject, predicate and object of the old triple. * If not all values are given, that parameter is treated as variable (and the first triple that is bound is updated). * </li> * <li> * s2, p2, o2, c2: for the subject, predicate and object that should act as replacement for the old triple. * If some of this parameters is not given, the respective part of the triple remains unchanged. * For example, if no subject is given, only the predicate and object are updated. * </li> * </ul> * * @param req the HTTP request. * @param resp the HTTP response. * @throws IOException in case of I/O failure. * @throws ServletException in case of application failure. * @see {@link http://www.w3.org/TR/n-triples/} * @see {@link edu.kit.aifb.cumulus.webapp.HttpProtocol.Parameters} * */ @Override public void doPut(final HttpServletRequest req, final HttpServletResponse resp) throws IOException, ServletException { final ServletContext ctx = getServletContext(); final Store crdf = (Store) ctx.getAttribute(ConfigParams.STORE); if ((crdf == null) || !crdf.isOpen()) { _log.error(MessageCatalog._00025_CUMULUS_SYSTEM_INTERNAL_FAILURE_MSG + " Store was null or not initialized."); sendError( req, resp, HttpServletResponse.SC_INTERNAL_SERVER_ERROR, MessageCatalog._00025_CUMULUS_SYSTEM_INTERNAL_FAILURE_MSG); return; } // old triple String s = getParameterValue(req, Parameters.S), p = getParameterValue(req, Parameters.P), o = getParameterValue(req, Parameters.O), c = getParameterValue(req, Parameters.C); // new triple String s2 = getParameterValue(req, Parameters.S2), p2 = getParameterValue(req, Parameters.P2), o2 = getParameterValue(req, Parameters.O2), c2 = getParameterValue(req, Parameters.C2); try { boolean quad = ConfigValues.STORE_LAYOUT_QUAD.equalsIgnoreCase((String) ctx.getAttribute(ConfigParams.LAYOUT)); final Value node_s = (s != null ? NTriplesUtil.parseResource(s, _valueFactory) : null); final Value node_p = (p != null ? NTriplesUtil.parseURI(p, _valueFactory) : null); final Value node_o = (o != null ? NTriplesUtil.parseValue(o, _valueFactory) : null); final Value node_c = (c != null ? NTriplesUtil.parseResource(c, _valueFactory) : null); Value[] old_triple = quad ? new Value[] { node_s, node_p, node_o, node_c } : new Value[] { node_s, node_p, node_o }; Statement firstMatchingTriple = null; if (ALL_VARS.apply(old_triple)) { sendError(req, resp, HttpServletResponse.SC_BAD_REQUEST, MessageCatalog._00115_WEB_MODULE_REQUEST_NOT_VALID + " There must be at least one constant."); return; } else if (CONTAINS_VAR.apply(old_triple)) { final Iterator<Statement> iterator = crdf.query(old_triple); if (!iterator.hasNext()) { sendError(req, resp, HttpServletResponse.SC_NOT_FOUND, MessageCatalog._00033_RESOURCE_NOT_FOUND_MSG); return; } else { firstMatchingTriple = iterator.next(); } } else { firstMatchingTriple = quad ? _valueFactory.createStatement((Resource) node_s, (URI) node_p, node_o, (Resource) node_c) : _valueFactory.createStatement((Resource) node_s, (URI) node_p, node_o); } final Value node_s2 = (s2 != null ? NTriplesUtil.parseResource(s2, _valueFactory) : firstMatchingTriple.getSubject()); final Value node_p2 = (p2 != null ? NTriplesUtil.parseURI(p2, _valueFactory) : firstMatchingTriple.getPredicate()); final Value node_o2 = (o2 != null ? NTriplesUtil.parseValue(o2, _valueFactory) : firstMatchingTriple.getObject()); final Value node_c2 = (c2 != null ? NTriplesUtil.parseResource(c2, _valueFactory) : (quad ? firstMatchingTriple.getContext() : null)); crdf.removeData(quad ? new Value[] { firstMatchingTriple.getSubject(), firstMatchingTriple.getPredicate(), firstMatchingTriple.getObject(), firstMatchingTriple.getContext() } : new Value[] { firstMatchingTriple.getSubject(), firstMatchingTriple.getPredicate(), firstMatchingTriple.getObject() }); crdf.addData(quad ? _valueFactory.createStatement((Resource) node_s2, (URI) node_p2, node_o2, (Resource) node_c2) : _valueFactory.createStatement((Resource) node_s2, (URI) node_p2, node_o2)); } catch (final CumulusStoreException exception) { _log.error(MessageCatalog._00026_NWS_SYSTEM_INTERNAL_FAILURE, exception); sendError(req, resp, HttpServletResponse.SC_BAD_REQUEST, MessageCatalog._00115_WEB_MODULE_REQUEST_NOT_VALID, exception); } catch (final Exception exception) { _log.error(MessageCatalog._00026_NWS_SYSTEM_INTERNAL_FAILURE, exception); sendError(req, resp, HttpServletResponse.SC_INTERNAL_SERVER_ERROR, MessageCatalog._00025_CUMULUS_SYSTEM_INTERNAL_FAILURE_MSG, exception); } } @Override public void init() { _valueFactory = ValueFactoryImpl.getInstance(); } }