Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions src/main/java/com/cefriel/template/io/rdf/RDFReader.java
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,59 @@ private List<Map<String,String>> getQueryResultsStringValue(String query) {
return new ArrayList<>(dataframe);
}

/**
* Executes a SPARQL query returning a list of rows as {@code List<Map<String, Map<String, String>>>}.
* Each element of the list is a row mapping every variable name to a metadata map describing the
* bound value. The metadata map mirrors the SPARQL JSON results structure and may contain the keys
* {@code value}, {@code type} , {@code datatype} and {@code lang}.
* If {@code hashVariable} is set, variable names are hashed; if {@code onlyDistinct} is set, duplicate rows are removed.
* @param query SPARQL query to be executed
* @return Result of the SPARQL query with per-binding metadata maps
*/
public List<Map<String, Map<String, String>>> getDataframeWithMetadata(String query) {
List<Map<String, Value>> valueResults = executeQuery(query);
Collection<Map<String, Map<String, String>>> dataframe = onlyDistinct ? new LinkedHashSet<>() : new ArrayList<>();
for (Map<String, Value> row : valueResults) {
Map<String, Map<String, String>> bindingRow = new HashMap<>(row.size());
for (var rowEntry : row.entrySet()) {
String sparqlBindingKey = hashVariable
? TemplateFunctions.literalHash(rowEntry.getKey())
: rowEntry.getKey();
bindingRow.put(sparqlBindingKey, bindingMetadata(rowEntry.getValue()));
}
dataframe.add(bindingRow);
}
return new ArrayList<>(dataframe);
}

/**
* Builds the metadata map for a single binding {@code value}, mirroring the SPARQL JSON results
* structure. For a missing (unbound) value an empty map is returned. Otherwise, it contains {@code value},
* {@code type} and, for literals, {@code datatype} and an optional {@code lang}.
* @param value binding value, may be {@code null}
* @return metadata map for the binding, empty if {@code value} is {@code null}
*/
private Map<String, String> bindingMetadata(Value value) {
Map<String, String> valueMap = new HashMap<>();
if (value == null)
return valueMap;
valueMap.put("value", value.stringValue());
if (value instanceof IRI) {
valueMap.put("type", "uri");
} else if (value instanceof BNode) {
valueMap.put("type", "bnode");
} else if (value instanceof Literal) {
valueMap.put("type", "literal");
Literal literal = (Literal) value;
literal.getLanguage().ifPresent(lang -> valueMap.put("lang", lang));
if (literal.getDatatype() != null)
valueMap.put("datatype", literal.getDatatype().stringValue());
}
return valueMap;
}



/**
* Executes a SPARQL query returning a list of rows as {@code List<Map<String,String>>}
* and logging ({@code INFO} level) the query, the duration and the number of rows returned.
Expand Down
98 changes: 98 additions & 0 deletions src/test/java/com/cefriel/template/RDFReaderTests.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,25 @@

import com.cefriel.template.io.rdf.RDFReader;
import com.cefriel.template.utils.TemplateFunctions;
import org.eclipse.rdf4j.model.Model;
import org.eclipse.rdf4j.model.impl.TreeModel;
import org.eclipse.rdf4j.model.util.Models;
import org.eclipse.rdf4j.repository.Repository;
import org.eclipse.rdf4j.repository.sail.SailRepository;
import org.eclipse.rdf4j.rio.RDFFormat;
import org.eclipse.rdf4j.rio.RDFParser;
import org.eclipse.rdf4j.rio.Rio;
import org.eclipse.rdf4j.rio.helpers.StatementCollector;
import org.eclipse.rdf4j.sail.memory.MemoryStore;
import org.junit.jupiter.api.Test;

import java.io.FileInputStream;
import java.io.InputStream;
import java.io.StringReader;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import java.util.Map;

public class RDFReaderTests {
Expand Down Expand Up @@ -117,4 +125,94 @@ public void agencyParametricStream() throws Exception {
}
// TODO Add tests for TemplateMap
// TODO Add tests for XMLReader

@Test
public void dataframeWithMetadata() throws Exception {
Repository repo = new SailRepository(new MemoryStore());
RDFReader reader = new RDFReader(repo);
reader.setBaseIRI("http://www.cefriel.com/data/");
reader.addFile(resolvePath("agency", "input.ttl"), RDFFormat.TURTLE);

String query = "PREFIX gtfs: <http://vocab.gtfs.org/terms#> " +
"PREFIX foaf: <http://xmlns.com/foaf/0.1/> " +
"PREFIX dct: <http://purl.org/dc/terms/> " +
"SELECT ?s ?name ?lang WHERE { " +
" ?s a gtfs:Agency ; foaf:name ?name ; dct:language ?lang . }";

List<Map<String, Map<String, String>>> rows = reader.getDataframeWithMetadata(query);

assert rows.size() == 2;
for (Map<String, Map<String, String>> row : rows) {
Map<String, String> s = row.get("s");
assert "uri".equals(s.get("type"));
assert s.get("value").startsWith("http://sprint-transport.eu/data/agencies/");
assert s.get("datatype") == null;

Map<String, String> name = row.get("name");
assert "literal".equals(name.get("type"));
assert name.get("value").endsWith("Agency");
assert "http://www.w3.org/2001/XMLSchema#string".equals(name.get("datatype"));

Map<String, String> lang = row.get("lang");
assert "en".equals(lang.get("value"));
}

reader.addString(
"@prefix base: <http://www.cefriel.com/data/> ." +
"@prefix xsd: <http://www.w3.org/2001/XMLSchema#> ." +
"base:event base:start \"2026-06-29T10:30:00Z\"^^xsd:dateTime ; " +
"base:count \"42\"^^xsd:integer ; " +
"base:label \"ciao\"@it .",
RDFFormat.TURTLE);

String typedQuery = "PREFIX base: <http://www.cefriel.com/data/> " +
"SELECT ?start ?count ?label WHERE { " +
" base:event base:start ?start ; base:count ?count ; base:label ?label . }";
List<Map<String, Map<String, String>>> typedRows = reader.getDataframeWithMetadata(typedQuery);

assert typedRows.size() == 1;
Map<String, Map<String, String>> typedRow = typedRows.get(0);

Map<String, String> start = typedRow.get("start");
assert "literal".equals(start.get("type"));
assert "2026-06-29T10:30:00Z".equals(start.get("value"));
assert "http://www.w3.org/2001/XMLSchema#dateTime".equals(start.get("datatype"));

Map<String, String> count = typedRow.get("count");
assert "42".equals(count.get("value"));
assert "http://www.w3.org/2001/XMLSchema#integer".equals(count.get("datatype"));

Map<String, String> label = typedRow.get("label");
assert "ciao".equals(label.get("value"));
assert "it".equals(label.get("lang"));

reader.shutDown();
}

@Test
public void dataframeWithMetadataMapping() throws Exception {
Repository repo = new SailRepository(new MemoryStore());
RDFReader reader = new RDFReader(repo);
reader.setBaseIRI("http://www.cefriel.com/data/");
String folder = "metadata";
reader.addFile(resolvePath(folder, "input.ttl"), RDFFormat.TURTLE);

TemplateExecutor executor = new TemplateExecutor(true, false, false, null);
Path template = Paths.get(resolvePath(folder, "template.vm"));
String result = executor.executeMapping(Map.of("reader", reader), template, new TemplateFunctions(), null);

String expectedOutput = Files.readString(Paths.get(resolvePath(folder, "output.ttl")));
Model resultModel = parseTurtle(result);
Model expectedModel = parseTurtle(expectedOutput);
assert Models.isomorphic(resultModel, expectedModel);
reader.shutDown();
}

private static Model parseTurtle(String rdf) throws Exception {
RDFParser parser = Rio.createParser(RDFFormat.TURTLE);
Model model = new TreeModel();
parser.setRDFHandler(new StatementCollector(model));
parser.parse(new StringReader(rdf), "http://example.com/base/");
return model;
}
}
15 changes: 15 additions & 0 deletions src/test/resources/metadata/input.ttl
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
@prefix base: <http://sprint-transport.eu/data/> .
@prefix foaf: <http://xmlns.com/foaf/0.1/> .
@prefix gtfs: <http://vocab.gtfs.org/terms#> .
@prefix dct: <http://purl.org/dc/terms/> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .

<http://sprint-transport.eu/data/agencies/BEST-AGENCY> a gtfs:Agency;
dct:language "en";
foaf:name "Best Agency" ;
rdfs:label "Migliore"@it .

<http://sprint-transport.eu/data/agencies/WOW-AGENCY> a gtfs:Agency;
dct:language "en";
foaf:name "Wow Agency" ;
rdfs:label "Stupenda"@it .
15 changes: 15 additions & 0 deletions src/test/resources/metadata/output.ttl
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
@prefix ex: <http://example.com/> .

<http://sprint-transport.eu/data/agencies/BEST-AGENCY>
ex:name "Best Agency" ;
ex:nameType "literal" ;
ex:nameDatatype "http://www.w3.org/2001/XMLSchema#string" ;
ex:label "Migliore" ;
ex:labelLang "it" .

<http://sprint-transport.eu/data/agencies/WOW-AGENCY>
ex:name "Wow Agency" ;
ex:nameType "literal" ;
ex:nameDatatype "http://www.w3.org/2001/XMLSchema#string" ;
ex:label "Stupenda" ;
ex:labelLang "it" .
10 changes: 10 additions & 0 deletions src/test/resources/metadata/template.vm
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
#set ( $h = $reader.setQueryHeader("
PREFIX foaf: <http://xmlns.com/foaf/0.1/>
PREFIX gtfs: <http://vocab.gtfs.org/terms#>
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
PREFIX dct: <http://purl.org/dc/terms/>
") )
#set ( $rows = $reader.getDataframeWithMetadata("SELECT ?s ?name ?label WHERE { ?s a gtfs:Agency ; foaf:name ?name ; rdfs:label ?label . }") )
@prefix ex: <http://example.com/> .
#foreach($row in $rows)<$row.s.value> ex:name "$row.name.value" ; ex:nameType "$row.name.type" ; ex:nameDatatype "$row.name.datatype" ; ex:label "$row.label.value" ; ex:labelLang "$row.label.lang" .
#end