diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/rdfservice/ModelChange.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/rdfservice/ModelChange.java index 66f1ddabd..10dd8934a 100644 --- a/webapp/src/edu/cornell/mannlib/vitro/webapp/rdfservice/ModelChange.java +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/rdfservice/ModelChange.java @@ -15,7 +15,7 @@ public interface ModelChange { ADD, REMOVE } - abstract InputStream getSerializedModel(); + public InputStream getSerializedModel(); public void setSerializedModel(InputStream serializedModel); diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/rdfservice/RDFService.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/rdfservice/RDFService.java index ec2cf4b4e..231f26a84 100644 --- a/webapp/src/edu/cornell/mannlib/vitro/webapp/rdfservice/RDFService.java +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/rdfservice/RDFService.java @@ -3,6 +3,7 @@ package edu.cornell.mannlib.vitro.webapp.rdfservice; import java.io.InputStream; +import java.io.OutputStream; import java.util.List; /* @@ -22,13 +23,16 @@ public interface RDFService { /** * Perform a series of additions to and or removals from specified graphs - * in the RDF store. For each change preConditionSparql will be executed - * before the update is made and if it returns a non-empty result, no updates - * will be made. The same preConditionSparql is used for each change/graph pair. + * in the RDF store. preConditionSparql will be executed against the + * union of all the graphs in the knowledge base before any updates are made. + * If the precondition query returns a non-empty result, no updates + * will be made. * - * @param ChangeSet - a set of changes to be performed on the RDF store. + * @param ChangeSet - a set of changes to be performed on the RDF store. + * + * @return boolean - indicates whether the precondition was satisfied */ - public void changeSetUpdate(ChangeSet changeSet) throws RDFServiceException; + public boolean changeSetUpdate(ChangeSet changeSet) throws RDFServiceException; /** * If the given individual already exists in the default graph, throws an @@ -52,16 +56,49 @@ public interface RDFService { public void newIndividual(String individualURI, String individualTypeURI, String graphURI) throws RDFServiceException; /** - * Performs a SPARQL query against the knowledge base. The query may have + * Performs a SPARQL construct query against the knowledge base. The query may have + * an embedded graph identifier. + * + * @param String query - the SPARQL query to be executed against the RDF store + * @param RDFService.ModelSerializationFormat resultFormat - type of serialization for RDF result of the SPARQL query + * @param OutputStream outputStream - the result of the query + * + */ + public InputStream sparqlConstructQuery(String query, RDFService.ModelSerializationFormat resultFormat) throws RDFServiceException; + + /** + * Performs a SPARQL describe query against the knowledge base. The query may have * an embedded graph identifier. * * @param String query - the SPARQL query to be executed against the RDF store - * @param RDFService.SPARQLQueryType queryType - the type of SPARQL query (SELECT, CONSTRUCT, DESCRIBE, ASK) * @param RDFService.ModelSerializationFormat resultFormat - type of serialization for RDF result of the SPARQL query * - * @return InputStream - the result of the SPARQL query + * @return InputStream - the result of the query + * */ - public InputStream sparqlConstructQuery(String query, RDFService.SPARQLQueryType queryType, RDFService.ModelSerializationFormat resultFormat) throws RDFServiceException; + public InputStream sparqlDescribeQuery(String query, RDFService.ModelSerializationFormat resultFormat) throws RDFServiceException; + + /** + * Performs a SPARQL select query against the knowledge base. The query may have + * an embedded graph identifier. + * + * @param String query - the SPARQL query to be executed against the RDF store + * + * @return InputStream - the result of the query + * + */ + public InputStream sparqlSelectQuery(String query) throws RDFServiceException; + + /** + * Performs a SPARQL ASK query against the knowledge base. The query may have + * an embedded graph identifier. + * + * @param String query - the SPARQL query to be executed against the RDF store + * + * @return boolean - the result of the SPARQL query + */ + + public boolean sparqlAskQuery(String query) throws RDFServiceException; /** * Get a list of all the graph URIs in the RDF store. diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/rdfservice/RDFServiceException.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/rdfservice/RDFServiceException.java index fd711717f..cbb38261b 100644 --- a/webapp/src/edu/cornell/mannlib/vitro/webapp/rdfservice/RDFServiceException.java +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/rdfservice/RDFServiceException.java @@ -5,5 +5,9 @@ public class RDFServiceException extends Exception { public RDFServiceException() { super(); } + + public RDFServiceException(String message) { + super(message); + } } diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/rdfservice/impl/RDFServiceImpl.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/rdfservice/impl/RDFServiceImpl.java new file mode 100644 index 000000000..61ff8ab4e --- /dev/null +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/rdfservice/impl/RDFServiceImpl.java @@ -0,0 +1,510 @@ +/* $This file is distributed under the terms of the license in /doc/license.txt$ */ + +package edu.cornell.mannlib.vitro.webapp.rdfservice.impl; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.InputStream; +import java.util.Iterator; +import java.util.List; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.openrdf.query.MalformedQueryException; +import org.openrdf.query.QueryLanguage; +import org.openrdf.query.Update; +import org.openrdf.query.UpdateExecutionException; +import org.openrdf.repository.Repository; +import org.openrdf.repository.RepositoryConnection; +import org.openrdf.repository.RepositoryException; +import org.openrdf.repository.http.HTTPRepository; + +import com.hp.hpl.jena.graph.Node; +import com.hp.hpl.jena.graph.Triple; +import com.hp.hpl.jena.query.Query; +import com.hp.hpl.jena.query.QueryExecution; +import com.hp.hpl.jena.query.QueryExecutionFactory; +import com.hp.hpl.jena.query.QueryFactory; +import com.hp.hpl.jena.query.ResultSet; +import com.hp.hpl.jena.query.ResultSetFormatter; +import com.hp.hpl.jena.rdf.model.Model; +import com.hp.hpl.jena.rdf.model.ModelFactory; +import com.hp.hpl.jena.rdf.model.Statement; +import com.hp.hpl.jena.rdf.model.StmtIterator; + +import edu.cornell.mannlib.vitro.webapp.rdfservice.ChangeListener; +import edu.cornell.mannlib.vitro.webapp.rdfservice.ChangeSet; +import edu.cornell.mannlib.vitro.webapp.rdfservice.ModelChange; +import edu.cornell.mannlib.vitro.webapp.rdfservice.RDFService; +import edu.cornell.mannlib.vitro.webapp.rdfservice.RDFServiceException; + +/* + * API to write, read, and update Vitro's RDF store, with support + * to allow listening, logging and auditing. + */ + +public class RDFServiceImpl implements RDFService { + + private static final Log log = LogFactory.getLog(RDFServiceImpl.class); + private String endpointURI; + private Repository repository; + + /** + * Returns an RDFService for a remote repository + * @param endpointURI + */ + public RDFServiceImpl(String endpointURI) { + this.endpointURI = endpointURI; + this.repository = new HTTPRepository(endpointURI); + } + + /** + * Perform a series of additions to and or removals from specified graphs + * in the RDF store. preConditionSparql will be executed against the + * union of all the graphs in the knowledge base before any updates are made. + * If the precondition query returns a non-empty result, no updates + * will be made. + * + * @param ChangeSet - a set of changes to be performed on the RDF store. + * + * @return boolean - indicates whether the precondition was satisfied + */ + @Override + public boolean changeSetUpdate(ChangeSet changeSet) throws RDFServiceException { + + if (!isPreconditionSatisfied(changeSet.getPreconditionQuery(), changeSet.getPreconditionQueryType())) { + return false; + } + + Iterator csIt = changeSet.getModelChanges().iterator(); + + while (csIt.hasNext()) { + + ModelChange modelChange = csIt.next(); + + if (modelChange.getOperation() == ModelChange.Operation.ADD) { + performAdd(modelChange); + } else if (modelChange.getOperation() == ModelChange.Operation.REMOVE) { + performRemove(modelChange); + } else { + log.error("unrecognized operation type"); + } + } + + return true; + } + + /** + * If the given individual already exists in the default graph, throws an + * RDFServiceException, otherwise adds one type assertion to the default + * graph. + * + * @param String individualURI - URI of the individual to be added + * @param String individualTypeURI - URI of the type for the individual + */ + @Override + public void newIndividual(String individualURI, + String individualTypeURI) throws RDFServiceException { + + } + + /** + * If the given individual already exists in the given graph, throws an + * RDFServiceException, otherwise adds one type assertion to the given + * graph. + * + * @param String individualURI - URI of the individual to be added + * @param String individualTypeURI - URI of the type for the individual + * @param String graphURI - URI of the graph to which to add the individual + */ + @Override + public void newIndividual(String individualURI, + String individualTypeURI, + String graphURI) throws RDFServiceException { + + } + + /** + * Performs a SPARQL construct query against the knowledge base. The query may have + * an embedded graph identifier. + * + * @param String query - the SPARQL query to be executed against the RDF store + * @param RDFService.ModelSerializationFormat resultFormat - type of serialization for RDF result of the SPARQL query + * @param OutputStream outputStream - the result of the query + * + */ + @Override + public InputStream sparqlConstructQuery(String queryStr, + RDFServiceImpl.ModelSerializationFormat resultFormat) throws RDFServiceException { + + Model model = ModelFactory.createDefaultModel(); + Query query = QueryFactory.create(queryStr); + QueryExecution qe = QueryExecutionFactory.sparqlService(endpointURI, query); + + try { + qe.execConstruct(model); + } finally { + qe.close(); + } + + ByteArrayOutputStream serializedModel = new ByteArrayOutputStream(); + model.write(serializedModel,getSerializationFormatString(resultFormat)); + InputStream result = new ByteArrayInputStream(serializedModel.toByteArray()); + return result; + } + + /** + * Performs a SPARQL describe query against the knowledge base. The query may have + * an embedded graph identifier. + * + * @param String query - the SPARQL query to be executed against the RDF store + * @param RDFService.ModelSerializationFormat resultFormat - type of serialization for RDF result of the SPARQL query + * + * @return InputStream - the result of the query + * + */ + @Override + public InputStream sparqlDescribeQuery(String queryStr, + RDFServiceImpl.ModelSerializationFormat resultFormat) throws RDFServiceException { + + Model model = ModelFactory.createDefaultModel(); + Query query = QueryFactory.create(queryStr); + QueryExecution qe = QueryExecutionFactory.sparqlService(endpointURI, query); + + try { + qe.execDescribe(model); + } finally { + qe.close(); + } + + ByteArrayOutputStream serializedModel = new ByteArrayOutputStream(); + model.write(serializedModel,getSerializationFormatString(resultFormat)); + InputStream result = new ByteArrayInputStream(serializedModel.toByteArray()); + return result; + } + + /** + * Performs a SPARQL select query against the knowledge base. The query may have + * an embedded graph identifier. + * + * @param String query - the SPARQL query to be executed against the RDF store + * + * @return InputStream - the result of the query + * + */ + @Override + public InputStream sparqlSelectQuery(String queryStr) throws RDFServiceException { + + Query query = QueryFactory.create(queryStr); + QueryExecution qe = QueryExecutionFactory.sparqlService(endpointURI, query); + + try { + ResultSet resultSet = qe.execSelect(); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + ResultSetFormatter.out(outputStream,resultSet); + InputStream result = new ByteArrayInputStream(outputStream.toByteArray()); + return result; + } finally { + qe.close(); + } + } + + /** + * Performs a SPARQL ASK query against the knowledge base. The query may have + * an embedded graph identifier. + * + * @param String query - the SPARQL query to be executed against the RDF store + * + * @return boolean - the result of the SPARQL query + */ + @Override + public boolean sparqlAskQuery(String queryStr) throws RDFServiceException { + + Query query = QueryFactory.create(queryStr); + QueryExecution qe = QueryExecutionFactory.sparqlService(endpointURI, query); + + try { + return qe.execAsk(); + } finally { + qe.close(); + } + } + + /** + * Get a list of all the graph URIs in the RDF store. + * + * @return List - list of all the graph URIs in the RDF store + */ + @Override + public List getGraphURIs() throws RDFServiceException { + List list = null; + + return list; + } + + /** + * TODO - what is the definition of this method? + * @return + */ + @Override + public void getGraphMetadata() throws RDFServiceException { + + } + + /** + * Get the URI of the default write graph + * + * @return String URI of default write graph + */ + @Override + public String getDefaultWriteGraphURI() throws RDFServiceException { + String graphURI = null; + + return graphURI; + } + + /** + * Get the URI of the default read graph + * + * @return String URI of default read graph + */ + @Override + public String getDefaultReadGraphURI() throws RDFServiceException { + String graphURI = null; + + return graphURI; + } + + /** + * Register a listener to listen to changes in any graph in + * the RDF store. + * + * @return String URI of default read graph + */ + @Override + public void registerListener(ChangeListener changeListener) throws RDFServiceException { + + } + + /** + * Unregister a listener to listen to changes in any graph in + * the RDF store. + * + * @return String URI of default read graph + */ + @Override + public void unregisterListener(ChangeListener changeListener) throws RDFServiceException { + + } + + /** + * Create a ChangeSet object + * + * @return a ChangeSet object + */ + @Override + public ChangeSet manufactureChangeSet() { + return new ChangeSetImpl(); + } + + //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // Non override methods below + //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + protected String getEndpointURI() { + return endpointURI; + } + + protected RepositoryConnection getConnection() { + try { + return this.repository.getConnection(); + } catch (RepositoryException e) { + throw new RuntimeException(e); + } + } + + protected void executeUpdate(String updateString) { + try { + RepositoryConnection conn = getConnection(); + try { + Update u = conn.prepareUpdate(QueryLanguage.SPARQL, updateString); + u.execute(); + } catch (MalformedQueryException e) { + throw new RuntimeException(e); + } catch (UpdateExecutionException e) { + log.error(e,e); + log.error("Update command: \n" + updateString); + throw new RuntimeException(e); + } finally { + conn.close(); + } + } catch (RepositoryException re) { + throw new RuntimeException(re); + } + } + + protected void addTriple(Triple t, String graphURI) { + + //log.info("adding " + t); + + String updateString = "INSERT DATA { " + ((graphURI != null) ? "GRAPH <" + graphURI + "> { " : "" ) + + sparqlNodeUpdate(t.getSubject(), "") + " " + + sparqlNodeUpdate(t.getPredicate(), "") + " " + + sparqlNodeUpdate(t.getObject(), "") + " } " + + ((graphURI != null) ? " } " : ""); + + //log.info(updateString); + + executeUpdate(updateString); + + } + + protected void removeTriple(Triple t, String graphURI) { + + String updateString = "DELETE DATA { " + ((graphURI != null) ? "GRAPH <" + graphURI + "> { " : "" ) + + sparqlNodeUpdate(t.getSubject(), "") + " " + + sparqlNodeUpdate(t.getPredicate(), "") + " " + + sparqlNodeUpdate(t.getObject(), "") + " } " + + ((graphURI != null) ? " } " : ""); + + //log.info(updateString); + + executeUpdate(updateString); + } + + + protected boolean isPreconditionSatisfied(String query, + RDFService.SPARQLQueryType queryType) + throws RDFServiceException { + + Model model = ModelFactory.createDefaultModel(); + + switch (queryType) { + case DESCRIBE: + model.read(sparqlDescribeQuery(query,RDFService.ModelSerializationFormat.N3), null); + return !model.isEmpty(); + case CONSTRUCT: + model.read(sparqlConstructQuery(query,RDFService.ModelSerializationFormat.N3), null); + return !model.isEmpty(); + case SELECT: + return sparqlSelectQueryHasResults(query); + case ASK: + return sparqlAskQuery(query); + default: + throw new RDFServiceException("unrecognized SPARQL query type"); + } + } + + + protected boolean sparqlSelectQueryHasResults(String queryStr) throws RDFServiceException { + + Query query = QueryFactory.create(queryStr); + QueryExecution qe = QueryExecutionFactory.sparqlService(endpointURI, query); + + try { + ResultSet resultSet = qe.execSelect(); + return resultSet.hasNext(); + } finally { + qe.close(); + } + } + + + protected void performAdd(ModelChange modelChange) throws RDFServiceException { + + Model model = ModelFactory.createDefaultModel(); + model.read(modelChange.getSerializedModel(),getSerializationFormatString(modelChange.getSerializationFormat())); + + StmtIterator stmtIt = model.listStatements(); + + while (stmtIt.hasNext()) { + Statement stmt = stmtIt.next(); + Triple triple = new Triple(stmt.getSubject().asNode(), stmt.getPredicate().asNode(), stmt.getObject().asNode()); + addTriple(triple, modelChange.getGraphURI()); + } + } + + protected void performRemove(ModelChange modelChange) throws RDFServiceException { + + Model model = ModelFactory.createDefaultModel(); + model.read(modelChange.getSerializedModel(),getSerializationFormatString(modelChange.getSerializationFormat())); + + StmtIterator stmtIt = model.listStatements(); + + while (stmtIt.hasNext()) { + Statement stmt = stmtIt.next(); + Triple triple = new Triple(stmt.getSubject().asNode(), stmt.getPredicate().asNode(), stmt.getObject().asNode()); + removeTriple(triple, modelChange.getGraphURI()); + } + } + + protected static String getSerializationFormatString(RDFService.ModelSerializationFormat format) { + switch (format) { + case RDFXML: + return "RDFXML"; + case N3: + return "N3"; + default: + log.error("unexpected format in getFormatString"); + return null; + } + } + + protected static String sparqlNodeUpdate(Node node, String varName) { + if (node.isBlank()) { + return "_:" + node.getBlankNodeLabel().replaceAll("\\W", ""); + } else { + return sparqlNode(node, varName); + } + } + + protected static String sparqlNode(Node node, String varName) { + if (node == null || node.isVariable()) { + return varName; + } else if (node.isBlank()) { + return ""; // or throw exception? + } else if (node.isURI()) { + StringBuffer uriBuff = new StringBuffer(); + return uriBuff.append("<").append(node.getURI()).append(">").toString(); + } else if (node.isLiteral()) { + StringBuffer literalBuff = new StringBuffer(); + literalBuff.append("\""); + pyString(literalBuff, node.getLiteralLexicalForm()); + literalBuff.append("\""); + if (node.getLiteralDatatypeURI() != null) { + literalBuff.append("^^<").append(node.getLiteralDatatypeURI()).append(">"); + } else if (node.getLiteralLanguage() != null && node.getLiteralLanguage() != "") { + literalBuff.append("@").append(node.getLiteralLanguage()); + } + return literalBuff.toString(); + } else { + return varName; + } + } + + // see http://www.python.org/doc/2.5.2/ref/strings.html + // or see jena's n3 grammar jena/src/com/hp/hpl/jena/n3/n3.g + protected static void pyString(StringBuffer sbuff, String s) { + for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + + // Escape escapes and quotes + if (c == '\\' || c == '"' ) + { + sbuff.append('\\') ; + sbuff.append(c) ; + continue ; + } + + // Whitespace + if (c == '\n'){ sbuff.append("\\n");continue; } + if (c == '\t'){ sbuff.append("\\t");continue; } + if (c == '\r'){ sbuff.append("\\r");continue; } + if (c == '\f'){ sbuff.append("\\f");continue; } + if (c == '\b'){ sbuff.append("\\b");continue; } + if( c == 7 ) { sbuff.append("\\a");continue; } + + // Output as is (subject to UTF-8 encoding on output that is) + sbuff.append(c) ; + } + } +}