diff --git a/docs/asciidoc/_export.adoc b/docs/asciidoc/_export.adoc index 9cd84089..d24d7e18 100644 --- a/docs/asciidoc/_export.adoc +++ b/docs/asciidoc/_export.adoc @@ -159,14 +159,12 @@ Again, notice the URL enconding of the URI (the clean URI is `http://neo4j.org/i ---- -Additionally, you can provide a graph URI to specify the context of the given resource using the `graphuri` parameter. -Here is how you can serialise as RDF the resource identified by URI `http://www.example.org/exampleDocument#Monica` -but only the statements in the named graph `http://www.example.org/exampleDocument#G1`. Normally such a model will - be the result of importing RDF Quads as described in the <> section. Note that URIS are URL encoded: +Additionally, you can provide a graph URI to specify the context of the given resource using the `graphUri` parameter. +Here is how you can serialise the resource identified by URI `http://www.example.org/exampleDocument#Monica` as RDF in TriG format, but only the statements in the named graph `http://www.example.org/exampleDocument#G1` are included, which was imported in the <> section. [source,Cypher] ---- -:GET /rdf/describe/uri/http%3A%2F%2Fwww.example.org%2FexampleDocument%23Monica?graphuri=http%3A%2F%2Fwww.example.org%2FexampleDocument%23G1&format=TriG +:GET /rdf/describe/uri/http%3A%2F%2Fwww.example.org%2FexampleDocument%23Monica?graphUri=http%3A%2F%2Fwww.example.org%2FexampleDocument%23G1&format=TriG ---- === By Label + property value diff --git a/docs/asciidoc/_import.adoc b/docs/asciidoc/_import.adoc index 9a331659..fed7ed24 100644 --- a/docs/asciidoc/_import.adoc +++ b/docs/asciidoc/_import.adoc @@ -285,6 +285,7 @@ Language tags can also be used as a filter criteria. If we are only interested i CALL semantics.importRDF("file:///Users/jesusbarrasa/Workspaces/neosemantics/docs/rdf/multilang.nt","Turtle", { languageFilter: 'es'}) ---- +[[CustomDataTypes]] === Handling custom data types In RDF custom data types are annotated to literals after the seperator `^^` in form of an IRI. diff --git a/docs/asciidoc/_install.adoc b/docs/asciidoc/_install.adoc index 74e5fd06..8cef6564 100644 --- a/docs/asciidoc/_install.adoc +++ b/docs/asciidoc/_install.adoc @@ -38,4 +38,11 @@ it will produce two jars Please provide feedback and report bugs as GitHub issues or join the Neo4j Community Forum. === Acknowledgements -NSMNTX uses https://rdf4j.eclipse.org/[rdf4j] for parsing and serialising RDF. Eclipse rdf4j is a powerful Java framework for processing and handling RDF data. \ No newline at end of file +NSMNTX uses https://rdf4j.eclipse.org/[rdf4j] for parsing and serialising RDF. Eclipse rdf4j is a powerful Java framework for processing and handling RDF data. + +The following functionalities have been developed by https://github.com/ArkanEmre[Emre Arkan] in the context of his bachelor thesis in cooperation with https://www.eccenca.com/en/index.html[eccenca GmbH] and https://www.th-brandenburg.de[Brandenburg University of Applied Sciences]: + +* Import (<>) and Export of Quadruples +* Preservation of custom data types (<>) +* Deletion of formerly imported RDF data (<>) + diff --git a/src/main/java/semantics/ContextResource.java b/src/main/java/semantics/ContextResource.java index 87380039..4671258c 100644 --- a/src/main/java/semantics/ContextResource.java +++ b/src/main/java/semantics/ContextResource.java @@ -7,7 +7,7 @@ *

* It is used as Key for the Maps containing labels and properties. * - * Created on 06.06.2019 + * Created on 06/06/2019 * * @author Emre Arkan * @see RDFQuadDirectStatementLoader diff --git a/src/main/java/semantics/DirectStatementDeleter.java b/src/main/java/semantics/DirectStatementDeleter.java index d5aa9319..5f4bcc8d 100644 --- a/src/main/java/semantics/DirectStatementDeleter.java +++ b/src/main/java/semantics/DirectStatementDeleter.java @@ -1,7 +1,6 @@ package semantics; import static semantics.RDFImport.RELATIONSHIP; -import static semantics.RDFParserConfig.URL_SHORTEN; import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; @@ -10,7 +9,6 @@ import java.time.LocalDate; import java.time.LocalDateTime; import java.util.ArrayList; -import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; @@ -28,7 +26,7 @@ import org.neo4j.logging.Log; /** - * This class implements an RDF handler to statement-wise delete imported RDF data + * This class implements an RDF handler to statement-wise delete imported RDF triples. * * Created on 03/06/2019. * @@ -37,42 +35,60 @@ class DirectStatementDeleter extends RDFToLPGStatementProcessor implements Callable { private static final Label RESOURCE = Label.label("Resource"); - private final Cache nodeCache; - private long notDeletedStatementCount; private long statementsWithBNodeCount; - private String bNodeInfo; + private String BNodeInfo; DirectStatementDeleter(GraphDatabaseService db, RDFParserConfig conf, Log l) { - super(db, conf, l); nodeCache = CacheBuilder.newBuilder() .maximumSize(conf.getNodeCacheSize()) .build(); - bNodeInfo = ""; + BNodeInfo = ""; notDeletedStatementCount = 0; statementsWithBNodeCount = 0; } + /** + * Analog to endRDF in {@link DirectStatementLoader}, however modified for deletion. + * + * Executed at the end of each commit to inform the user of the current state of the deletion + * process. + */ @Override public void endRDF() throws RDFHandlerException { Util.inTx(graphdb, this); totalTriplesMapped += mappedTripleCounter; - if (parserConfig.getHandleVocabUris() == URL_SHORTEN) { - persistNamespaceNode(); - } - log.info("Successful (last) partial commit of " + mappedTripleCounter + " triples. " + "Total number of triples deleted is " + totalTriplesMapped + " out of " + totalTriplesParsed + " parsed."); } + /** + * Analog to call in {@link DirectStatementLoader}, however strongly modified to delete nodes + * rather than creating or updating. + * + * {@link #resourceLabels}, {@link #resourceProps}, and {@link #statements}, which contain the + * statements to be deleted, are processed respectively. If a statement does not exist in the + * database, {@link #notDeletedStatementCount} is increased to inform the user of not deleted + * statement count. + * + * {@link #statementsWithBNodeCount} counts the number of statements, which could not be deleted + * due to containing a blank node. + * + * {@link #deleteNodeIfEmpty(Node)} is called for each {@code Node} processed, to check and delete + * it, if applicable. + * + * @return An obligatory return, which is always 0, since the overridden method must return an + * Integer + */ @Override public Integer call() throws Exception { for (Map.Entry> entry : resourceLabels.entrySet()) { if (entry.getKey().startsWith("genid")) { + //if the node represents a blank node statementsWithBNodeCount += entry.getValue().size() + 1; continue; } @@ -87,6 +103,7 @@ public Integer call() throws Exception { node = tempNode; entry.getValue().forEach(l -> { if (node != null && node.hasLabel(Label.label(l))) { + //if node exist in the database and has the label to be deleted node.removeLabel(Label.label(l)); } else { notDeletedStatementCount++; @@ -170,17 +187,15 @@ public Integer call() throws Exception { } Node fromNode = null; try { - fromNode = nodeCache.get(st.getSubject().stringValue(), () -> { //throws AnyException - return graphdb.findNode(RESOURCE, "uri", st.getSubject().stringValue()); - }); + fromNode = nodeCache.get(st.getSubject().stringValue(), + () -> graphdb.findNode(RESOURCE, "uri", st.getSubject().stringValue())); } catch (InvalidCacheLoadException icle) { icle.printStackTrace(); } Node toNode = null; try { - toNode = nodeCache.get(st.getObject().stringValue(), () -> { //throws AnyException - return graphdb.findNode(RESOURCE, "uri", st.getObject().stringValue()); - }); + toNode = nodeCache.get(st.getObject().stringValue(), + () -> graphdb.findNode(RESOURCE, "uri", st.getObject().stringValue())); } catch (InvalidCacheLoadException icle) { icle.printStackTrace(); } @@ -214,19 +229,23 @@ public Integer call() throws Exception { deleteNodeIfEmpty(toNode); deleteNodeIfEmpty(fromNode); } - statements.clear(); resourceLabels.clear(); resourceProps.clear(); if (statementsWithBNodeCount > 0) { - setbNodeInfo(statementsWithBNodeCount + setBNodeInfo(statementsWithBNodeCount + " of the statements could not be deleted, due to containing a blank node."); } - - //TODO what to return here? number of nodes and rels? return 0; } + /** + * Analog to periodicOperation in {@link DirectStatementLoader}, however modified for the + * deletion + * + * After each partial commit, a short information about the current state of the deletion process + * is logged. + */ @Override protected void periodicOperation() { Util.inTx(graphdb, this); @@ -236,18 +255,33 @@ protected void periodicOperation() { mappedTripleCounter = 0; } + /** + * @return amount of not deleted statement count and statements with blank node count + */ long getNotDeletedStatementCount() { return notDeletedStatementCount + statementsWithBNodeCount; } - String getbNodeInfo() { - return bNodeInfo; + /** + * @return information about statement not deleted due to containing a blank node + */ + String getBNodeInfo() { + return BNodeInfo; } - private void setbNodeInfo(String bNodeInfo) { - this.bNodeInfo = bNodeInfo; + /* + * Called in {@link DirectStatementDeleter#call()} after the deletion process is done + * @param BNodeInfo information about statement not deleted due to containing a blank node + */ + private void setBNodeInfo(String BNodeInfo) { + this.BNodeInfo = BNodeInfo; } + /** + * Deletes a given {@code node}, if all conditions are met. Call in the {@link #call()} method. + * + * @param node to be deleted + */ private void deleteNodeIfEmpty(Node node) { int nodePropertyCount = node.getAllProperties().size(); int labelCount = Iterators.size(node.getLabels().iterator()); @@ -259,13 +293,7 @@ private void deleteNodeIfEmpty(Node node) { } } - private void persistNamespaceNode() { - Map params = new HashMap<>(); - params.put("props", namespaces); - graphdb.execute("MERGE (n:NamespacePrefixDefinition) SET n+={props}", params); - } - - // Adapted from APOC :) + // Adapted from APOC private Object toPropertyValue(Object value) { if (value instanceof Iterable) { Iterable it = (Iterable) value; diff --git a/src/main/java/semantics/RDFImport.java b/src/main/java/semantics/RDFImport.java index 294963d2..b51e04b9 100644 --- a/src/main/java/semantics/RDFImport.java +++ b/src/main/java/semantics/RDFImport.java @@ -53,10 +53,14 @@ import semantics.result.StreamedStatement; /** - * Created by jbarrasa on 21/03/2016.

RDF importer based on: 1. Instancdes of DatatypeProperties - * become node attributes 2. rdf:type relationships are transformed either into labels or - * relationships to nodes representing the class 3. Instances of ObjectProperties become - * relationships ( See https://jbarrasa.com/2016/06/07/importing-rdf-data-into-neo4j/ ) + * RDF importer based on: 1. Instances of DatatypeProperties become node attributes 2. rdf:type + * relationships are transformed either into labels or relationships to nodes representing the class + * 3. Instances of ObjectProperties become relationships ( See https://jbarrasa.com/2016/06/07/importing-rdf-data-into-neo4j/ + * ) + * + * Created on 21/03/2016 + * + * @author Jesús Barrasa */ public class RDFImport { @@ -170,6 +174,16 @@ public Stream importOntology(@Name("url") String url, return Stream.of(importResults); } + /** + * User-defined procedure to import quadruples. Analog to {@link #importRDF(String, String, Map)} + * but utilizes {@link RDFQuadDirectStatementLoader} to import quadruples + * + * @param url the url or the path to the RDF dataset to be imported + * @param format the format of the given RDF dataset + * @param props the map of properties containing the configuration for the import + * @return {@link Stream} containing information about the results of the import + * process + */ @Procedure(mode = Mode.WRITE) public Stream importQuadRDF(@Name("url") String url, @Name("format") String format, @@ -315,6 +329,16 @@ public Stream previewRDFSnippet(@Name("rdf") String rdfFragment, } + /** + * User-defined procedure to delete triples. Analog to {@link #importRDF(String, String, Map)} but + * utilizes {@link DirectStatementDeleter} to delete triples. + * + * @param url the url or the path to the RDF dataset to be deleted + * @param format the format of the given RDF dataset + * @param props the map of properties containing the configuration for the deletion + * @return {@link Stream} containing information about the results of the deletion + * process + */ @Procedure(mode = Mode.WRITE) @Description("Deletes triples from Neo4j. Works on a graph resulted of importing RDF via " + "semantics.importRDF(). Delete config must match the one used on import.") @@ -342,12 +366,22 @@ public Stream deleteRDF(@Name("url") String url, @Name("format") } finally { deleteResults.setTriplesDeleted( statementDeleter.totalTriplesMapped - statementDeleter.getNotDeletedStatementCount()); - deleteResults.setExtraInfo(statementDeleter.getbNodeInfo()); + deleteResults.setExtraInfo(statementDeleter.getBNodeInfo()); deleteResults.setNamespaces(statementDeleter.getNamespaces()); } return Stream.of(deleteResults); } + /** + * User-defined procedure to delete quadruples. Analog to {@link #deleteRDF(String, String, Map)} + * but utilizes {@link RDFQuadDirectStatementDeleter} to delete quadruples. + * + * @param url the url or the path to the RDF dataset to be deleted + * @param format the format of the given RDF dataset + * @param props the map of properties containing the configuration for the deletion + * @return {@link Stream} containing information about the results of the deletion + * process + */ @Procedure(mode = Mode.WRITE) public Stream deleteQuadRDF(@Name("url") String url, @Name("format") String format, @@ -374,14 +408,21 @@ public Stream deleteQuadRDF(@Name("url") String url, } finally { deleteResults.setTriplesDeleted( statementDeleter.totalTriplesMapped - statementDeleter.getNotDeletedStatementCount()); - deleteResults.setExtraInfo(statementDeleter.getbNodeInfo()); + deleteResults.setExtraInfo(statementDeleter.getBNodeInfo()); deleteResults.setNamespaces(statementDeleter.getNamespaces()); } return Stream.of(deleteResults); } + /** + * User-defined function to retrieve the data type of a literal + * + * @param literal whose data type is to return + * @return a {@code String} containing the custom data type IRI or the XSD data type IRI if given, + * {@code null} otherwise + */ @UserFunction - @Description("Returns the XMLSchema or custom datatype of a property when present") + @Description("Returns the XMLSchema or custom data type of a property when present") public String getDataType(@Name("literal") Object literal) { String result; @@ -413,6 +454,13 @@ public String getDataType(@Name("literal") Object literal) { return result; } + /** + * User-defined function to retrieve the value of a literal + * + * @param literal whose value is to return + * @return a {@code String} containing the value of the literal, which is stripped out using + * regular expressions. If not applicable, original value in {@param literal} + */ @UserFunction @Description("Returns the value of a datatype of a property after stripping out the datatype " + "information when present") @@ -557,29 +605,30 @@ public Stream importJSONAsTree(@Name("containerNode") Node container @Name("jsonpayload") String jsonPayload, @Name(value = "connectingRel", defaultValue = "_jsonTree") String relName) { - //emptystring, no parsing and return null - if (jsonPayload.isEmpty()) return null; + if (jsonPayload.isEmpty()) { + return null; + } HashMap params = new HashMap<>(); - params.put("handleVocabUris","IGNORE"); - params.put("commitSize",Long.MAX_VALUE); + params.put("handleVocabUris", "IGNORE"); + params.put("commitSize", Long.MAX_VALUE); RDFParserConfig conf = new RDFParserConfig(params); PlainJsonStatementLoader plainJSONStatementLoader = new PlainJsonStatementLoader(db, conf, log); try { checkIndexesExist(); - String containerUri = (String)containerNode.getProperty("uri", null); - if (containerUri == null ){ + String containerUri = (String) containerNode.getProperty("uri", null); + if (containerUri == null) { containerUri = "neo4j://indiv#" + UUID.randomUUID().toString(); - containerNode.setProperty("uri",containerUri); + containerNode.setProperty("uri", containerUri); containerNode.addLabel(Label.label("Resource")); } GenericJSONParser rdfParser = new GenericJSONParser(); rdfParser.set(BasicParserSettings.VERIFY_URI_SYNTAX, false); rdfParser.setRDFHandler(plainJSONStatementLoader); rdfParser.parse(new ByteArrayInputStream(jsonPayload.getBytes(Charset.defaultCharset())), - "neo4j://voc#", containerUri,relName); + "neo4j://voc#", containerUri, relName); } catch (IOException | RDFHandlerException | QueryExecutionException | RDFParseException | RDFImportPreRequisitesNotMet e) { e.printStackTrace(); @@ -599,7 +648,8 @@ private void checkIndexesExist() throws RDFImportPreRequisitesNotMet { private boolean missing(Iterator iterator, String indexLabel) { while (iterator.hasNext()) { IndexDefinition indexDef = iterator.next(); - if (!indexDef.isCompositeIndex() && indexDef.getLabels().iterator().next().name().equals(indexLabel) && + if (!indexDef.isCompositeIndex() && indexDef.getLabels().iterator().next().name() + .equals(indexLabel) && indexDef.getPropertyKeys().iterator().next().equals("uri")) { return false; } @@ -651,6 +701,12 @@ public void setTerminationKO(String message) { } + /** + * Analog to {@link ImportResults}, however modified for methods for deletion + * + * @see #deleteRDF(String, String, Map) + * @see #deleteQuadRDF(String, String, Map) + */ public static class DeleteResults { public String terminationStatus = "OK"; @@ -658,18 +714,30 @@ public static class DeleteResults { public Map namespaces; public String extraInfo = ""; + /** + * @param triplesDeleted number of deleted triples + */ public void setTriplesDeleted(long triplesDeleted) { this.triplesDeleted = triplesDeleted; } + /** + * @param extraInfo for the additional information column + */ public void setExtraInfo(String extraInfo) { this.extraInfo = extraInfo; } + /** + * @param namespaces in the database after the deletion + */ public void setNamespaces(Map namespaces) { this.namespaces = namespaces; } + /** + * @param message termination message, set if process was aborted + */ public void setTerminationKO(String message) { this.terminationStatus = "KO"; this.extraInfo = message; diff --git a/src/main/java/semantics/RDFQuadDirectStatementDeleter.java b/src/main/java/semantics/RDFQuadDirectStatementDeleter.java index ed8e4ee5..cca24294 100644 --- a/src/main/java/semantics/RDFQuadDirectStatementDeleter.java +++ b/src/main/java/semantics/RDFQuadDirectStatementDeleter.java @@ -1,7 +1,6 @@ package semantics; import static semantics.RDFImport.RELATIONSHIP; -import static semantics.RDFParserConfig.URL_SHORTEN; import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; @@ -29,52 +28,71 @@ import org.neo4j.logging.Log; /** - * This class implements an RDF handler to statement-wise delete imported RDF data sets + * /** This class implements an RDF handler to statement-wise delete imported RDF quadruples. * * Created on 18/06/2019. * * @author Emre Arkan */ - class RDFQuadDirectStatementDeleter extends RDFQuadToLPGStatementProcessor implements Callable { private static final Label RESOURCE = Label.label("Resource"); - private Cache nodeCache; private long notDeletedStatementCount; - private long statementsWithbNodeCount; - private String bNodeInfo; + private long statementsWithBNodeCount; + private String BNodeInfo; RDFQuadDirectStatementDeleter(GraphDatabaseService db, RDFParserConfig conf, Log l) { super(db, conf, l); nodeCache = CacheBuilder.newBuilder() .maximumSize(conf.getNodeCacheSize()) .build(); - bNodeInfo = ""; + BNodeInfo = ""; notDeletedStatementCount = 0; - statementsWithbNodeCount = 0; + statementsWithBNodeCount = 0; } + /** + * Analog to endRDF in {@link DirectStatementDeleter} + * + * Executed at the end of each commit to inform the user of the current state of the deletion + * process. + */ @Override public void endRDF() throws RDFHandlerException { Util.inTx(graphdb, this); totalTriplesMapped += mappedTripleCounter; - if (parserConfig.getHandleVocabUris() == URL_SHORTEN) { - persistNamespaceNode(); - } - log.info("Successful (last) partial commit of " + mappedTripleCounter + " triples. " + "Total number of triples deleted is " + totalTriplesMapped + " out of " + totalTriplesParsed + " parsed."); } + /** + * Analog to call in {@link DirectStatementDeleter}, however modified to delete quadruples rather + * than triples + * + * {@link #resourceLabels}, {@link #resourceProps}, and {@link #statements}, which contain the + * statements to be deleted, are processed respectively. If a statement does not exist in the + * database, {@link #notDeletedStatementCount} is increased to inform the user of not deleted + * statement count + * + * {@link #statementsWithBNodeCount} counts the number of statements, which could not be deleted + * due to containing a blank node. + * + * {@link #deleteNodeIfEmpty(Node)} is called for each {@code Node} processed, to check and delete + * it, if applicable + * + * @return An obligatory return, which is always 0, since the overridden method must return an + * Integer + */ @Override public Integer call() throws Exception { for (Map.Entry> entry : resourceLabels.entrySet()) { if (entry.getKey().getUri().startsWith("genid")) { - statementsWithbNodeCount += entry.getValue().size() + 1; + //if the node represents a blank node + statementsWithBNodeCount += entry.getValue().size() + 1; continue; } Node tempNode = null; @@ -109,6 +127,7 @@ public Node call() { node = tempNode; entry.getValue().forEach(l -> { if (node != null && node.hasLabel(Label.label(l))) { + //if node exist in the database and has the label to be deleted node.removeLabel(Label.label(l)); } else { notDeletedStatementCount++; @@ -186,7 +205,7 @@ public Node call() { for (Statement st : statements) { if (st.getSubject() instanceof BNode != st.getObject() instanceof BNode) { - statementsWithbNodeCount++; + statementsWithBNodeCount++; } if (st.getSubject() instanceof BNode || st.getObject() instanceof BNode) { continue; @@ -195,28 +214,25 @@ public Node call() { st.getContext() != null ? st.getContext().stringValue() : null); Node fromNode = null; try { - fromNode = nodeCache.get(from, new Callable() { - @Override - public Node call() { //throws AnyException - Node node = null; - Map params = new HashMap<>(); - String cypher = buildCypher(st.getSubject().stringValue(), - st.getContext() != null ? st.getContext().stringValue() : null, - params); - Result result = graphdb.execute(cypher, params); + fromNode = nodeCache.get(from, () -> { + Node node = null; + Map params = new HashMap<>(); + String cypher = buildCypher(st.getSubject().stringValue(), + st.getContext() != null ? st.getContext().stringValue() : null, + params); + Result result = graphdb.execute(cypher, params); + if (result.hasNext()) { + node = (Node) result.next().get("n"); if (result.hasNext()) { - node = (Node) result.next().get("n"); - if (result.hasNext()) { - String props = - "{uri: " + st.getSubject().stringValue() + - (st.getContext() == null ? "}" : - ", graphUri: " + st.getContext().stringValue() + "}"); - throw new IllegalStateException( - "There are multiple matching nodes for the given properties " + props); - } + String props = + "{uri: " + st.getSubject().stringValue() + + (st.getContext() == null ? "}" : + ", graphUri: " + st.getContext().stringValue() + "}"); + throw new IllegalStateException( + "There are multiple matching nodes for the given properties " + props); } - return node; } + return node; }); } catch (InvalidCacheLoadException | IllegalStateException e) { e.printStackTrace(); @@ -225,28 +241,25 @@ public Node call() { //throws AnyException st.getContext() != null ? st.getContext().stringValue() : null); Node toNode = null; try { - toNode = nodeCache.get(to, new Callable() { - @Override - public Node call() { //throws AnyException - Node node = null; - Map params = new HashMap<>(); - String cypher = buildCypher(st.getObject().stringValue(), - st.getContext() != null ? st.getContext().stringValue() : null, - params); - Result result = graphdb.execute(cypher, params); + toNode = nodeCache.get(to, () -> { + Node node = null; + Map params = new HashMap<>(); + String cypher = buildCypher(st.getObject().stringValue(), + st.getContext() != null ? st.getContext().stringValue() : null, + params); + Result result = graphdb.execute(cypher, params); + if (result.hasNext()) { + node = (Node) result.next().get("n"); if (result.hasNext()) { - node = (Node) result.next().get("n"); - if (result.hasNext()) { - String props = - "{uri: " + st.getObject().stringValue() + - (st.getContext() == null ? "}" : - ", graphUri: " + st.getContext().stringValue() + "}"); - throw new IllegalStateException( - "There are multiple matching nodes for the given properties " + props); - } + String props = + "{uri: " + st.getObject().stringValue() + + (st.getContext() == null ? "}" : + ", graphUri: " + st.getContext().stringValue() + "}"); + throw new IllegalStateException( + "There are multiple matching nodes for the given properties " + props); } - return node; } + return node; }); } catch (InvalidCacheLoadException | IllegalStateException e) { e.printStackTrace(); @@ -255,7 +268,6 @@ public Node call() { //throws AnyException notDeletedStatementCount++; continue; } - // find relationship if it exists if (fromNode.getDegree(RelationshipType.withName(handleIRI(st.getPredicate(), RELATIONSHIP)), Direction.OUTGOING) < @@ -282,19 +294,19 @@ public Node call() { //throws AnyException deleteNodeIfEmpty(toNode); deleteNodeIfEmpty(fromNode); } - statements.clear(); resourceLabels.clear(); resourceProps.clear(); - if (statementsWithbNodeCount > 0) { - setbNodeInfo(statementsWithbNodeCount + if (statementsWithBNodeCount > 0) { + setBNodeInfo(statementsWithBNodeCount + " of the statements could not be deleted, due to containing a blank node."); } - - //TODO what to return here? number of nodes and rels? return 0; } + /** + * Analog to periodicOperation in {@link DirectStatementDeleter} + */ @Override protected void periodicOperation() { Util.inTx(graphdb, this); @@ -304,18 +316,43 @@ protected void periodicOperation() { mappedTripleCounter = 0; } + /** + * Analog to getNotDeletedStatementCount in {@link DirectStatementDeleter} + * + * @return amount of not deleted statement count and statements with blank node count + */ long getNotDeletedStatementCount() { - return notDeletedStatementCount + statementsWithbNodeCount; + return notDeletedStatementCount + statementsWithBNodeCount; } - String getbNodeInfo() { - return bNodeInfo; + /** + * Analog to getBNodeInfo in {@link DirectStatementDeleter} + * + * @return information about statement not deleted due to containing a blank node + */ + String getBNodeInfo() { + return BNodeInfo; } - private void setbNodeInfo(String bNodeInfo) { - this.bNodeInfo = bNodeInfo; + /** + * Analog to setBNodeInfo in {@link DirectStatementDeleter} + * + * Called in {@link DirectStatementDeleter#call()} after the deletion process is done + * + * @param BNodeInfo information about statement not deleted due to containing a blank node + */ + private void setBNodeInfo(String BNodeInfo) { + this.BNodeInfo = BNodeInfo; } + /** + * Analog to deleteNode in {@link DirectStatementDeleter}, however slightly modified to delete + * quadruples as well. + * + * Deletes a given {@code node}, if all conditions are met. Call in the {@link #call()} method. + * + * @param node node to be deleted + */ private void deleteNodeIfEmpty(Node node) { int nodePropertyCount = node.getAllProperties().size(); int labelCount = Iterators.size(node.getLabels().iterator()); @@ -329,13 +366,7 @@ private void deleteNodeIfEmpty(Node node) { } } - private void persistNamespaceNode() { - Map params = new HashMap<>(); - params.put("props", namespaces); - graphdb.execute("MERGE (n:NamespacePrefixDefinition) SET n+={props}", params); - } - - // Adapted from APOC :) + // Adapted from APOC private Object toPropertyValue(Object value) { if (value instanceof Iterable) { Iterable it = (Iterable) value; diff --git a/src/main/java/semantics/RDFQuadDirectStatementLoader.java b/src/main/java/semantics/RDFQuadDirectStatementLoader.java index 9cafa1cc..bfb1507b 100644 --- a/src/main/java/semantics/RDFQuadDirectStatementLoader.java +++ b/src/main/java/semantics/RDFQuadDirectStatementLoader.java @@ -25,11 +25,12 @@ import org.neo4j.logging.Log; /** + * This class implements an RDF handler to statement-wise imported RDF quadruples. + * * Created on 06/06/2019. * * @author Emre Arkan */ - class RDFQuadDirectStatementLoader extends RDFQuadToLPGStatementProcessor implements Callable { @@ -38,68 +39,79 @@ class RDFQuadDirectStatementLoader extends RDFQuadToLPGStatementProcessor implem private Cache nodeCache; RDFQuadDirectStatementLoader(GraphDatabaseService db, RDFParserConfig conf, Log l) { - super(db, conf, l); nodeCache = CacheBuilder.newBuilder() .maximumSize(conf.getNodeCacheSize()) .build(); } + /** + * Analog to endRDF in {@link DirectStatementLoader} + * + * Executed at the end of each commit to inform the user of the current state of the import + * process. + */ @Override public void endRDF() throws RDFHandlerException { Util.inTx(graphdb, this); totalTriplesMapped += mappedTripleCounter; if (parserConfig.getHandleVocabUris() == URL_SHORTEN) { - // Namespaces are only persisted at the end of each periodic commit. - // This makes importRDF not thread safe when using url shortening. TODO: fix this. persistNamespaceNode(); } - log.info("Import complete: " + totalTriplesMapped + " triples ingested out of " + totalTriplesParsed + " parsed"); } + /** + * Analog to persistNamespaceNode in {@link DirectStatementLoader} + */ private void persistNamespaceNode() { Map params = new HashMap<>(); params.put("props", namespaces); graphdb.execute("MERGE (n:NamespacePrefixDefinition) SET n+={props}", params); } + /** + * Analog to call in {@link DirectStatementLoader}, however strongly modified to process + * quadruples rather than triples. + * + * {@link #resourceLabels}, {@link #resourceProps}, and {@link #statements}, which contain the + * statements to be imported are processed respectively. If a statement already exist in the + * database, it is ignored. + * + * @return An obligatory return, which is always 0, since the overridden method must return an + * Integer + */ @Override public Integer call() throws Exception { - int count = 0; - for (Map.Entry> entry : resourceLabels.entrySet()) { - final Node node = nodeCache.get(entry.getKey(), new Callable() { - @Override - public Node call() { - Node node = null; - Map params = new HashMap<>(); - String cypher = buildCypher(entry.getKey().getUri(), - entry.getKey().getGraphUri(), - params); - Result result = graphdb.execute(cypher, params); + final Node node = nodeCache.get(entry.getKey(), () -> { + Node searched_node = null; + Map params = new HashMap<>(); + String cypher = buildCypher(entry.getKey().getUri(), + entry.getKey().getGraphUri(), + params); + Result result = graphdb.execute(cypher, params); + if (result.hasNext()) { + searched_node = (Node) result.next().get("n"); if (result.hasNext()) { - node = (Node) result.next().get("n"); - if (result.hasNext()) { - String props = - "{uri: " + entry.getKey().getUri() + - (entry.getKey().getGraphUri() == null ? "}" : - ", graphUri: " + entry.getKey().getGraphUri() + "}"); - throw new IllegalStateException( - "There are multiple matching nodes for the given properties " + props); - } + String props = + "{uri: " + entry.getKey().getUri() + + (entry.getKey().getGraphUri() == null ? "}" : + ", graphUri: " + entry.getKey().getGraphUri() + "}"); + throw new IllegalStateException( + "There are multiple matching nodes for the given properties " + props); } - if (node == null) { - node = graphdb.createNode(RESOURCE); - node.setProperty("uri", entry.getKey().getUri()); - if (entry.getKey().getGraphUri() != null) { - node.setProperty("graphUri", entry.getKey().getGraphUri()); - } + } + if (searched_node == null) { + searched_node = graphdb.createNode(RESOURCE); + searched_node.setProperty("uri", entry.getKey().getUri()); + if (entry.getKey().getGraphUri() != null) { + searched_node.setProperty("graphUri", entry.getKey().getGraphUri()); } - return node; } + return searched_node; }); entry.getValue().forEach(l -> node.addLabel(Label.label(l))); @@ -113,12 +125,10 @@ public Node call() { Object[] properties = (Object[]) currentValue; for (int i = 0; i < properties.length; i++) { ((List) v).add(properties[i]); - //here an exception can be raised if types are conflicting } } else { ((List) v).add(node.getProperty(k)); } - //we make it a set to remove duplicates. Semantics of multivalued props in RDF. node.setProperty(k, toPropertyValue(((List) v).stream().collect(Collectors.toSet()))); } } else { @@ -130,65 +140,58 @@ public Node call() { for (Statement st : statements) { ContextResource from = new ContextResource(st.getSubject().stringValue(), st.getContext() != null ? st.getContext().stringValue() : null); - final Node fromNode = nodeCache.get(from, new Callable() { - @Override - public Node call() { //throws AnyException - Node node; - Map params = new HashMap<>(); - String cypher = buildCypher(st.getSubject().stringValue(), - st.getContext() != null ? st.getContext().stringValue() : null, - params); - Result result = graphdb.execute(cypher, params); + final Node fromNode = nodeCache.get(from, () -> { + Node node; + Map params = new HashMap<>(); + String cypher = buildCypher(st.getSubject().stringValue(), + st.getContext() != null ? st.getContext().stringValue() : null, + params); + Result result = graphdb.execute(cypher, params); + if (result.hasNext()) { + node = (Node) result.next().get("n"); if (result.hasNext()) { - node = (Node) result.next().get("n"); - if (result.hasNext()) { - String props = - "{uri: " + st.getSubject().stringValue() + - (st.getContext() == null ? "}" : - ", graphUri: " + st.getContext().stringValue() + "}"); - throw new IllegalStateException( - "There are multiple matching nodes for the given properties " + props); - } - } else { - throw new NoSuchElementException( - "There exists no node with \"uri\": " + st.getSubject().stringValue() - + " and \"graphUri\": " + st.getContext().stringValue()); + String props = + "{uri: " + st.getSubject().stringValue() + + (st.getContext() == null ? "}" : + ", graphUri: " + st.getContext().stringValue() + "}"); + throw new IllegalStateException( + "There are multiple matching nodes for the given properties " + props); } - return node; + } else { + throw new NoSuchElementException( + "There exists no node with \"uri\": " + st.getSubject().stringValue() + + " and \"graphUri\": " + st.getContext().stringValue()); } + return node; }); ContextResource to = new ContextResource(st.getObject().stringValue(), st.getContext() != null ? st.getContext().stringValue() : null); - final Node toNode = nodeCache.get(to, new Callable() { - @Override - public Node call() { //throws AnyException - Node node; - Map params = new HashMap<>(); - String cypher = buildCypher(st.getObject().stringValue(), - st.getContext() != null ? st.getContext().stringValue() : null, - params); - Result result = graphdb.execute(cypher, params); + final Node toNode = nodeCache.get(to, () -> { + Node node; + Map params = new HashMap<>(); + String cypher = buildCypher(st.getObject().stringValue(), + st.getContext() != null ? st.getContext().stringValue() : null, + params); + Result result = graphdb.execute(cypher, params); + if (result.hasNext()) { + node = (Node) result.next().get("n"); if (result.hasNext()) { - node = (Node) result.next().get("n"); - if (result.hasNext()) { - String props = - "{uri: " + st.getObject().stringValue() + - (st.getContext() == null ? "}" : - ", graphUri: " + st.getContext().stringValue() + "}"); - throw new IllegalStateException( - "There are multiple matching nodes for the given properties " + props); - } - } else { - throw new NoSuchElementException( - "There exists no node with \"uri\": " + st.getSubject().stringValue() - + " and \"graphUri\": " + st.getContext().stringValue()); + String props = + "{uri: " + st.getObject().stringValue() + + (st.getContext() == null ? "}" : + ", graphUri: " + st.getContext().stringValue() + "}"); + throw new IllegalStateException( + "There are multiple matching nodes for the given properties " + props); } - return node; + } else { + throw new NoSuchElementException( + "There exists no node with \"uri\": " + st.getSubject().stringValue() + + " and \"graphUri\": " + st.getContext().stringValue()); } + return node; }); - // check if the rel is already present. If so, don't recreate. - // explore the node with the lowest degree + // Check if the relationship is already present. If so, don't recreate. boolean found = false; if (fromNode.getDegree(RelationshipType.withName(handleIRI(st.getPredicate(), RELATIONSHIP)), Direction.OUTGOING) < @@ -219,15 +222,15 @@ public Node call() { //throws AnyException RelationshipType.withName(handleIRI(st.getPredicate(), RELATIONSHIP))); } } - statements.clear(); resourceLabels.clear(); resourceProps.clear(); - - //TODO what to return here? number of nodes and rels? return 0; } + /** + * Analog to periodicOperation in {@link DirectStatementLoader} + */ @Override protected void periodicOperation() { Util.inTx(graphdb, this); @@ -236,7 +239,7 @@ protected void periodicOperation() { persistNamespaceNode(); } - // Stolen from APOC :) + // Adapted from APOC private Object toPropertyValue(Object value) { Iterable it = (Iterable) value; Object first = Iterables.firstOrNull(it); diff --git a/src/main/java/semantics/RDFQuadToLPGStatementProcessor.java b/src/main/java/semantics/RDFQuadToLPGStatementProcessor.java index 5b3936c6..bb1a91e4 100644 --- a/src/main/java/semantics/RDFQuadToLPGStatementProcessor.java +++ b/src/main/java/semantics/RDFQuadToLPGStatementProcessor.java @@ -19,19 +19,18 @@ import org.eclipse.rdf4j.model.Statement; import org.eclipse.rdf4j.model.Value; import org.eclipse.rdf4j.model.vocabulary.RDF; -import org.eclipse.rdf4j.rio.RDFHandler; import org.neo4j.graphdb.GraphDatabaseService; import org.neo4j.logging.Log; /** + * This class extends {@link RDFToLPGStatementProcessor} to process RDF quadruples. + * * Created on 06/06/2019. * * @author Emre Arkan */ - -abstract class RDFQuadToLPGStatementProcessor extends RDFToLPGStatementProcessor implements - RDFHandler { +abstract class RDFQuadToLPGStatementProcessor extends RDFToLPGStatementProcessor { Map> resourceProps; Map> resourceLabels; @@ -42,6 +41,12 @@ abstract class RDFQuadToLPGStatementProcessor extends RDFToLPGStatementProcessor resourceLabels = new HashMap<>(); } + /** + * Analog to handleStatement in {@link RDFToLPGStatementProcessor}, however modified for {@link + * ContextResource}. + * + * @param st statement to be handled + */ @Override public void handleStatement(Statement st) { Resource context = st.getContext(); @@ -80,8 +85,14 @@ public void handleStatement(Statement st) { } } - protected abstract void periodicOperation(); - + /** + * Constructs a Cypher query to find a specific node defined by its uri and optionally graphUri + * + * @param uri of the node that is searched + * @param graphUri of the node that is searched, can be {@code null} + * @param params parameters of the Cypher query + * @return constructed Cypher query + */ String buildCypher(String uri, String graphUri, Map params) { Preconditions.checkNotNull(uri); StringBuilder cypher = new StringBuilder(); @@ -98,14 +109,23 @@ String buildCypher(String uri, String graphUri, Map params) { return cypher.toString(); } + /** + * Analog to setProp in {@link RDFToLPGStatementProcessor}, however modified for {@link + * ContextResource}. + * + * Adds a given predicate-literal pair to {@link #resourceProps} of a given {@link + * ContextResource} + * + * @param contextResource, to which the property belongs + * @param propertyIRI, predicate IRI of the statement + * @param propValueRaw literal value + * @return true if property value is not {@code null}, false otherwise + */ private boolean setProp(ContextResource contextResource, IRI propertyIRI, Literal propValueRaw) { Map props; - String propName = handleIRI(propertyIRI, PROPERTY); - Object propValue = getObjectValue(propertyIRI, propValueRaw); - if (propValue != null) { if (!resourceProps.containsKey(contextResource)) { props = initialiseProps(contextResource); @@ -114,8 +134,6 @@ private boolean setProp(ContextResource contextResource, IRI propertyIRI, props = resourceProps.get(contextResource); } if (parserConfig.getHandleMultival() == PROP_OVERWRITE) { - // Ok for single valued props. If applied to multivalued ones - // only the last value read is kept. props.put(propName, propValue); } else if (parserConfig.getHandleMultival() == PROP_ARRAY) { if (parserConfig.getMultivalPropList() == null || parserConfig.getMultivalPropList() @@ -123,7 +141,6 @@ private boolean setProp(ContextResource contextResource, IRI propertyIRI, if (props.containsKey(propName)) { List propVals = (List) props.get(propName); propVals.add(propValue); - // If multiple datatypes are tried to be stored in the same List, a java.lang // .ArrayStoreException arises } else { @@ -132,7 +149,6 @@ private boolean setProp(ContextResource contextResource, IRI propertyIRI, props.put(propName, propVals); } } else { - //if handleMultival set to ARRAY but prop not in list, then default to overwrite. props.put(propName, propValue); } } @@ -140,6 +156,15 @@ private boolean setProp(ContextResource contextResource, IRI propertyIRI, return propValue != null; } + /** + * Analog to setLabel in {@link RDFToLPGStatementProcessor}, however modified for {@link + * ContextResource}. + * + * Adds a given label to {@link #resourceLabels} of a given {@link ContextResource}. + * + * @param contextResource, to which the label belongs + * @param label to be added to {@link #resourceLabels} of the given {@link ContextResource} + */ private void setLabel(ContextResource contextResource, String label) { Set labels; if (!resourceLabels.containsKey(contextResource)) { @@ -151,23 +176,61 @@ private void setLabel(ContextResource contextResource, String label) { labels.add(label); } + /** + * Analog to addResource in {@link RDFToLPGStatementProcessor}, however modified for {@link + * ContextResource}. + * + * Adds a given {@link ContextResource} as a resource. + * + * @param contextResource which is to be added as a resource, if not already existent + */ private void addResource(ContextResource contextResource) { if (!resourceLabels.containsKey(contextResource)) { initialise(contextResource); } } + /** + * Analog to initialise in {@link RDFToLPGStatementProcessor}, however modified for {@link + * ContextResource}. + * + * Calls {@link #initialiseProps(ContextResource)} and {@link #initialiseLabels(ContextResource)} + * for the given {@link ContextResource} to initialize these. + * + * @param contextResource whose {@link #resourceProps} and {@link #resourceLabels} are to be + * initialized + */ private void initialise(ContextResource contextResource) { initialiseProps(contextResource); initialiseLabels(contextResource); } + /** + * Analog to initialiseLabels in {@link RDFToLPGStatementProcessor}, however modified for {@link + * ContextResource}. + * + * Initializes the {@link Set} of a {@link ContextResource} that contains the labels + * (Object IRI) of the given {@link ContextResource}. + * + * @param contextResource whose label set is to be initialized + * @return {@link Set} for the label {@link Set} of the current {@link ContextResource} + */ private Set initialiseLabels(ContextResource contextResource) { Set labels = new HashSet<>(); resourceLabels.put(contextResource, labels); return labels; } + /** + * Analog to initialiseProps in {@link RDFToLPGStatementProcessor}, however modified for {@link + * ContextResource}. + * + * Initializes the {@link HashMap} of the given {@link ContextResource} that contains {@link + * String} as key (predicate IRI) and {@link Object} as value (literal). + * + * @param contextResource whose property map is to be initialized + * @return {@link HashMap} for the predicate-literal pairs of the given {@link ContextResource} + */ private HashMap initialiseProps(ContextResource contextResource) { HashMap props = new HashMap<>(); resourceProps.put(contextResource, props); diff --git a/src/main/java/semantics/RDFToLPGStatementProcessor.java b/src/main/java/semantics/RDFToLPGStatementProcessor.java index 399baabb..0c2da286 100644 --- a/src/main/java/semantics/RDFToLPGStatementProcessor.java +++ b/src/main/java/semantics/RDFToLPGStatementProcessor.java @@ -38,9 +38,21 @@ /** - * Created by jbarrasa on 15/04/2019. + * An RDF handler which is the superclass to various RDF handlers + * + * @see DirectStatementLoader + * @see OntologyImporter + * @see StatementPreviewer + * @see PlainJsonStatementLoader + * @see DirectStatementDeleter + * @see RDFQuadToLPGStatementProcessor + * @see RDFQuadDirectStatementLoader + * @see RDFQuadDirectStatementDeleter + * + * Created on 15/04/2019. + * + * @author Jesús Barrasa */ - abstract class RDFToLPGStatementProcessor extends ConfiguredStatementHandler { protected final Log log; @@ -124,12 +136,18 @@ private String nextPrefix() { } /** - * Processing for literals as follows Mapping according to this figure: - * https://www.w3.org/TR/xmlschema11-2/#built-in-datatypes String -> String Each sub-category of - * integer -> long decimal, float, and double -> double boolean -> boolean Custom data type -> - * String (value + CUSTOM_DATA_TYPE_SEPERATOR + custom DT IRI) + * Processing for literals with a mapping according to this figure: + * https://www.w3.org/TR/xmlschema11-2/#built-in-datatypes + * + * String -> String + * Al sub-categories of integer -> long + * decimal, float, and double -> double + * boolean -> boolean + * Custom data type -> String (value + CUSTOM_DATA_TYPE_SEPERATOR + custom data type IRI) * - * @return processed literal + * @param propertyIRI predicate IRI of the statement + * @param object literal value of the statement + * @return processed literal value */ Object getObjectValue(IRI propertyIRI, Literal object) { IRI datatype = object.getDatatype(); @@ -188,11 +206,21 @@ Object getObjectValue(IRI propertyIRI, Literal object) { return object.stringValue(); } + /** + * @param datatype to be classified + * @return {@code boolean} true if, the data type IRI should be mapped as {@code double}, false + * otherwise + */ private boolean typeMapsToDouble(IRI datatype) { return datatype.equals(XMLSchema.DECIMAL) || datatype.equals(XMLSchema.DOUBLE) || datatype.equals(XMLSchema.FLOAT); } + /** + * @param datatype to be classified + * @return {@code boolean} true if, the data type IRI should be mapped as {@code long}, false + * otherwise + */ private boolean typeMapsToLongType(IRI datatype) { return datatype.equals(XMLSchema.INTEGER) || datatype.equals(XMLSchema.LONG) || datatype .equals(XMLSchema.INT) || diff --git a/src/main/java/semantics/extension/RDFEndpoint.java b/src/main/java/semantics/extension/RDFEndpoint.java index a5266e07..4a1df9d8 100644 --- a/src/main/java/semantics/extension/RDFEndpoint.java +++ b/src/main/java/semantics/extension/RDFEndpoint.java @@ -55,7 +55,11 @@ import semantics.ContextResource; /** - * Created by jbarrasa on 08/09/2016. + * This class implements an RDF endpoint to export data in various RDF serializations from Neo4j. + * + * Created on 08/09/2016. + * + * @author Jesús Barrasa */ @Path("/") public class RDFEndpoint { @@ -136,6 +140,15 @@ public Response cypherOnPlainLPG(@Context GraphDatabaseService gds, }).build(); } + /** + * This method processes a {@link POST} request. It executes the given Cypher query, if the result + * set is not empty, LPG nodes and relationships are converted into RDF statements in the chosen + * serialization accordingly + * + * @param body {@link Map} containing Cypher query, optional query parameters and the desired + * export serialization + * @return HTTP response containing the created RDF + */ @POST @Path("/cypheronrdf") @Produces({"application/rdf+xml", "text/plain", "text/turtle", "text/n3", @@ -213,18 +226,26 @@ public Response cypherOnImportedRDF(@Context GraphDatabaseService gds, }).build(); } + /** + * Processes an LPG relationship and if all conditions are met, an RDF statement is created. + * + * @param namespaces in the database + * @param writer to export RDF statements in a given serialization + * @param valueFactory to construct statements from LPG relationships + * @param rel relationship to be processed + */ private void processRelationship(Map namespaces, RDFWriter writer, SimpleValueFactory valueFactory, String baseVocabNS, Relationship rel) { - Resource subject = buildSubjectOrContext(rel.getStartNode().getProperty("uri").toString(), + Resource subject = createResourceOrBNode(rel.getStartNode().getProperty("uri").toString(), valueFactory); IRI predicate = valueFactory.createIRI(buildURI(baseVocabNS, rel.getType().name(), namespaces)); - Resource object = buildSubjectOrContext(rel.getEndNode().getProperty("uri").toString(), + Resource object = createResourceOrBNode(rel.getEndNode().getProperty("uri").toString(), valueFactory); Resource context = null; if (rel.getStartNode().hasProperty("graphUri") && rel.getEndNode().hasProperty("graphUri")) { if (rel.getStartNode().getProperty("graphUri").toString() .equals(rel.getEndNode().getProperty("graphUri").toString())) { - context = buildSubjectOrContext(rel.getStartNode().getProperty("graphUri").toString(), + context = createResourceOrBNode(rel.getStartNode().getProperty("graphUri").toString(), valueFactory); } else { throw new IllegalStateException( @@ -238,6 +259,14 @@ private void processRelationship(Map namespaces, RDFWriter write writer.handleStatement(valueFactory.createStatement(subject, predicate, object, context)); } + /** + * Processes an LPG node and creates RDF statements from imported statements stored in the node. + * + * @param namespaces in the database + * @param writer to export RDF statements in a given serialization + * @param valueFactory to construct statements from LPG relationships + * @param node node to be processed + */ private void processNode(Map namespaces, RDFWriter writer, SimpleValueFactory valueFactory, String baseVocabNS, Node node) { @@ -249,7 +278,7 @@ private void processNode(Map namespaces, RDFWriter writer, writer.handleStatement( valueFactory .createStatement( - buildSubjectOrContext(node.getProperty("uri").toString(), valueFactory), + createResourceOrBNode(node.getProperty("uri").toString(), valueFactory), RDF.TYPE, valueFactory.createIRI(buildURI(baseVocabNS, label.name(), namespaces)), node.hasProperty("graphUri") ? valueFactory @@ -260,12 +289,12 @@ private void processNode(Map namespaces, RDFWriter writer, Map allProperties = node.getAllProperties(); for (String key : allProperties.keySet()) { if (!key.equals("uri") && !key.equals("graphUri")) { - Resource subject = buildSubjectOrContext(node.getProperty("uri").toString(), valueFactory); + Resource subject = createResourceOrBNode(node.getProperty("uri").toString(), valueFactory); IRI predicate = valueFactory.createIRI(buildURI(baseVocabNS, key, namespaces)); Object propertyValueObject = allProperties.get(key); Resource context = null; if (node.hasProperty("graphUri")) { - context = buildSubjectOrContext(node.getProperty("graphUri").toString(), valueFactory); + context = createResourceOrBNode(node.getProperty("graphUri").toString(), valueFactory); } if (propertyValueObject instanceof long[]) { for (int i = 0; i < ((long[]) propertyValueObject).length; i++) { @@ -325,7 +354,14 @@ private void processNode(Map namespaces, RDFWriter writer, } } - private Resource buildSubjectOrContext(String id, ValueFactory vf) { + /** + * Creates an IRI or a blank node from a given resource. + * + * @param id IRI of the resource + * @param vf ValueFactory to create the resource + * @return created resource or blank node + */ + private Resource createResourceOrBNode(String id, ValueFactory vf) { Resource result; try { result = vf.createIRI(id); @@ -336,14 +372,25 @@ private Resource buildSubjectOrContext(String id, ValueFactory vf) { return result; } - + /** + * This method processes a {@link GET} request. It searches for a given LPG node representing a + * resource with an optional context, if found statements stored in the node are converted into + * RDF statements in the chosen serialization. + * + * @param uriParam IRI of the resource to export + * @param graphUriParam graph IRI of the resource to export + * @param excludeContextParam parameter to include or exclude incoming or outgoing relationships + * from the node representing the resource + * @param format RDF serialization format for the export + * @return HTTP response containing the created RDF + */ @GET - @Path("/describe/uri/{nodeuri}") + @Path("/describe/uri/{nodeUri}") @Produces({"application/rdf+xml", "text/plain", "text/turtle", "text/n3", "application/trig", "application/ld+json", "application/n-quads"}) - public Response nodebyuri(@Context GraphDatabaseService gds, - @PathParam("nodeuri") String uriParam, - @QueryParam("graphuri") String graphUriParam, + public Response nodeByUri(@Context GraphDatabaseService gds, + @PathParam("nodeUri") String uriParam, + @QueryParam("graphUri") String graphUriParam, @QueryParam("excludeContext") String excludeContextParam, @QueryParam("format") String format, @HeaderParam("accept") String acceptHeaderParam) { @@ -356,14 +403,14 @@ public Response nodebyuri(@Context GraphDatabaseService gds, params.put("uri", uriParam); if (graphUriParam == null || graphUriParam.equals("")) { queryWithContext = "MATCH (x:Resource {uri:{uri}}) " + - "WHERE NOT EXISTS(x.graphUri)\n" + + "WHERE NOT exists(x.graphUri)\n" + "OPTIONAL MATCH (x)-[r]-(val:Resource) " + "WHERE exists(val.uri)\n" + - "AND NOT EXISTS(val.graphUri)\n" + + "AND NOT exists(val.graphUri)\n" + "RETURN x, r, val.uri AS value"; queryNoContext = "MATCH (x:Resource {uri:{uri}}) " + - "WHERE NOT EXISTS(x.graphUri)\n" + + "WHERE NOT exists(x.graphUri)\n" + "RETURN x, null AS r, null AS value"; } else { queryWithContext = "MATCH (x:Resource {uri:{uri}, graphUri:{graphUri}}) " + @@ -447,6 +494,14 @@ private String buildURI(String baseVocabNS, String name, Map nam } + /** + * This method constructs a literal with its data type IRI, if existent from a shortened data type + * IRI. + * + * @param literal a {@link String} containing the literal + * @param namespaces a {@link Map} containing the namespaces in the database + * @return literal + data type IRI if it exists, the initial literal value otherwise + */ private String buildCustomDTFromShortURI(String literal, Map namespaces) { Matcher matcher = customDataTypedLiteralShortenedURIPattern.matcher(literal); if (matcher.matches()) { @@ -813,6 +868,14 @@ private Literal createTypedLiteral(SimpleValueFactory valueFactory, Object value return result; } + /** + * Creates a literal, including language tag or data type IRI if existent. + * + * @param value literal with data type or language tag + * @param vf ValueFactory to create the literal + * @return created Literal, with a language tag or a custom data type or with neither, if not + * existent + */ private Literal getLiteralWithTagOrDTIfPresent(String value, ValueFactory vf) { Matcher langTag = langTagPattern.matcher(value); Matcher customDT = customDataTypePattern.matcher(value); diff --git a/src/test/java/semantics/extension/RDFEndpointTest.java b/src/test/java/semantics/extension/RDFEndpointTest.java index 72eb699d..3d84b5d5 100644 --- a/src/test/java/semantics/extension/RDFEndpointTest.java +++ b/src/test/java/semantics/extension/RDFEndpointTest.java @@ -1830,7 +1830,7 @@ public void testNodeByUriWithGraphUriOnQuadRDFTrig() throws Exception { + "describe/uri/" + URLEncoder .encode("http://www.example.org/exampleDocument#Monica", StandardCharsets.UTF_8.toString()) - + "?graphuri=http://www.example.org/exampleDocument%23G1"); + + "?graphUri=http://www.example.org/exampleDocument%23G1"); String expected = " {\n" + " \n" @@ -1884,7 +1884,7 @@ public void testNodeByUriWithGraphUriOnQuadRDFNQuads() throws Exception { + "describe/uri/" + URLEncoder .encode("http://www.example.org/exampleDocument#Monica", StandardCharsets.UTF_8.toString()) - + "?graphuri=http://www.example.org/exampleDocument%23G1"); + + "?graphUri=http://www.example.org/exampleDocument%23G1"); String expected = " \"Monica Murphy\" .\n" + " .\n"