Skip to content
6 changes: 5 additions & 1 deletion src/main/java/com/cefriel/template/Main.java
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
import javax.tools.ToolProvider;
import java.io.File;
import java.io.FileWriter;
import java.io.InputStream;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.Files;
Expand Down Expand Up @@ -181,7 +182,10 @@ public void exec() throws Exception {
}
}
if(compileRML) {
Util.validateRML(templatePath, verbose);
try (InputStream rmlMappingStream = Files.newInputStream(templatePath))
{
Util.validateRML(rmlMappingStream, verbose);
}

Reader compilerReader = TemplateFunctions.getRDFReaderFromFile(templatePath.toString());
Map<String, Reader> compilerReaderMap = new HashMap<>();
Expand Down
21 changes: 12 additions & 9 deletions src/main/java/com/cefriel/template/utils/RMLCompilerUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,17 @@ public boolean checkReference(String s){
return false;
}

public String getBaseIRI(String turtle) {
Pattern p = Pattern.compile("@base <([^<>]*)>");
Matcher m = p.matcher(turtle);

if (m.find()) {
return m.group(1);
} else {
return null;
}
}

/**
* From rmlmapper <a href="https://github.com/RMLio/rmlmapper-java/blob/6492743f9c81523b6e9142c929d3aaecc78d67eb/src/main/java/be/ugent/rml/Utils.java#L634">...</a>
*
Expand All @@ -223,15 +234,7 @@ public String getBaseIRI(Path rmlPath) {
} catch (IOException e) {
turtle = "";
}

Pattern p = Pattern.compile("@base <([^<>]*)>");
Matcher m = p.matcher(turtle);

if (m.find()) {
return m.group(1);
} else {
return null;
}
return getBaseIRI(turtle);
}

}
Expand Down
86 changes: 65 additions & 21 deletions src/main/java/com/cefriel/template/utils/Util.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

package com.cefriel.template.utils;

import com.cefriel.template.TemplateExecutor;
import com.cefriel.template.TemplateMap;
import com.cefriel.template.io.Formatter;
import com.cefriel.template.io.Reader;
Expand All @@ -30,9 +31,11 @@
import org.apache.velocity.VelocityContext;
import org.apache.velocity.app.VelocityEngine;
import org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader;
import org.apache.velocity.runtime.resource.loader.FileResourceLoader;
import org.apache.velocity.tools.generic.*;
import org.eclipse.rdf4j.common.exception.ValidationException;
import org.eclipse.rdf4j.model.Model;
import org.eclipse.rdf4j.model.base.CoreDatatype;
Copy link

Copilot AI Oct 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This import appears to be unused. Consider removing it to keep the imports clean.

Suggested change
import org.eclipse.rdf4j.model.base.CoreDatatype;

Copilot uses AI. Check for mistakes.
import org.eclipse.rdf4j.model.vocabulary.RDF4J;
import org.eclipse.rdf4j.model.vocabulary.SHACL;
import org.eclipse.rdf4j.repository.RepositoryConnection;
Expand All @@ -45,6 +48,7 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
Expand Down Expand Up @@ -195,11 +199,16 @@ public static VelocityEngine createVelocityEngine(boolean templateInResources, b
velocityEngine.setProperty("resource.loaders", "class");
velocityEngine.setProperty("resource.loader.class.class",
ClasspathResourceLoader.class.getName());
} else{
velocityEngine.setProperty("resource.loaders", "file");
velocityEngine.setProperty("resource.loader.file.class", FileResourceLoader.class.getName());
velocityEngine.setProperty("resource.loader.file.path", "");
}
velocityEngine.init();
return velocityEngine;
}
public static void validateRML(Path templatePath, boolean verbose) {

public static void validateRML(InputStream rmlMapping, boolean verbose) {

ShaclSail shaclSail = new ShaclSail(new MemoryStore());
SailRepository repository = new SailRepository(shaclSail);
Expand All @@ -208,33 +217,25 @@ public static void validateRML(Path templatePath, boolean verbose) {
try (RepositoryConnection connection = repository.getConnection()) {
try (InputStream shapesStream = Util.class.getResourceAsStream("/rml/core.ttl")) {
Model rules = Rio.parse(shapesStream, RDFFormat.TURTLE);
// cf. https://github.com/eclipse-rdf4j/rdf4j/discussions/4287
rules.remove(null, SHACL.NAME, null);
rules.remove(null, SHACL.DESCRIPTION, null);

connection.begin();
connection.add(rules, RDF4J.SHACL_SHAPE_GRAPH);
connection.commit();
}

// Load RML
try (InputStream dataStream = Files.newInputStream(templatePath)) {
connection.begin();
connection.add(dataStream, "", org.eclipse.rdf4j.rio.RDFFormat.TURTLE);

try {
connection.commit();
log.info("RML validated correctly");
} catch (RepositoryException exception) {
Throwable cause = exception.getCause();
log.error("RML not valid");
if (verbose)
if (cause instanceof ValidationException) {
Model validationReportModel = ((ValidationException) cause).validationReportAsModel();
Rio.write(validationReportModel, System.out, RDFFormat.TURTLE);
}
throw exception;
connection.begin();
connection.add(rmlMapping, "", RDFFormat.TURTLE);
try {
connection.commit();
log.info("RML validated correctly");
} catch (RepositoryException exception) {
Throwable cause = exception.getCause();
log.error("RML not valid");
if (verbose && cause instanceof ValidationException) {
Model validationReportModel = ((ValidationException) cause).validationReportAsModel();
Rio.write(validationReportModel, System.out, RDFFormat.TURTLE);
}
throw exception;
}
} catch (Exception e) {
throw new RuntimeException(e);
Expand All @@ -243,6 +244,7 @@ public static void validateRML(Path templatePath, boolean verbose) {
}
}


/**
* Creates a new {@link VelocityContext} and populates it with the provided readers, template map, and template functions.
* Additionally, it adds a set of common tools for mathematical operations, number formatting, date manipulation,
Expand Down Expand Up @@ -331,4 +333,46 @@ public static VelocityContext createVelocityContext(Reader reader, TemplateMap t
public static VelocityContext createVelocityContext(Map<String, Reader> readers, TemplateMap templateMap) {
return createVelocityContext(readers, templateMap, new TemplateFunctions());
}

/**
* Compiles an MTL mapping from the given RML file.
*
* @param mappingRML Path to the RML mapping file.
* @param baseIri Base IRI to use if not found in the mapping.
* @param basePath Base path for output files.
* @param trimTemplate Whether to use the trimmed template.
* @param verbose Enable verbose validation output.
* @return Path to the compiled template.
* @throws Exception if validation or compilation fails.
*/
public static Path compiledMTLMapping(InputStream mappingRML, String baseIri, Path basePath, boolean trimTemplate, boolean verbose) throws Exception {
Comment on lines +346 to +348
Copy link

Copilot AI Oct 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method throws a generic Exception. Consider documenting the specific exceptions that can be thrown or using more specific exception types in the throws clause.

Suggested change
* @throws Exception if validation or compilation fails.
*/
public static Path compiledMTLMapping(InputStream mappingRML, String baseIri, Path basePath, boolean trimTemplate, boolean verbose) throws Exception {
* @throws IOException if an I/O error occurs during reading or writing files.
* @throws ValidationException if RML validation fails.
* @throws RepositoryException if an RDF repository error occurs.
*/
public static Path compiledMTLMapping(InputStream mappingRML, String baseIri, Path basePath, boolean trimTemplate, boolean verbose)
throws IOException, ValidationException, RepositoryException {

Copilot uses AI. Check for mistakes.
String mappingRMLString = inputStreamToString(mappingRML);
Util.validateRML(new ByteArrayInputStream(mappingRMLString.getBytes(StandardCharsets.UTF_8)), verbose);
Reader compilerReader = TemplateFunctions.getRDFReaderFromString(mappingRMLString, RDFFormat.TURTLE.toString());
Map<String, Reader> compilerReaderMap = new HashMap<>();
compilerReaderMap.put("reader", compilerReader);

try (InputStream rmlCompiler = trimTemplate
? Util.class.getResourceAsStream("/rml/rml-compiler.vm.tmp.vm")
: Util.class.getResourceAsStream("/rml/rml-compiler.vm")) {

RMLCompilerUtils rmlCompilerUtils = new RMLCompilerUtils();

Map<String, String> rmlMap = new HashMap<>();
String baseIriRML = rmlCompilerUtils.getBaseIRI(mappingRMLString);
baseIriRML = baseIriRML != null ? baseIriRML : baseIri;
rmlMap.put("baseIRI", baseIriRML);
rmlMap.put("basePath", basePath.toString() + "/");

Path compiledTemplatePath = Paths.get(basePath.toString(), "template.rml.vm");
TemplateExecutor templateExecutor = new TemplateExecutor(false, false, true, null);
return templateExecutor.executeMapping(
compilerReaderMap,
rmlCompiler,
compiledTemplatePath,
rmlCompilerUtils,
new TemplateMap(rmlMap)
);
}
}
}
51 changes: 23 additions & 28 deletions src/test/java/com/cefriel/template/RMLTests.java
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
import org.eclipse.rdf4j.rio.helpers.StatementCollector;
import org.junit.jupiter.api.Test;

import java.io.InputStream;
import java.io.StringReader;
import java.nio.file.Files;
import java.nio.file.Path;
Expand All @@ -40,54 +41,48 @@

public class RMLTests {

final static String FOLDER = "src/test/resources/rml/";
private String resolvePath(String folder, String file) {
return FOLDER + file;
}
final static Path FOLDER = Path.of("src/test/resources/rml/");

@Test
public void invalidRMLTest() throws Exception {
String folder = "rml";
Path rmlMappings = Paths.get(resolvePath(folder, "invalid-mapping.ttl"));
assertThrows(RuntimeException.class, () -> Util.validateRML(rmlMappings, false));
Path rmlMappings = FOLDER.resolve(Path.of("invalid-mapping.ttl"));
try (InputStream rmlMapping = Files.newInputStream(rmlMappings)) {
assertThrows(RuntimeException.class,
() -> Util.validateRML(rmlMapping, false));
}
}

@Test
public void validRMLTest() throws Exception {
Path rmlMapping = FOLDER.resolve(Path.of("mapping.ttl"));
String expectedOutput = Files.readString(FOLDER.resolve("output.nq"));

String folder = "rml";
String rmlMappings = resolvePath(folder, "mapping.ttl");

String expectedOutput = Files.readString(Paths.get(resolvePath(folder, "output.nq")));
try (InputStream rmlMappingStream = Files.newInputStream(rmlMapping)) {
Util.validateRML(rmlMappingStream, false);
}

Util.validateRML(Paths.get(rmlMappings), false);

Reader compilerReader = TemplateFunctions.getRDFReaderFromFile(rmlMappings);
Path rmlCompiler = Paths.get("rml/rml-compiler.vm");
Path compiledTemplatePath = Paths.get(resolvePath(folder,"template.rml.vm"));
Reader compilerReader = TemplateFunctions.getRDFReaderFromFile(rmlMapping.toString());

Map<String,String> rmlMap = new HashMap<>();
rmlMap.put("basePath", FOLDER);
rmlMap.put("basePath", null);
Copy link

Copilot AI Oct 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Setting basePath to null might cause issues downstream. Consider using an empty string or a default value instead of null.

Suggested change
rmlMap.put("basePath", null);
rmlMap.put("basePath", "");

Copilot uses AI. Check for mistakes.

TemplateExecutor rmlTemplateExecutor = new TemplateExecutor(false, false, true, null);
TemplateExecutor templateExecutor = new TemplateExecutor(false, false, false, null);
rmlTemplateExecutor.executeMapping(Map.of("reader", compilerReader), rmlCompiler, compiledTemplatePath, new RMLCompilerUtils(), new TemplateMap(rmlMap));
String result = templateExecutor.executeMapping(Map.of("reader", compilerReader), compiledTemplatePath);
Files.delete(compiledTemplatePath);

Model resultModel = parseRDFString(result);
Model expectedOutputModel = parseRDFString(expectedOutput);

assert(Models.isomorphic(resultModel, expectedOutputModel));
try (InputStream rmlMappingStream = Files.newInputStream(rmlMapping)) {
Path mappingMTL = Util.compiledMTLMapping(rmlMappingStream, null, FOLDER, false, false);
String result = templateExecutor.executeMapping(Map.of("reader", compilerReader), FOLDER.resolve(mappingMTL.getFileName()));
Files.delete(mappingMTL);

Model resultModel = parseRDFString(result);
Model expectedOutputModel = parseRDFString(expectedOutput);
assert(Models.isomorphic(resultModel, expectedOutputModel));
}
}

private static Model parseRDFString(String rdfString) throws Exception {
RDFParser rdfParser = Rio.createParser(RDFFormat.NQUADS);

Model model = new TreeModel();
rdfParser.setRDFHandler(new StatementCollector(model));
rdfParser.parse(new StringReader(rdfString), "http://example.com/base/");
return model;
}

}