diff --git a/.development/esmf-checkstyle.xml b/.development/esmf-checkstyle.xml
index f927584fc..71838114b 100644
--- a/.development/esmf-checkstyle.xml
+++ b/.development/esmf-checkstyle.xml
@@ -191,7 +191,7 @@
value="Local variable name ''{0}'' must match pattern ''{1}''."/>
-
+
diff --git a/.github/workflows/pull-request-check.yml b/.github/workflows/pull-request-check.yml
index 7a5a12102..e7e008636 100644
--- a/.github/workflows/pull-request-check.yml
+++ b/.github/workflows/pull-request-check.yml
@@ -124,7 +124,7 @@ jobs:
cp tools/samm-cli/scripts/windows/run.bat ./${bundle}/
curl -Lo warp-packer.exe https://github.com/kirbylink/warp/releases/download/v1.3.0/windows-x64.warp-packer.exe
- if [ "$(sha256sum warp-packer | cut -d' ' -f1)" != "483726e0eb10a43167c03c8e4f27a2901741affd245e5ec8ed45509f9dcd7a8a" ]; then
+ if [ "$(sha256sum warp-packer.exe | cut -d' ' -f1)" != "483726e0eb10a43167c03c8e4f27a2901741affd245e5ec8ed45509f9dcd7a8a" ]; then
echo "Warp packer checksum does not match"
exit 1
fi
@@ -145,7 +145,7 @@ jobs:
chmod +x ./${bundle}/run.sh
curl -Lo warp-packer https://github.com/kirbylink/warp/releases/download/v1.3.0/macos-x64.warp-packer
- if [ "$(sha256sum warp-packer | cut -d' ' -f1)" != "311bd238d087ea92f486d3d754a0f11826d9bd49d6f8ae849e5d6f6089a2e9c9" ]; then
+ if [ "$(shasum -a 256 warp-packer | cut -d' ' -f1)" != "311bd238d087ea92f486d3d754a0f11826d9bd49d6f8ae849e5d6f6089a2e9c9" ]; then
echo "Warp packer checksum does not match"
exit 1
fi
diff --git a/core/esmf-aspect-meta-model-interface/src/main/java/org/eclipse/esmf/aspectmodel/AspectLoadingException.java b/core/esmf-aspect-meta-model-interface/src/main/java/org/eclipse/esmf/aspectmodel/AspectLoadingException.java
index c4b8c7956..a9dc1f037 100644
--- a/core/esmf-aspect-meta-model-interface/src/main/java/org/eclipse/esmf/aspectmodel/AspectLoadingException.java
+++ b/core/esmf-aspect-meta-model-interface/src/main/java/org/eclipse/esmf/aspectmodel/AspectLoadingException.java
@@ -13,18 +13,35 @@
package org.eclipse.esmf.aspectmodel;
+import org.apache.jena.rdf.model.RDFNode;
+import org.jspecify.annotations.Nullable;
+
public class AspectLoadingException extends RuntimeException {
private static final long serialVersionUID = 7687644022103150329L;
+ private final @Nullable RDFNode highlightElement;
+
public AspectLoadingException( final Throwable cause ) {
super( cause );
+ highlightElement = null;
}
public AspectLoadingException( final String message ) {
super( message );
+ highlightElement = null;
}
public AspectLoadingException( final String message, final Throwable cause ) {
super( message, cause );
+ highlightElement = null;
+ }
+
+ public AspectLoadingException( final String message, final RDFNode highlightElement ) {
+ super( message );
+ this.highlightElement = highlightElement;
+ }
+
+ public @Nullable RDFNode highlightElement() {
+ return highlightElement;
}
}
diff --git a/core/esmf-aspect-meta-model-java/pom.xml b/core/esmf-aspect-meta-model-java/pom.xml
index a0a23969d..2ee6a02b7 100644
--- a/core/esmf-aspect-meta-model-java/pom.xml
+++ b/core/esmf-aspect-meta-model-java/pom.xml
@@ -34,6 +34,10 @@
+
+ org.eclipse.esmf
+ esmf-util
+
org.eclipse.esmf
esmf-semantic-aspect-meta-model
@@ -42,6 +46,10 @@
org.eclipse.esmf
esmf-aspect-meta-model-interface
+
+ org.eclipse.esmf
+ esmf-tree-sitter-turtle
+
org.apache.commons
commons-text
diff --git a/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/loader/AspectModelLoader.java b/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/loader/AspectModelLoader.java
index 8f90b07d9..909fa5a1c 100644
--- a/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/loader/AspectModelLoader.java
+++ b/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/loader/AspectModelLoader.java
@@ -38,6 +38,15 @@
import java.util.stream.Collectors;
import java.util.stream.Stream;
+import org.apache.jena.rdf.model.Model;
+import org.apache.jena.rdf.model.ModelFactory;
+import org.apache.jena.rdf.model.RDFNode;
+import org.apache.jena.rdf.model.Resource;
+import org.apache.jena.rdf.model.Statement;
+import org.apache.jena.rdf.model.StmtIterator;
+import org.apache.jena.vocabulary.RDF;
+import org.apache.jena.vocabulary.XSD;
+
import org.eclipse.esmf.aspectmodel.AspectLoadingException;
import org.eclipse.esmf.aspectmodel.AspectModelFile;
import org.eclipse.esmf.aspectmodel.RdfUtil;
@@ -68,19 +77,13 @@
import org.eclipse.esmf.metamodel.impl.DefaultNamespace;
import org.eclipse.esmf.metamodel.vocabulary.SammNs;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
import com.google.common.collect.Streams;
+
import io.vavr.control.Either;
import io.vavr.control.Try;
-import org.apache.jena.rdf.model.Model;
-import org.apache.jena.rdf.model.ModelFactory;
-import org.apache.jena.rdf.model.RDFNode;
-import org.apache.jena.rdf.model.Resource;
-import org.apache.jena.rdf.model.Statement;
-import org.apache.jena.rdf.model.StmtIterator;
-import org.apache.jena.vocabulary.RDF;
-import org.apache.jena.vocabulary.XSD;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
/**
* The core class to load an {@link AspectModel}. The AspectModelLoader is also a
@@ -339,6 +342,23 @@ public AspectModel loadUrns( final Collection urns ) {
return loadAspectModelFiles( loaderContext.loadedFiles() );
}
+ /**
+ * Load an Aspect Model from a String containing the RDF/Turtle represenatation and set the srouce
+ * location
+ * for this input
+ *
+ * @param turtleRepresentation the document as RDF/Turtle
+ * @param sourceLocation the source location for the model
+ * @return the Aspect Model
+ */
+ public AspectModel load( final String turtleRepresentation, final URI sourceLocation ) {
+ final AspectModelFile rawFile = AspectModelFileLoader.load( turtleRepresentation, sourceLocation );
+ final AspectModelFile migratedModel = migrate( rawFile );
+ final LoaderContext loaderContext = new LoaderContext();
+ resolve( List.of( migratedModel ), loaderContext );
+ return loadAspectModelFiles( loaderContext.loadedFiles() );
+ }
+
/**
* Load an Aspect Model (.ttl) from an input stream and set the source location for this input. For
* loading an Aspect Model Namespace Package (.zip), use
@@ -640,7 +660,11 @@ public AspectModel loadAspectModelFiles( final Collection input
.filter( statement -> !statement.getObject().isURIResource() || !statement.getResource().equals( SammNs.SAMM.Namespace() ) )
.map( Statement::getSubject )
.filter( RDFNode::isURIResource )
- .map( resource -> mergedModel.createResource( resource.getURI() ) )
+ .map( resource -> {
+ final Resource newResource = mergedModel.createResource( resource.getURI() );
+ TokenRegistry.updateNode( resource.asNode(), newResource.asNode() );
+ return newResource;
+ } )
.map( resource -> modelElementFactory.create( ModelElement.class, resource ) )
.toList();
aspectModelFile.setElements( fileElements );
diff --git a/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/loader/Instantiator.java b/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/loader/Instantiator.java
index c9c151551..a8aed45b1 100644
--- a/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/loader/Instantiator.java
+++ b/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/loader/Instantiator.java
@@ -23,6 +23,14 @@
import java.util.stream.Collectors;
import java.util.stream.Stream;
+import org.apache.jena.rdf.model.Literal;
+import org.apache.jena.rdf.model.Model;
+import org.apache.jena.rdf.model.RDFList;
+import org.apache.jena.rdf.model.RDFNode;
+import org.apache.jena.rdf.model.Resource;
+import org.apache.jena.rdf.model.Statement;
+import org.apache.jena.vocabulary.RDF;
+
import org.eclipse.esmf.aspectmodel.AspectLoadingException;
import org.eclipse.esmf.aspectmodel.RdfUtil;
import org.eclipse.esmf.aspectmodel.urn.AspectModelUrn;
@@ -44,14 +52,6 @@
import org.eclipse.esmf.metamodel.vocabulary.SAMM;
import org.eclipse.esmf.metamodel.vocabulary.SammNs;
-import org.apache.jena.rdf.model.Literal;
-import org.apache.jena.rdf.model.Model;
-import org.apache.jena.rdf.model.RDFList;
-import org.apache.jena.rdf.model.RDFNode;
-import org.apache.jena.rdf.model.Resource;
-import org.apache.jena.rdf.model.Statement;
-import org.apache.jena.vocabulary.RDF;
-
public abstract class Instantiator extends AttributeValueRetriever implements Function {
protected final ModelElementFactory modelElementFactory;
protected Class targetClass;
@@ -128,7 +128,8 @@ private Statement getDataType( final Resource resource ) {
final Statement dataType = resource.getProperty( SammNs.SAMM.dataType() );
if ( dataType == null ) {
throw new AspectLoadingException(
- String.format( "No datatype is defined on the Characteristic instance '%s: '.", resource.getLocalName() ) );
+ String.format( "No datatype is defined on the Characteristic instance '%s'.", resource.getLocalName() ),
+ resource );
}
return dataType;
} );
@@ -157,7 +158,7 @@ protected Value buildValue( final RDFNode node, final Optional charact
if ( node.isLiteral() ) {
final Literal literal = node.asLiteral();
return valueInstantiator.buildScalarValue( literal.getLexicalForm(), literal.getLanguage(), literal.getDatatypeURI() )
- .orElseThrow( () -> new AspectLoadingException( "Literal can not be parsed: " + literal ) );
+ .orElseThrow( () -> new AspectLoadingException( "Literal can not be parsed: " + literal, literal ) );
}
if ( node.isResource() ) {
@@ -166,7 +167,7 @@ protected Value buildValue( final RDFNode node, final Optional charact
final Optional valueOpt = optionalAttributeValue( resource, SammNs.SAMM.value() ).map( Statement::getString );
if ( valueOpt.isEmpty() ) {
- throw new AspectLoadingException( "samm:Value must contain a samm:value property" );
+ throw new AspectLoadingException( "samm:Value must contain a samm:value property", resource );
}
return new DefaultScalarValue( buildBaseAttributes( resource ), valueOpt.get(), new DefaultScalar( type.getUrn() ) );
@@ -196,7 +197,7 @@ protected Value buildValue( final RDFNode node, final Optional charact
// This could happen if an entity instance should be constructed for an AbstractEntity type
if ( !type.is( Entity.class ) ) {
- throw new AspectLoadingException( "Expected type of value " + node + " to be samm:Entity, but it is not" );
+ throw new AspectLoadingException( "Expected type of value " + node + " to be samm:Entity, but it is not", node );
}
// Entities
@@ -221,11 +222,12 @@ protected EntityInstance buildEntityInstance( final Resource entityInstance, fin
if ( property.isOptional() ) {
return;
}
- throw new AspectLoadingException( "Mandatory Property " + property + " not found in Entity instance " + entityInstance );
+ throw new AspectLoadingException( "Mandatory Property " + property + " not found in Entity instance " + entityInstance,
+ entityInstance );
}
final RDFNode rdfValue = entityInstance.getProperty( rdfProperty ).getObject();
final Type propertyType = property.getDataType()
- .orElseThrow( () -> new AspectLoadingException( "Invalid Property without a dataType found" ) );
+ .orElseThrow( () -> new AspectLoadingException( "Invalid Property without a dataType found", entityInstance ) );
final Resource characteristic = attributeValue( rdfProperty, SammNs.SAMM.characteristic() ).getResource();
final Value value = buildValue( rdfValue, Optional.of( characteristic ), propertyType );
assertions.put( property, value );
diff --git a/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/loader/ModelElementFactory.java b/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/loader/ModelElementFactory.java
index 075bc3291..2c093f00d 100644
--- a/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/loader/ModelElementFactory.java
+++ b/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/loader/ModelElementFactory.java
@@ -26,6 +26,15 @@
import java.util.stream.Collectors;
import java.util.stream.Stream;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.jena.rdf.model.Model;
+import org.apache.jena.rdf.model.RDFNode;
+import org.apache.jena.rdf.model.Resource;
+import org.apache.jena.rdf.model.Statement;
+import org.apache.jena.rdf.model.StmtIterator;
+import org.apache.jena.vocabulary.RDF;
+import org.apache.jena.vocabulary.RDFS;
+
import org.eclipse.esmf.aspectmodel.AspectLoadingException;
import org.eclipse.esmf.aspectmodel.AspectModelFile;
import org.eclipse.esmf.aspectmodel.loader.instantiator.AbstractEntityInstantiator;
@@ -77,14 +86,6 @@
import org.eclipse.esmf.metamodel.vocabulary.SammNs;
import com.google.common.collect.Streams;
-import org.apache.commons.lang3.StringUtils;
-import org.apache.jena.rdf.model.Model;
-import org.apache.jena.rdf.model.RDFNode;
-import org.apache.jena.rdf.model.Resource;
-import org.apache.jena.rdf.model.Statement;
-import org.apache.jena.rdf.model.StmtIterator;
-import org.apache.jena.vocabulary.RDF;
-import org.apache.jena.vocabulary.RDFS;
/**
* Used as part of the loading process in the {@link AspectModelLoader}, it creates instance for the
@@ -170,11 +171,12 @@ public T create( final Class clazz, final Resource m
// No generic instantiator could be found. This means the element is an entity instance
if ( !model.contains( targetType, RDF.type, (RDFNode) null ) ) {
- throw new AspectLoadingException( "Could not load " + modelElement + ": Unknown type " + targetType );
+ throw new AspectLoadingException( "Could not load " + modelElement + ": Unknown type " + targetType, modelElement );
}
final Entity entity = create( Entity.class, targetType );
if ( entity == null ) {
- throw new AspectLoadingException( "Could not load " + modelElement + ": Expected " + targetType + " to be an Entity" );
+ throw new AspectLoadingException( "Could not load " + modelElement + ": Expected " + targetType + " to be an Entity",
+ modelElement );
}
return (T) new EntityInstanceInstantiator( this, entity ).apply( modelElement );
}
@@ -190,7 +192,7 @@ public Unit findOrCreateUnit( final Resource unitResource ) {
if ( SammNs.UNIT.getNamespace().equals( unitResource.getNameSpace() ) ) {
final AspectModelUrn unitUrn = AspectModelUrn.fromUrn( unitResource.getURI() );
return Units.fromName( unitUrn.getName() )
- .orElseThrow( () -> new AspectLoadingException( "Unit definition for " + unitUrn + " is invalid" ) );
+ .orElseThrow( () -> new AspectLoadingException( "Unit definition for " + unitUrn + " is invalid", unitResource ) );
}
final Set quantityKinds = Streams.stream(
@@ -220,7 +222,7 @@ private Resource resourceType( final Resource resource ) {
.filter( Optional::isPresent )
.map( Optional::get )
.findFirst()
- .orElseThrow( () -> new AspectLoadingException( "Resource " + resource + " has no type" ) );
+ .orElseThrow( () -> new AspectLoadingException( "Resource " + resource + " has no type", resource ) );
}
protected Model getModel() {
@@ -301,7 +303,8 @@ private static List getSeeValues( final Resource resource, final Attribu
private static String getSyntheticName( final Resource modelElement ) {
final Resource namedParent = getNamedParent( modelElement, modelElement.getModel() );
if ( namedParent == null ) {
- throw new AspectLoadingException( "At least one anonymous node in the model does not have a parent with a regular name." );
+ throw new AspectLoadingException( "At least one anonymous node in the model does not have a parent with a regular name.",
+ modelElement );
}
final String parentModelElementUri = namedParent.getURI();
final String parentModelElementName = AspectModelUrn.from( parentModelElementUri )
@@ -365,7 +368,7 @@ private static Resource getModelElementType( final Resource modelElement ) {
// This model element has no type, but maybe it extends another element
final Statement extendsStatement = modelElement.getProperty( SammNs.SAMM._extends() );
if ( extendsStatement == null ) {
- throw new AspectLoadingException( "Model element has no type and does not extend another type: " + modelElement );
+ throw new AspectLoadingException( "Model element has no type and does not extend another type: " + modelElement, modelElement );
}
final Resource superElement = extendsStatement.getObject().asResource();
diff --git a/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/loader/instantiator/PropertyInstantiator.java b/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/loader/instantiator/PropertyInstantiator.java
index 192d60106..be9087cd7 100644
--- a/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/loader/instantiator/PropertyInstantiator.java
+++ b/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/loader/instantiator/PropertyInstantiator.java
@@ -98,7 +98,7 @@ private ScalarValue buildScalarValue( final RDFNode node, final Type expectedTyp
if ( node.isLiteral() ) {
final Literal literal = node.asLiteral();
return valueInstantiator.buildScalarValue( literal.getLexicalForm(), literal.getLanguage(), literal.getDatatypeURI() )
- .orElseThrow( () -> new AspectLoadingException( "Literal cannot be parsed: " + literal ) );
+ .orElseThrow( () -> new AspectLoadingException( "Literal cannot be parsed: " + literal, literal ) );
}
if ( node.isResource() ) {
@@ -108,7 +108,7 @@ private ScalarValue buildScalarValue( final RDFNode node, final Type expectedTyp
final Optional valueOpt = optionalAttributeValue( resource, SammNs.SAMM.value() ).map( Statement::getString );
if ( valueOpt.isEmpty() ) {
- throw new AspectLoadingException( "samm:Value must contain a samm:value property" );
+ throw new AspectLoadingException( "samm:Value must contain a samm:value property", resource );
}
return new DefaultScalarValue( buildBaseAttributes( resource ), valueOpt.get(), new DefaultScalar( expectedType.toString() ) );
@@ -117,6 +117,6 @@ private ScalarValue buildScalarValue( final RDFNode node, final Type expectedTyp
return new DefaultScalarValue( buildBaseAttributes( resource ), resource.getURI(), new DefaultScalar( expectedType.toString() ) );
}
- throw new AspectLoadingException( "Unexpected RDF node type: " + node );
+ throw new AspectLoadingException( "Unexpected RDF node type: " + node, node );
}
}
diff --git a/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/resolver/AspectModelFileLoader.java b/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/resolver/AspectModelFileLoader.java
index 4533350bb..f63aa40f9 100644
--- a/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/resolver/AspectModelFileLoader.java
+++ b/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/resolver/AspectModelFileLoader.java
@@ -32,15 +32,19 @@
import java.util.List;
import java.util.Optional;
+import org.apache.jena.rdf.model.Model;
+
import org.eclipse.esmf.aspectmodel.RdfUtil;
import org.eclipse.esmf.aspectmodel.loader.AspectModelLoader;
import org.eclipse.esmf.aspectmodel.resolver.exceptions.ModelResolutionException;
import org.eclipse.esmf.aspectmodel.resolver.exceptions.ParserException;
import org.eclipse.esmf.aspectmodel.resolver.modelfile.RawAspectModelFile;
import org.eclipse.esmf.aspectmodel.resolver.services.TurtleLoader;
+import org.eclipse.esmf.treesitterturtle.ParserTokenType;
+import org.eclipse.esmf.treesitterturtle.TurtleSyntaxTree;
+import org.eclipse.esmf.util.download.Download;
import io.vavr.control.Try;
-import org.apache.jena.rdf.model.Model;
/**
* Loads an input source into a {@link RawAspectModelFile}, i.e., an Aspect Model file that does not
@@ -89,6 +93,18 @@ public static RawAspectModelFile load( final String rdfTurtle, final URI sourceL
return new RawAspectModelFile( rdfTurtle, model, headerComment, Optional.of( sourceLocation ) );
}
+ public static RawAspectModelFile load( final TurtleSyntaxTree syntaxTree, final URI sourceLocation ) {
+ final String sourceRepresentation = syntaxTree.sourceRepresentationSupplier().get();
+ final List headerComment = headerComment( syntaxTree );
+ final Try tryModel = TurtleLoader.loadTurtle( syntaxTree, sourceLocation );
+ if ( tryModel.isFailure() && tryModel.getCause() instanceof final ParserException parserException ) {
+ throw parserException;
+ }
+ final Model model = tryModel.getOrElseThrow(
+ () -> new ModelResolutionException( "Can not load model", tryModel.getCause() ) );
+ return new RawAspectModelFile( sourceRepresentation, model, headerComment, Optional.of( sourceLocation ) );
+ }
+
/**
* Loads the content of an AspectModelFile from an input stream
*
@@ -104,7 +120,7 @@ public static RawAspectModelFile load( final InputStream inputStream, final URI
* Loads the content of an AspectModelFile from an RDF model
*
* @param model the input model
- * @param sourceLocation the logical location of the file file source
+ * @param sourceLocation the logical location of the file source
* @return the loaded file content
*/
public static RawAspectModelFile load( final Model model, final URI sourceLocation ) {
@@ -191,6 +207,16 @@ private static List headerComment( final String content ) {
.toList();
}
+ private static List headerComment( final TurtleSyntaxTree turtleSyntaxTree ) {
+ return turtleSyntaxTree.tokens()
+ .filter( token -> !token.type().equals( ParserTokenType.DOCUMENT ) )
+ .takeWhile( token -> token.type().equals( ParserTokenType.COMMENT ) )
+ .map( TurtleSyntaxTree.Token::content )
+ .dropWhile( String::isBlank )
+ .map( line -> line.substring( 1 ).trim() )
+ .toList();
+ }
+
private static URI buildArtificialUri( final Object object, final String objectType ) {
return URI.create( "inmemory:%s:%s".formatted( objectType, object.hashCode() ) );
}
diff --git a/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/resolver/CommandExecutor.java b/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/resolver/CommandExecutor.java
index 848bf869a..076716662 100644
--- a/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/resolver/CommandExecutor.java
+++ b/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/resolver/CommandExecutor.java
@@ -18,10 +18,10 @@
import java.util.List;
import java.util.Optional;
-import org.eclipse.esmf.aspectmodel.resolver.exceptions.ProcessExecutionException;
-import org.eclipse.esmf.aspectmodel.resolver.process.BinaryLauncher;
-import org.eclipse.esmf.aspectmodel.resolver.process.ExecutableJarLauncher;
-import org.eclipse.esmf.aspectmodel.resolver.process.ProcessLauncher;
+import org.eclipse.esmf.util.process.ProcessExecutionException;
+import org.eclipse.esmf.util.process.BinaryLauncher;
+import org.eclipse.esmf.util.process.ExecutableJarLauncher;
+import org.eclipse.esmf.util.process.ProcessLauncher;
/**
* Executes an external resolver via the underlying OS command and returns the stdout from the
diff --git a/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/resolver/parser/ReaderRiotTurtle.java b/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/resolver/parser/ReaderRiotTurtle.java
index aa0abe41e..c46f1472b 100644
--- a/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/resolver/parser/ReaderRiotTurtle.java
+++ b/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/resolver/parser/ReaderRiotTurtle.java
@@ -26,28 +26,36 @@
import org.apache.jena.riot.tokens.TokenizerText;
import org.apache.jena.sparql.util.Context;
+import org.eclipse.esmf.aspectmodel.resolver.services.TurtleLoader;
+import org.eclipse.esmf.treesitterturtle.TurtleSyntaxTree;
+
public class ReaderRiotTurtle implements ReaderRIOT {
public static ReaderRIOTFactory factory = ReaderRiotTurtle::new;
-
- private final Lang lang;
private final ParserProfile parserProfile;
ReaderRiotTurtle( final Lang lang, final ParserProfile parserProfile ) {
- this.lang = lang;
- this.parserProfile = new TurtleParserProfile( parserProfile );
+ this.parserProfile = parserProfile;
}
@Override
public void read( final InputStream in, final String baseUri, final ContentType ct, final StreamRDF output, final Context context ) {
- final TurtleTokenizer tokenizer = new TurtleTokenizer( in, parserProfile.getErrorHandler() );
- final TurtleParser parser = TurtleParser.create( tokenizer, parserProfile, output );
+ final TurtleSyntaxTree syntaxTree = context != null && context.get( TurtleLoader.TREE_SITTER_SYNTAX_TREE ) != null
+ ? context.get( TurtleLoader.TREE_SITTER_SYNTAX_TREE )
+ : null;
+ final ParserProfile wrappedParserProfile = new TurtleParserProfile( parserProfile, syntaxTree );
+ final TurtleTokenizer tokenizer = new TurtleTokenizer( in, wrappedParserProfile.getErrorHandler() );
+ final TurtleParser parser = TurtleParser.create( tokenizer, wrappedParserProfile, output );
parser.parse();
}
@Override
public void read( final Reader in, final String baseUri, final ContentType ct, final StreamRDF output, final Context context ) {
- final Tokenizer tokenizer = TokenizerText.create().source( in ).errorHandler( parserProfile.getErrorHandler() ).build();
- final TurtleParser parser = TurtleParser.create( tokenizer, parserProfile, output );
+ final TurtleSyntaxTree syntaxTree = context != null && context.get( TurtleLoader.TREE_SITTER_SYNTAX_TREE ) != null
+ ? context.get( TurtleLoader.TREE_SITTER_SYNTAX_TREE )
+ : null;
+ final ParserProfile wrappedParserProfile = new TurtleParserProfile( parserProfile, syntaxTree );
+ final Tokenizer tokenizer = TokenizerText.create().source( in ).errorHandler( wrappedParserProfile.getErrorHandler() ).build();
+ final TurtleParser parser = TurtleParser.create( tokenizer, wrappedParserProfile, output );
parser.parse();
}
}
diff --git a/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/resolver/parser/SmartToken.java b/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/resolver/parser/SmartToken.java
index f18020229..2e2ed90c6 100644
--- a/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/resolver/parser/SmartToken.java
+++ b/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/resolver/parser/SmartToken.java
@@ -16,28 +16,38 @@
import java.util.Objects;
import java.util.Optional;
-import org.eclipse.esmf.aspectmodel.AspectModelFile;
-
import org.apache.jena.riot.tokens.Token;
import org.apache.jena.riot.tokens.TokenType;
+import org.eclipse.esmf.aspectmodel.AspectModelFile;
+import org.eclipse.esmf.treesitterturtle.TurtleSyntaxTree;
+
+import org.jspecify.annotations.Nullable;
+
/**
* Wrapper class for a {@link Token}. This provides access to the actual string representation of
* the token, 1-based line and column information.
*/
public final class SmartToken {
- private final Token token;
+ private final Token jenaToken;
+ private final TurtleSyntaxTree.Token treesitterToken;
private AspectModelFile originatingFile;
/**
- * @param token the token
+ * @param jenaToken the token
*/
- public SmartToken( final Token token ) {
- this.token = token;
+ public SmartToken( final Token jenaToken ) {
+ this.jenaToken = jenaToken;
+ treesitterToken = null;
+ }
+
+ public SmartToken( final TurtleSyntaxTree.Token treesitterToken ) {
+ jenaToken = null;
+ this.treesitterToken = treesitterToken;
}
- public TokenType type() {
- return token.getType();
+ public @Nullable TokenType type() {
+ return jenaToken.getType();
}
/**
@@ -46,7 +56,10 @@ public TokenType type() {
* @return the lexical representation
*/
public String image() {
- return token.getImage();
+ if ( jenaToken != null ) {
+ return jenaToken.getImage();
+ }
+ return treesitterToken.content();
}
/**
@@ -55,15 +68,24 @@ public String image() {
* @return the lexical representation
*/
public String image2() {
- return token.getImage2();
+ if ( jenaToken != null ) {
+ return jenaToken.getImage2();
+ }
+ return "";
}
- public Token subToken1() {
- return token.getSubToken1();
+ public @Nullable Token subToken1() {
+ if ( jenaToken != null ) {
+ return jenaToken.getSubToken1();
+ }
+ return null;
}
- public Token subToken2() {
- return token.getSubToken2();
+ public @Nullable Token subToken2() {
+ if ( jenaToken != null ) {
+ return jenaToken.getSubToken2();
+ }
+ return null;
}
/**
@@ -72,16 +94,46 @@ public Token subToken2() {
* @return the line
*/
public int line() {
- return (int) token.getLine();
+ if ( jenaToken != null ) {
+ return (int) jenaToken.getLine();
+ }
+ return treesitterToken.location().fromLine() + 1;
}
/**
- * The column of the token, 1-based. *
+ * The column of the token, 1-based.
*
* @return the column
*/
public int column() {
- return (int) token.getColumn();
+ if ( jenaToken != null ) {
+ return (int) jenaToken.getColumn();
+ }
+ return treesitterToken.location().fromColumn() + 1;
+ }
+
+ /**
+ * The line where the token ends, 1-based.
+ *
+ * @return the line
+ */
+ public int toLine() {
+ if ( jenaToken != null ) {
+ return line();
+ }
+ return treesitterToken.location().toLine() + 1;
+ }
+
+ /**
+ * The column where the token ends, 1-based.
+ *
+ * @return the colum
+ */
+ public int toColumn() {
+ if ( jenaToken != null ) {
+ return column() + content().length();
+ }
+ return treesitterToken.location().toColumn() + 1;
}
/**
@@ -93,11 +145,15 @@ public int column() {
* @return the token's effective content
*/
public String content() {
- return switch ( token.getType() ) {
- case IRI -> "<" + token.getImage() + ">";
- case DIRECTIVE -> "@" + token.getImage();
+ if ( treesitterToken != null ) {
+ return treesitterToken.content();
+ }
+ // For Jena, only an approximation of the token can be provided
+ return switch ( jenaToken.getType() ) {
+ case IRI -> "<" + jenaToken.getImage() + ">";
+ case DIRECTIVE -> "@" + jenaToken.getImage();
case PREFIXED_NAME ->
- Optional.ofNullable( token.getImage() ).orElse( "" ) + ":" + Optional.ofNullable( token.getImage2() ).orElse( "" );
+ Optional.ofNullable( jenaToken.getImage() ).orElse( "" ) + ":" + Optional.ofNullable( jenaToken.getImage2() ).orElse( "" );
case LT -> "<";
case GT -> ">";
case LE -> "<=";
@@ -122,16 +178,17 @@ public String content() {
case STAR -> "*";
case SLASH -> "/";
case RSLASH -> "\\";
- case STRING -> "\"" + token.getImage() + "\"";
- case LITERAL_LANG -> String.format( "\"%s\"@%s", token.getImage(), token.getImage2() );
+ case STRING -> "\"" + jenaToken.getImage() + "\"";
+ case LITERAL_LANG -> String.format( "\"%s\"@%s", jenaToken.getImage(), jenaToken.getImage2() );
case LITERAL_DT ->
- String.format( "\"%s\"^^%s:%s", token.getImage(), token.getSubToken2().getImage(), token.getSubToken2().getImage2() );
- default -> token.getImage();
+ String.format( "\"%s\"^^%s:%s", jenaToken.getImage(), jenaToken.getSubToken2().getImage(),
+ jenaToken.getSubToken2().getImage2() );
+ default -> jenaToken.getImage();
};
}
public Token token() {
- return token;
+ return jenaToken;
}
@Override
@@ -143,21 +200,29 @@ public boolean equals( final Object obj ) {
return false;
}
final var that = (SmartToken) obj;
- return Objects.equals( token, that.token );
+ return jenaToken != null
+ ? Objects.equals( jenaToken, that.jenaToken )
+ : Objects.equals( treesitterToken, that.treesitterToken );
}
@Override
public int hashCode() {
- return Objects.hash( token );
+ return Objects.hash( jenaToken );
}
@Override
public String toString() {
- return "SmartToken[token=" + token + ']';
+ return jenaToken != null
+ ? "SmartToken[jenaToken=" + jenaToken + ']'
+ : "SmartToken[treesitterToken=" + treesitterToken + ']';
+ }
+
+ public Token getJenaToken() {
+ return jenaToken;
}
- public Token getToken() {
- return token;
+ public TurtleSyntaxTree.Token getTreesitterToken() {
+ return treesitterToken;
}
public AspectModelFile getOriginatingFile() {
diff --git a/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/resolver/parser/TokenRegistry.java b/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/resolver/parser/TokenRegistry.java
index 5c2d7b942..4ffa539a7 100644
--- a/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/resolver/parser/TokenRegistry.java
+++ b/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/resolver/parser/TokenRegistry.java
@@ -16,10 +16,11 @@
import java.util.Map;
import java.util.Optional;
+import org.apache.jena.graph.Node;
+
import org.eclipse.esmf.aspectmodel.resolver.services.TurtleLoader;
import com.google.common.collect.MapMaker;
-import org.apache.jena.graph.Node;
/**
* This map keeps track of location information for nodes, i.e., when an RDF document is parsed
@@ -44,4 +45,17 @@ public static void put( final Node node, final SmartToken token ) {
public static Optional getToken( final Node node ) {
return Optional.ofNullable( TOKENS.get( node ) );
}
+
+ /**
+ * Replace a registered node, but keep the associated token
+ *
+ * @param oldNode the old node
+ * @param newNode the new node
+ */
+ public static synchronized void updateNode( final Node oldNode, final Node newNode ) {
+ if ( TOKENS.containsKey( oldNode ) ) {
+ final SmartToken token = TOKENS.remove( oldNode );
+ TOKENS.put( newNode, token );
+ }
+ }
}
diff --git a/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/resolver/parser/TurtleParser.java b/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/resolver/parser/TurtleParser.java
index e12ba5263..9fb8bffac 100644
--- a/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/resolver/parser/TurtleParser.java
+++ b/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/resolver/parser/TurtleParser.java
@@ -19,7 +19,6 @@
import org.apache.jena.riot.tokens.Tokenizer;
public class TurtleParser extends LangTurtle {
-
private TurtleParser( final Tokenizer tokenizer, final ParserProfile parserProfile, final StreamRDF output ) {
super( tokenizer, parserProfile, output );
}
diff --git a/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/resolver/parser/TurtleParserProfile.java b/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/resolver/parser/TurtleParserProfile.java
index 21692680e..d9a04ca90 100644
--- a/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/resolver/parser/TurtleParserProfile.java
+++ b/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/resolver/parser/TurtleParserProfile.java
@@ -13,7 +13,7 @@
package org.eclipse.esmf.aspectmodel.resolver.parser;
-import org.eclipse.esmf.aspectmodel.ValueParsingException;
+import java.util.List;
import org.apache.jena.datatypes.RDFDatatype;
import org.apache.jena.graph.Graph;
@@ -28,16 +28,27 @@
import org.apache.jena.riot.tokens.TokenType;
import org.apache.jena.sparql.core.Quad;
+import org.eclipse.esmf.aspectmodel.ValueParsingException;
+import org.eclipse.esmf.treesitterturtle.ParserTokenType;
+import org.eclipse.esmf.treesitterturtle.TurtleSyntaxTree;
+
+import org.jspecify.annotations.Nullable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
/**
* Customized parser profile that delegates to Jena's built-in Node generation but also registers
* the nodes in the {@link TokenRegistry}, where information about the line/column/token can be
* retrieved at a later time.
*/
public class TurtleParserProfile implements ParserProfile {
+ private static final Logger LOG = LoggerFactory.getLogger( TurtleParserProfile.class );
private final ParserProfile parserProfile;
+ private final List tokens;
- public TurtleParserProfile( final ParserProfile parserProfile ) {
+ public TurtleParserProfile( final ParserProfile parserProfile, final @Nullable TurtleSyntaxTree syntaxTree ) {
this.parserProfile = parserProfile;
+ tokens = syntaxTree == null ? List.of() : syntaxTree.tokens().toList();
}
@Override
@@ -45,11 +56,71 @@ public String getBaseURI() {
return parserProfile.getBaseURI();
}
+ /**
+ * Finds the matching Tree-sitter token for a given Jena token based on line and column position.
+ * If no exact match is found, returns the nearest token.
+ *
+ * @param targetLine the line of the originating Jena token (1-based)
+ * @param targetColumn the column of the originating Jena token (1-based)
+ * @return the matching Tree-sitter token, or null if syntax tree is not available or no tokens
+ * found
+ */
+ private TurtleSyntaxTree.@Nullable Token findMatchingTreeSitterToken( final long targetLine, final long targetColumn ) {
+ if ( tokens.isEmpty() ) {
+ return null;
+ }
+
+ TurtleSyntaxTree.Token exactMatch = null;
+ TurtleSyntaxTree.Token nearestMatch = null;
+ long minDistance = Long.MAX_VALUE;
+ final long originatingLine = targetLine - 1;
+ final long originatingColumn = targetColumn - 1;
+
+ for ( final TurtleSyntaxTree.Token treeToken : tokens ) {
+ if ( treeToken.type().equals( ParserTokenType.OBJECT_LIST ) || treeToken.type().equals( ParserTokenType.TRIPLE ) ) {
+ continue;
+ }
+ final int tokenLine = treeToken.location().fromLine();
+ final int tokenColumn = treeToken.location().fromColumn();
+
+ if ( tokenLine == originatingLine && tokenColumn == originatingColumn ) {
+ exactMatch = treeToken;
+ break;
+ }
+
+ // Calculate Manhattan distance for nearest match
+ final long distance = Math.abs( tokenLine - originatingLine ) * 1000 + Math.abs( tokenColumn - originatingColumn );
+
+ if ( distance < minDistance ) {
+ minDistance = distance;
+ nearestMatch = treeToken;
+ }
+ }
+
+ return exactMatch != null ? exactMatch : nearestMatch;
+ }
+
+ /**
+ * Finds the matching Tree-sitter token for a given Jena token based on line and column position.
+ * If no exact match is found, returns the nearest token.
+ *
+ * @param token the Jena token to find a match for
+ * @return the matching Tree-sitter token, or null if syntax tree is not available or no tokens
+ * found
+ */
+ private TurtleSyntaxTree.@Nullable Token findMatchingTreeSitterToken( final Token token ) {
+ return findMatchingTreeSitterToken( token.getLine(), token.getColumn() );
+ }
+
@Override
public Node create( final Node currentGraph, final Token token ) {
try {
final Node node = parserProfile.create( currentGraph, token );
- TokenRegistry.put( node, new SmartToken( token ) );
+ final TurtleSyntaxTree.@Nullable Token treeSitterToken = findMatchingTreeSitterToken( token );
+ final SmartToken smartToken = treeSitterToken == null
+ ? new SmartToken( token )
+ : new SmartToken( treeSitterToken );
+ TokenRegistry.put( node, smartToken );
return node;
} catch ( final ValueParsingException exception ) {
exception.setLine( token.getLine() );
@@ -90,7 +161,12 @@ public void setBaseIRI( final String baseIri ) {
@Override
public Triple createTriple( final Node subject, final Node predicate, final Node object, final long line, final long col ) {
- return parserProfile.createTriple( subject, predicate, object, line, col );
+ final Triple triple = parserProfile.createTriple( subject, predicate, object, line, col );
+ final TurtleSyntaxTree.@Nullable Token treeSitterToken = findMatchingTreeSitterToken( line, col );
+ if ( treeSitterToken != null ) {
+ TokenRegistry.put( object, new SmartToken( treeSitterToken ) );
+ }
+ return triple;
}
@Override
diff --git a/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/resolver/services/TurtleLoader.java b/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/resolver/services/TurtleLoader.java
index 2369c9682..c93a03d81 100644
--- a/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/resolver/services/TurtleLoader.java
+++ b/core/esmf-aspect-meta-model-java/src/main/java/org/eclipse/esmf/aspectmodel/resolver/services/TurtleLoader.java
@@ -23,32 +23,36 @@
import java.util.Objects;
import java.util.stream.Collectors;
-import org.eclipse.esmf.aspectmodel.ValueParsingException;
-import org.eclipse.esmf.aspectmodel.resolver.exceptions.ParserException;
-import org.eclipse.esmf.aspectmodel.resolver.parser.ReaderRiotTurtle;
-import org.eclipse.esmf.metamodel.datatype.SammXsdType;
-
-import io.vavr.control.Try;
+import org.apache.commons.io.IOUtils;
import org.apache.jena.rdf.model.Model;
import org.apache.jena.riot.Lang;
import org.apache.jena.riot.RDFParser;
import org.apache.jena.riot.RDFParserRegistry;
import org.apache.jena.riot.RiotException;
import org.apache.jena.riot.system.FactoryRDFStd;
+import org.apache.jena.sparql.util.Context;
+import org.apache.jena.sparql.util.Symbol;
+
+import org.eclipse.esmf.aspectmodel.ValueParsingException;
+import org.eclipse.esmf.aspectmodel.resolver.exceptions.ParserException;
+import org.eclipse.esmf.aspectmodel.resolver.parser.ReaderRiotTurtle;
+import org.eclipse.esmf.metamodel.datatype.SammXsdType;
+import org.eclipse.esmf.treesitterturtle.TurtleSyntaxTree;
+
import org.jspecify.annotations.NonNull;
import org.jspecify.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import io.vavr.control.Try;
+
public final class TurtleLoader {
private static final Logger LOG = LoggerFactory.getLogger( TurtleLoader.class );
-
private static volatile boolean isTurtleRegistered = false;
private TurtleLoader() {}
public static void init() {
- SammXsdType.setupTypeMapping();
registerTurtle();
}
@@ -114,6 +118,40 @@ public static Try loadTurtle( @NonNull final String modelContent, @Nullab
}
}
+ public static class TreeSitterTurtleSyntaxTreeSymbol extends Symbol {
+ protected TreeSitterTurtleSyntaxTreeSymbol() {
+ super( "treesittersyntaxtree" );
+ }
+ }
+
+ public static final Symbol TREE_SITTER_SYNTAX_TREE = new TreeSitterTurtleSyntaxTreeSymbol();
+
+ public static Try loadTurtle( final TurtleSyntaxTree syntaxTree, final URI location ) {
+ init();
+ final String sourceRepresentation = syntaxTree.sourceRepresentationSupplier().get();
+ try ( final InputStream input = IOUtils.toInputStream( sourceRepresentation, StandardCharsets.UTF_8 ) ) {
+ final Context context = Context.create();
+ context.put( new TreeSitterTurtleSyntaxTreeSymbol(), syntaxTree );
+ final Model streamModel = RDFParser.create()
+ .factory( new FactoryRDFStd() )
+ .source( input )
+ .lang( Lang.TURTLE )
+ .context( context )
+ .toModel();
+ return Try.success( streamModel );
+ } catch ( final ValueParsingException exception ) {
+ // This is thrown by the custom value parsers in SammXsdType
+ exception.setSourceDocument( sourceRepresentation );
+ exception.setSourceLocation( location );
+ throw exception;
+ } catch ( final RiotException exception ) {
+ // Thrown by Jena for regular syntax errors
+ return Try.failure( new ParserException( exception, sourceRepresentation, location ) );
+ } catch ( final IOException exception ) {
+ return Try.failure( exception );
+ }
+ }
+
/**
* Loads a Turtle model from a String containing RDF/Turtle
*
@@ -130,6 +168,7 @@ private static void registerTurtle() {
}
synchronized ( TurtleLoader.class ) {
if ( !isTurtleRegistered ) {
+ SammXsdType.setupTypeMapping();
RDFParserRegistry.registerLangTriples( Lang.TURTLE, ReaderRiotTurtle.factory );
isTurtleRegistered = true;
}
diff --git a/core/esmf-aspect-meta-model-java/src/test/java/org/eclipse/esmf/aspectmodel/loader/AspectModelLoaderTest.java b/core/esmf-aspect-meta-model-java/src/test/java/org/eclipse/esmf/aspectmodel/loader/AspectModelLoaderTest.java
index c320a4733..807c54198 100644
--- a/core/esmf-aspect-meta-model-java/src/test/java/org/eclipse/esmf/aspectmodel/loader/AspectModelLoaderTest.java
+++ b/core/esmf-aspect-meta-model-java/src/test/java/org/eclipse/esmf/aspectmodel/loader/AspectModelLoaderTest.java
@@ -66,7 +66,7 @@ void testLoadAspectModelsSourceFilesArePresent( final TestAspect testAspect ) {
void loadAspectModelWithoutCharacteristicDatatype() {
assertThatThrownBy( () -> TestResources.load( InvalidTestAspect.INVALID_CHARACTERISTIC_DATATYPE ) )
.isInstanceOf( AspectLoadingException.class )
- .hasMessage( "No datatype is defined on the Characteristic instance 'Characteristic1: '." );
+ .hasMessage( "No datatype is defined on the Characteristic instance 'Characteristic1'." );
}
@Test
diff --git a/core/esmf-aspect-meta-model-java/src/test/java/org/eclipse/esmf/aspectmodel/resolver/services/TurtleLoaderTest.java b/core/esmf-aspect-meta-model-java/src/test/java/org/eclipse/esmf/aspectmodel/resolver/services/TurtleLoaderTest.java
index f4af86f27..4d2b36a7a 100644
--- a/core/esmf-aspect-meta-model-java/src/test/java/org/eclipse/esmf/aspectmodel/resolver/services/TurtleLoaderTest.java
+++ b/core/esmf-aspect-meta-model-java/src/test/java/org/eclipse/esmf/aspectmodel/resolver/services/TurtleLoaderTest.java
@@ -20,16 +20,26 @@
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
+import java.net.URI;
import java.nio.charset.StandardCharsets;
-import org.eclipse.esmf.aspectmodel.resolver.exceptions.ParserException;
-
-import io.vavr.control.Try;
import org.apache.jena.rdf.model.Model;
import org.apache.jena.rdf.model.ModelFactory;
import org.apache.jena.riot.RDFLanguages;
import org.apache.jena.riot.RiotException;
+
+import org.eclipse.esmf.aspectmodel.resolver.exceptions.ParserException;
+import org.eclipse.esmf.test.InvalidTestAspect;
+import org.eclipse.esmf.test.TestAspect;
+import org.eclipse.esmf.test.TestResources;
+import org.eclipse.esmf.treesitterturtle.TreeSitterTurtle;
+import org.eclipse.esmf.treesitterturtle.TurtleSyntaxTree;
+
import org.junit.jupiter.api.Test;
+import org.treesitter.TSParser;
+import org.treesitter.TSTree;
+
+import io.vavr.control.Try;
public class TurtleLoaderTest {
private static final String MODEL = """
@@ -57,4 +67,40 @@ void jenaReaderSucceedsWhenPrefixIsNotDefined() throws IOException {
.hasMessageContaining( "[line: 2, col: 13] Undefined prefix: aPrefix" );
}
}
+
+ @Test
+ void loadModelUsingTreeSitter() throws IOException {
+ final String turtle =
+ new String( TestResources.testModelSource( TestAspect.ASPECT_WITH_BINARY ).readAllBytes(), StandardCharsets.UTF_8 );
+ try ( final TSParser parser = new TSParser() ) {
+ parser.setLanguage( new TreeSitterTurtle() );
+ final TSTree tsTree = parser.parseString( null, turtle );
+ final TurtleSyntaxTree turtleSyntaxTree = TurtleSyntaxTree.fromConcreteSyntaxTree( tsTree, () -> turtle,
+ new TurtleSyntaxTree.StringTokenProvider( turtle ) );
+ assertThatCode( () -> {
+ final Try tryModel = TurtleLoader.loadTurtle( turtleSyntaxTree, buildArtificialUri( turtle, "model" ) );
+ assertThat( tryModel.isFailure() ).isFalse();
+ final Model model = tryModel.get();
+ assertThat( model.listStatements().toList() ).isNotEmpty();
+ } ).doesNotThrowAnyException();
+ }
+ }
+
+ @Test
+ void loadModelWithSyntaxErrorUsingTreeSitter() throws IOException {
+ final String turtle =
+ new String( TestResources.testModelSource( InvalidTestAspect.INVALID_SYNTAX ).readAllBytes(), StandardCharsets.UTF_8 );
+ try ( final TSParser parser = new TSParser() ) {
+ parser.setLanguage( new TreeSitterTurtle() );
+ final TSTree tsTree = parser.parseString( null, turtle );
+ final TurtleSyntaxTree turtleSyntaxTree = TurtleSyntaxTree.fromConcreteSyntaxTree( tsTree, () -> turtle,
+ new TurtleSyntaxTree.StringTokenProvider( turtle ) );
+ final Try tryModel = TurtleLoader.loadTurtle( turtleSyntaxTree, buildArtificialUri( turtle, "model" ) );
+ assertThat( tryModel.isFailure() ).isTrue();
+ }
+ }
+
+ private static URI buildArtificialUri( final Object object, final String objectType ) {
+ return URI.create( "inmemory:%s:%s".formatted( objectType, object.hashCode() ) );
+ }
}
diff --git a/core/esmf-aspect-meta-model-java/src/test/java/org/eclipse/esmf/test/TestResources.java b/core/esmf-aspect-meta-model-java/src/test/java/org/eclipse/esmf/test/TestResources.java
index 75456bfc7..20566c1ae 100644
--- a/core/esmf-aspect-meta-model-java/src/test/java/org/eclipse/esmf/test/TestResources.java
+++ b/core/esmf-aspect-meta-model-java/src/test/java/org/eclipse/esmf/test/TestResources.java
@@ -23,6 +23,13 @@
import org.eclipse.esmf.samm.KnownVersion;
public class TestResources {
+ @SuppressWarnings( "DataFlowIssue" )
+ public static InputStream testModelSource( final TestAspect model ) {
+ final String path = String.format( "valid/%s/%s/%s.ttl", model.getUrn().getNamespaceMainPart(), model.getUrn().getVersion(),
+ model.getName() );
+ return TestResources.class.getClassLoader().getResourceAsStream( path );
+ }
+
public static AspectModel load( final TestAspect model ) {
final KnownVersion metaModelVersion = KnownVersion.getLatest();
final String path = String.format( "valid/%s/%s/%s.ttl", model.getUrn().getNamespaceMainPart(), model.getUrn().getVersion(),
@@ -49,6 +56,13 @@ public static AspectModel load( final OrderingTestAspect model ) {
return new AspectModelLoader( testModelsResolutionStrategy ).load( inputStream, URI.create( "testmodel:" + path ) );
}
+ @SuppressWarnings( "DataFlowIssue" )
+ public static InputStream testModelSource( final InvalidTestAspect model ) {
+ final String path = String.format( "invalid/%s/%s/%s.ttl", model.getUrn().getNamespaceMainPart(), model.getUrn().getVersion(),
+ model.getName() );
+ return TestResources.class.getClassLoader().getResourceAsStream( path );
+ }
+
public static AspectModel load( final InvalidTestAspect model ) {
final KnownVersion metaModelVersion = KnownVersion.getLatest();
final String path = String.format( "invalid/%s/%s/%s.ttl", model.getUrn().getNamespaceMainPart(), model.getUrn().getVersion(),
diff --git a/core/esmf-aspect-model-document-generators/src/test/java/org/eclipse/esmf/aspectmodel/generator/openapi/AspectModelOpenApiGeneratorTest.java b/core/esmf-aspect-model-document-generators/src/test/java/org/eclipse/esmf/aspectmodel/generator/openapi/AspectModelOpenApiGeneratorTest.java
index c6cb3f0f1..dd4e7f2a4 100644
--- a/core/esmf-aspect-model-document-generators/src/test/java/org/eclipse/esmf/aspectmodel/generator/openapi/AspectModelOpenApiGeneratorTest.java
+++ b/core/esmf-aspect-model-document-generators/src/test/java/org/eclipse/esmf/aspectmodel/generator/openapi/AspectModelOpenApiGeneratorTest.java
@@ -15,6 +15,7 @@
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatCode;
+import static org.assertj.core.api.Assumptions.assumeThat;
import java.io.IOException;
import java.io.InputStream;
@@ -26,6 +27,9 @@
import java.util.Locale;
import java.util.Map;
+import org.apache.commons.io.IOUtils;
+import org.assertj.core.api.InstanceOfAssertFactories;
+
import org.eclipse.esmf.aspectmodel.generator.AbstractSchemaArtifact;
import org.eclipse.esmf.aspectmodel.generator.jsonschema.AspectModelJsonSchemaGenerator;
import org.eclipse.esmf.metamodel.Aspect;
@@ -33,9 +37,13 @@
import org.eclipse.esmf.test.TestAspect;
import org.eclipse.esmf.test.TestResources;
-import ch.qos.logback.classic.Logger;
-import ch.qos.logback.classic.spi.ILoggingEvent;
-import ch.qos.logback.core.read.ListAppender;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.parallel.Execution;
+import org.junit.jupiter.api.parallel.ExecutionMode;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.EnumSource;
+import org.slf4j.LoggerFactory;
+
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import com.fasterxml.jackson.databind.JsonNode;
@@ -50,6 +58,10 @@
import com.jayway.jsonpath.DocumentContext;
import com.jayway.jsonpath.JsonPath;
import com.jayway.jsonpath.Option;
+
+import ch.qos.logback.classic.Logger;
+import ch.qos.logback.classic.spi.ILoggingEvent;
+import ch.qos.logback.core.read.ListAppender;
import io.swagger.parser.OpenAPIParser;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.Operation;
@@ -59,14 +71,6 @@
import io.swagger.v3.oas.models.parameters.Parameter;
import io.swagger.v3.oas.models.responses.ApiResponse;
import io.swagger.v3.parser.core.models.SwaggerParseResult;
-import org.apache.commons.io.IOUtils;
-import org.assertj.core.api.InstanceOfAssertFactories;
-import org.junit.jupiter.api.Test;
-import org.junit.jupiter.api.parallel.Execution;
-import org.junit.jupiter.api.parallel.ExecutionMode;
-import org.junit.jupiter.params.ParameterizedTest;
-import org.junit.jupiter.params.provider.EnumSource;
-import org.slf4j.LoggerFactory;
class AspectModelOpenApiGeneratorTest {
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
@@ -301,7 +305,9 @@ void testValidTemplate() throws IOException {
@Test
void testInValidParameterName() throws IOException {
final ListAppender logAppender = new ListAppender<>();
- final Logger logger = (Logger) LoggerFactory.getLogger( AspectModelOpenApiGenerator.class );
+ final org.slf4j.Logger theLogger = LoggerFactory.getLogger( AspectModelOpenApiGenerator.class );
+ assumeThat( theLogger ).isInstanceOf( ch.qos.logback.classic.Logger.class );
+ final Logger logger = (Logger) theLogger;
logger.addAppender( logAppender );
logAppender.start();
final Aspect aspect = TestResources.load( TestAspect.ASPECT_WITHOUT_SEE_ATTRIBUTE ).aspect();
diff --git a/core/esmf-aspect-model-github-resolver/src/main/java/org/eclipse/esmf/aspectmodel/resolver/github/GitHubModelSource.java b/core/esmf-aspect-model-github-resolver/src/main/java/org/eclipse/esmf/aspectmodel/resolver/github/GitHubModelSource.java
index ea37b8b54..cda29b1c2 100644
--- a/core/esmf-aspect-model-github-resolver/src/main/java/org/eclipse/esmf/aspectmodel/resolver/github/GitHubModelSource.java
+++ b/core/esmf-aspect-model-github-resolver/src/main/java/org/eclipse/esmf/aspectmodel/resolver/github/GitHubModelSource.java
@@ -29,7 +29,7 @@
import org.eclipse.esmf.aspectmodel.AspectModelFile;
import org.eclipse.esmf.aspectmodel.resolver.AspectModelFileLoader;
-import org.eclipse.esmf.aspectmodel.resolver.Download;
+import org.eclipse.esmf.util.download.Download;
import org.eclipse.esmf.aspectmodel.resolver.GithubRepository;
import org.eclipse.esmf.aspectmodel.resolver.ModelSource;
import org.eclipse.esmf.aspectmodel.resolver.modelfile.RawAspectModelFile;
diff --git a/core/esmf-aspect-model-github-resolver/src/main/java/org/eclipse/esmf/aspectmodel/resolver/github/GithubModelSourceConfig.java b/core/esmf-aspect-model-github-resolver/src/main/java/org/eclipse/esmf/aspectmodel/resolver/github/GithubModelSourceConfig.java
index b68e601ac..1c9004643 100644
--- a/core/esmf-aspect-model-github-resolver/src/main/java/org/eclipse/esmf/aspectmodel/resolver/github/GithubModelSourceConfig.java
+++ b/core/esmf-aspect-model-github-resolver/src/main/java/org/eclipse/esmf/aspectmodel/resolver/github/GithubModelSourceConfig.java
@@ -16,7 +16,7 @@
import java.util.Optional;
import org.eclipse.esmf.aspectmodel.resolver.GithubRepository;
-import org.eclipse.esmf.aspectmodel.resolver.ProxyConfig;
+import org.eclipse.esmf.util.download.ProxyConfig;
import io.soabase.recordbuilder.core.RecordBuilder;
diff --git a/core/esmf-aspect-model-validator/src/main/java/org/eclipse/esmf/aspectmodel/shacl/violation/Violation.java b/core/esmf-aspect-model-validator/src/main/java/org/eclipse/esmf/aspectmodel/shacl/violation/Violation.java
index ba76c0166..c1dab3513 100644
--- a/core/esmf-aspect-model-validator/src/main/java/org/eclipse/esmf/aspectmodel/shacl/violation/Violation.java
+++ b/core/esmf-aspect-model-validator/src/main/java/org/eclipse/esmf/aspectmodel/shacl/violation/Violation.java
@@ -17,6 +17,8 @@
import java.util.List;
import java.util.Optional;
+import org.apache.jena.rdf.model.RDFNode;
+
import org.eclipse.esmf.aspectmodel.AspectModelFile;
import org.eclipse.esmf.aspectmodel.resolver.parser.SmartToken;
import org.eclipse.esmf.aspectmodel.resolver.parser.TokenRegistry;
@@ -27,7 +29,7 @@
import org.eclipse.esmf.aspectmodel.validation.ProcessingViolation;
import org.eclipse.esmf.aspectmodel.validation.RegularExpressionConstraintViolation;
-import org.apache.jena.rdf.model.RDFNode;
+import org.jspecify.annotations.Nullable;
/**
* Represents a single violation raised by one or more SHACL shapes against an RDF model. A
@@ -59,8 +61,8 @@ public interface Violation {
/**
* The RDF node this violation focusses on
*/
- default RDFNode highlight() {
- return context().element();
+ default @Nullable RDFNode highlight() {
+ return context() == null ? null : context().element();
}
/**
diff --git a/core/esmf-aspect-model-validator/src/main/java/org/eclipse/esmf/aspectmodel/validation/ProcessingViolation.java b/core/esmf-aspect-model-validator/src/main/java/org/eclipse/esmf/aspectmodel/validation/ProcessingViolation.java
index 5ca9f32d5..c26fbb65f 100644
--- a/core/esmf-aspect-model-validator/src/main/java/org/eclipse/esmf/aspectmodel/validation/ProcessingViolation.java
+++ b/core/esmf-aspect-model-validator/src/main/java/org/eclipse/esmf/aspectmodel/validation/ProcessingViolation.java
@@ -13,9 +13,14 @@
package org.eclipse.esmf.aspectmodel.validation;
+import org.apache.jena.rdf.model.RDFNode;
+
+import org.eclipse.esmf.aspectmodel.AspectLoadingException;
import org.eclipse.esmf.aspectmodel.shacl.violation.EvaluationContext;
import org.eclipse.esmf.aspectmodel.shacl.violation.Violation;
+import org.jspecify.annotations.Nullable;
+
/**
* Meta violation: The validation was unsuccessful, for example because the model could not be
* loaded or not be resolved
@@ -42,6 +47,11 @@ public String message() {
return violationSpecificMessage();
}
+ @Override
+ public @Nullable RDFNode highlight() {
+ return cause instanceof final AspectLoadingException aspectLoadingException ? aspectLoadingException.highlightElement() : null;
+ }
+
@Override
public T accept( final Visitor visitor ) {
return visitor.visitProcessingViolation( this );
diff --git a/core/esmf-tree-sitter-turtle/.gitignore b/core/esmf-tree-sitter-turtle/.gitignore
new file mode 100644
index 000000000..eb4c9a6ad
--- /dev/null
+++ b/core/esmf-tree-sitter-turtle/.gitignore
@@ -0,0 +1,18 @@
+src/main/c/parser.c
+src/main/c/grammar.json
+src/main/c/node-types.json
+src/main/c/tree_sitter/*
+src/main/c/include/jni.h
+src/main/c/include/linux/jni_md.h
+src/main/c/include/linux/jawt_md.h
+src/main/c/include/macos/jni_md.h
+src/main/c/include/macos/jawt_md.h
+src/main/c/include/windows/jni_md.h
+src/main/c/include/windows/jawt_md.h
+src/main/c/include/windows/bridge/AccessBridgeCallbacks.h
+src/main/c/include/windows/bridge/AccessBridgeCalls.h
+src/main/c/include/windows/bridge/AccessBridgePackages.h
+src/main/js/grammar.js
+src/main/js/bindings
+src/main/js/js
+.zig-cache
diff --git a/core/esmf-tree-sitter-turtle/README.md b/core/esmf-tree-sitter-turtle/README.md
new file mode 100644
index 000000000..18d9528c4
--- /dev/null
+++ b/core/esmf-tree-sitter-turtle/README.md
@@ -0,0 +1,22 @@
+# ESMF tree-sitter-turtle
+
+This module provides a parser for RDF/Turtle, to be used with the [tree-sitter Java binding](https://github.com/bonede/tree-sitter-ng).
+First, native C code is generated from the grammar defined in src/main/js/grammar.js using the tree-sitter CLI. This C code is then compiled
+to native libraries (.so/.dylib/.dll) for Linux/MacOS/Windows using the [Zig compiler](https://ziglang.org/); since it has built-in
+cross-platform compilation capabilities. As the last step, a Java class makes the library available to Java code via JNI.
+The Java code that performs the download and execution of Zig is compiled and executed during the build, but is not part of the
+final module artifact.
+
+The build works as follows, in chronological order:
+
+| Maven lifecycle phase | Plugin | Maven Goal | Description |
+|-----------------------|-----------------------|------------|-------------|
+| initialize | frontend-maven-plugin | install-node-and-npm | Locally install node and npm |
+| initialize | fronted-maven-plugin | npm | Use npm to install tree-sitter CLI locally |
+| generate-sources | exec-maven-plugin | exec | Execute tree-sitter CLI to generate Turtle parser C code |
+| generate-sources | maven-compiler-plugin | compile | Compile DownloadZig and NativeCompile classes (build time code) |
+| process-sources | exec-maven-plugin | java | Execute DownloadZig class, to download and extract Zig release |
+| compile | maven-compiler-plugin | compile | Compile regular Java binding class (TreeSitterTurtle) |
+| compile | exec-maven-plugin | java | Execute NativeCompile class, to use Zig compiler to create native libraries from Turtle parser C code |
+
+At the end of the build, platform-specific native libs are present in target/classes/libs.
diff --git a/core/esmf-tree-sitter-turtle/pom.xml b/core/esmf-tree-sitter-turtle/pom.xml
new file mode 100644
index 000000000..987e61eb3
--- /dev/null
+++ b/core/esmf-tree-sitter-turtle/pom.xml
@@ -0,0 +1,472 @@
+
+
+
+
+
+ org.eclipse.esmf
+ esmf-sdk-parent
+ DEV-SNAPSHOT
+ ../../pom.xml
+
+ 4.0.0
+
+ esmf-tree-sitter-turtle
+ ESMF Tree Sitter Turtle
+ jar
+
+
+
+ 1.12.1
+ 0.2.1
+ 1.9.0
+
+ 11.2.0
+ 22.14.0
+ 0.15.2
+ 0.11
+
+ jdk-25+36
+
+
+ 0.25.10
+ tree-sitter-cli
+ 0.26.6
+
+
+ ${project.build.directory}/node/node
+ ${project.basedir}/.zig-cache
+
+ c
+ js
+
+
+
+
+ org.eclipse.esmf
+ esmf-util
+ compile
+
+
+ io.github.bonede
+ tree-sitter
+ ${tree-sitter.version}
+
+
+ org.apache.commons
+ commons-compress
+ compile
+
+
+ org.tukaani
+ xz
+ compile
+
+
+ com.fasterxml.jackson.core
+ jackson-databind
+ compile
+
+
+
+
+ org.junit.jupiter
+ junit-jupiter
+ test
+
+
+ org.assertj
+ assertj-core
+ test
+
+
+ org.slf4j
+ slf4j-nop
+ test
+
+
+
+
+
+
+
+ com.github.eirslett
+ frontend-maven-plugin
+ ${frontend-maven-plugin.version}
+
+
+
+
+
+
+ org.codehaus.mojo
+ build-helper-maven-plugin
+
+
+ add-generated-sources
+ initialize
+
+ add-source
+
+
+
+ ${generated-sources}/main/java
+
+
+
+
+ add-buildtime-sources
+ initialize
+
+ add-source
+
+
+
+ ${build-time-sources}/main/java
+
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-jar-plugin
+
+
+ **/buildtime/*.class
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-clean-plugin
+
+
+
+ ${generated-sources}
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-resources-plugin
+
+
+ copy-npm-config
+ initialize
+
+ copy-resources
+
+
+
+
+ ${project.basedir}/../..
+
+ package-lock.json
+ package.json
+
+
+
+ ${project.build.directory}
+
+
+
+
+
+
+ com.googlecode.maven-download-plugin
+ download-maven-plugin
+ ${download-maven-plugin.version}
+
+
+ download-jni-header
+ initialize
+
+ wget
+
+
+ https://raw.githubusercontent.com/openjdk/jdk/refs/tags/${openjdk.tag}/src/java.base/share/native/include/jni.h
+ ${project.basedir}/src/main/c/include
+ true
+
+
+
+ download-jni-headers-unix
+ initialize
+
+ wget
+
+
+ https://raw.githubusercontent.com/openjdk/jdk/refs/tags/${openjdk.tag}/src/java.base/unix/native/include/jni_md.h
+ ${project.basedir}/src/main/c/include/linux
+ true
+
+
+
+ download-jni-headers-macos
+ initialize
+
+ wget
+
+
+ https://raw.githubusercontent.com/openjdk/jdk/refs/tags/${openjdk.tag}/src/java.base/unix/native/include/jni_md.h
+ ${project.basedir}/src/main/c/include/macos
+ true
+
+
+
+ download-jni-headers-windows
+ initialize
+
+ wget
+
+
+ https://raw.githubusercontent.com/openjdk/jdk/refs/tags/${openjdk.tag}/src/java.base/windows/native/include/jni_md.h
+ ${project.basedir}/src/main/c/include/windows
+ true
+
+
+
+ download-jawt-headers-unix
+ initialize
+
+ wget
+
+
+ https://raw.githubusercontent.com/openjdk/jdk/refs/tags/${openjdk.tag}/src/java.desktop/unix/native/include/jawt_md.h
+ ${project.basedir}/src/main/c/include/linux
+ true
+
+
+
+ download-jawt-headers-macos
+ initialize
+
+ wget
+
+
+ https://raw.githubusercontent.com/openjdk/jdk/refs/tags/${openjdk.tag}/src/java.desktop/unix/native/include/jawt_md.h
+ ${project.basedir}/src/main/c/include/macos
+ true
+
+
+
+ download-jawt-headers-windows
+ initialize
+
+ wget
+
+
+ https://raw.githubusercontent.com/openjdk/jdk/refs/tags/${openjdk.tag}/src/java.desktop/windows/native/include/jawt_md.h
+ ${project.basedir}/src/main/c/include/windows
+ true
+
+
+
+ download-accessbridge-callbacks-windows
+ initialize
+
+ wget
+
+
+
+ https://raw.githubusercontent.com/openjdk/jdk/refs/tags/${openjdk.tag}/src/jdk.accessibility/windows/native/include/bridge/AccessBridgeCallbacks.h
+
+ ${project.basedir}/src/main/c/include/windows/bridge
+ true
+
+
+
+ download-accessbridge-calls-windows
+ initialize
+
+ wget
+
+
+
+ https://raw.githubusercontent.com/openjdk/jdk/refs/tags/${openjdk.tag}/src/jdk.accessibility/windows/native/include/bridge/AccessBridgeCalls.h
+
+ ${project.basedir}/src/main/c/include/windows/bridge
+ true
+
+
+
+ download-accessbridge-packages-windows
+ initialize
+
+ wget
+
+
+
+ https://raw.githubusercontent.com/openjdk/jdk/refs/tags/${openjdk.tag}/src/jdk.accessibility/windows/native/include/bridge/AccessBridgePackages.h
+
+ ${project.basedir}/src/main/c/include/windows/bridge
+ true
+
+
+
+
+
+
+ com.github.eirslett
+ frontend-maven-plugin
+
+
+ install-node-and-npm
+ initialize
+
+ install-node-and-npm
+
+
+ v${node.version}
+ ${npm.version}
+ ${project.build.directory}
+
+
+
+ install-tree-sitter-cli
+ initialize
+
+ npm
+
+
+ ${project.build.directory}
+ ci --prefix .
+
+
+
+
+
+
+ org.codehaus.mojo
+ exec-maven-plugin
+
+
+ download-turtle-grammar
+ generate-sources
+
+ java
+
+
+ org.eclipse.esmf.buildtime.DownloadTurtleGrammar
+
+
+ ${project.basedir}/src/main/${js.source.directory}
+
+
+
+
+ tree-sitter-generate
+ generate-sources
+
+ exec
+
+
+ ${node.path}
+ ${project.basedir}/src/main/js
+
+ ${project.build.directory}/node_modules/tree-sitter-cli/cli.js generate grammar.js --output ../${c.source.directory}
+
+
+
+
+ download-zig
+ process-sources
+
+ java
+
+
+ org.eclipse.esmf.buildtime.DownloadZig
+
+
+ ${zig-cache.path}
+
+ ${zig.version}
+
+ ${minisign.version}
+
+
+ ${settings.localRepository}/org/tukaani/xz/${xz.version}/xz-${xz.version}.jar
+
+
+
+
+ native-compile
+ compile
+
+ java
+
+
+ org.eclipse.esmf.buildtime.NativeCompile
+
+
+ ${zig-cache.path}
+
+ ${zig.version}
+
+ ${project.build.outputDirectory}/lib
+
+ ${project.basedir}/src/main/${c.source.directory}
+
+
+
+
+ generate-parser-token-types
+ process-sources
+
+ java
+
+
+ org.eclipse.esmf.buildtime.GenerateParserTokenType
+
+
+ ${project.basedir}/src/main/${c.source.directory}/node-types.json
+
+ ${generated-sources}
+
+
+
+
+
+
+
+ maven-compiler-plugin
+
+
+ -h
+ ${project.build.directory}/generated-include
+
+
+
+
+ compile-build-time-code
+ initialize
+
+ compile
+
+
+ ${build-time-sources}
+
+
+
+
+
+
+
diff --git a/core/esmf-tree-sitter-turtle/src-buildtime/main/java/org/eclipse/esmf/buildtime/BuildTimeException.java b/core/esmf-tree-sitter-turtle/src-buildtime/main/java/org/eclipse/esmf/buildtime/BuildTimeException.java
new file mode 100644
index 000000000..8a05a0e09
--- /dev/null
+++ b/core/esmf-tree-sitter-turtle/src-buildtime/main/java/org/eclipse/esmf/buildtime/BuildTimeException.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright (c) 2026 Robert Bosch Manufacturing Solutions GmbH
+ *
+ * See the AUTHORS file(s) distributed with this work for additional
+ * information regarding authorship.
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ *
+ * SPDX-License-Identifier: MPL-2.0
+ */
+
+package org.eclipse.esmf.buildtime;
+
+public class BuildTimeException extends RuntimeException {
+ public BuildTimeException( final String message ) {
+ super( message );
+ }
+
+ public BuildTimeException( final Exception exception ) {
+ super( exception );
+ }
+}
diff --git a/core/esmf-tree-sitter-turtle/src-buildtime/main/java/org/eclipse/esmf/buildtime/BuildTimeTool.java b/core/esmf-tree-sitter-turtle/src-buildtime/main/java/org/eclipse/esmf/buildtime/BuildTimeTool.java
new file mode 100644
index 000000000..b7e89c9a9
--- /dev/null
+++ b/core/esmf-tree-sitter-turtle/src-buildtime/main/java/org/eclipse/esmf/buildtime/BuildTimeTool.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright (c) 2026 Robert Bosch Manufacturing Solutions GmbH
+ *
+ * See the AUTHORS file(s) distributed with this work for additional
+ * information regarding authorship.
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ *
+ * SPDX-License-Identifier: MPL-2.0
+ */
+
+package org.eclipse.esmf.buildtime;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.MalformedURLException;
+import java.net.URI;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.security.DigestInputStream;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.time.Duration;
+
+import org.eclipse.esmf.util.download.Download;
+import org.eclipse.esmf.util.download.ProxyConfig;
+
+import org.jspecify.annotations.Nullable;
+
+import jakarta.xml.bind.annotation.adapters.HexBinaryAdapter;
+
+public abstract class BuildTimeTool {
+ private final @Nullable Path cacheLocation;
+ protected final ProxyConfig proxyConfig;
+
+ public BuildTimeTool() {
+ this( null );
+ }
+
+ public BuildTimeTool( final @Nullable Path cacheLocation ) {
+ this.cacheLocation = cacheLocation;
+ proxyConfig = ProxyConfig.detectProxySettings();
+ }
+
+ protected void mkdir( final Path path ) {
+ try {
+ Files.createDirectories( path );
+ } catch ( final IOException exception ) {
+ throw new BuildTimeException( exception );
+ }
+ }
+
+ protected URL url( final String url ) {
+ try {
+ return URI.create( url ).toURL();
+ } catch ( final MalformedURLException exception ) {
+ throw new BuildTimeException( exception );
+ }
+ }
+
+ protected Path cacheLocation() {
+ if ( cacheLocation == null ) {
+ throw new BuildTimeException( "Cache was not initialized" );
+ }
+ return cacheLocation;
+ }
+
+ protected File getOrDownloadFile( final URL location ) {
+ final Path filePath = Paths.get( location.getPath() );
+ final String filename = filePath.getName( filePath.getNameCount() - 1 ).toString();
+ final File targetFile = cacheLocation().resolve( filename ).toFile();
+ if ( targetFile.exists() ) {
+ return targetFile;
+ }
+
+ final Download.Config config = new Download.Config( proxyConfig, Duration.ofSeconds( 3L ) );
+ System.out.println( "Downloading " + location + " to " + targetFile.getName() + "..." );
+ return new Download( config ).downloadFile( location, targetFile );
+ }
+
+ protected String sha1( final File file ) {
+ try ( final InputStream input = new FileInputStream( file );
+ final DigestInputStream digestStream = new DigestInputStream( input, MessageDigest.getInstance( "SHA-1" ) ) ) {
+ digestStream.readAllBytes();
+ return new HexBinaryAdapter().marshal( digestStream.getMessageDigest().digest() ).toLowerCase();
+ } catch ( final NoSuchAlgorithmException | IOException exception ) {
+ throw new BuildTimeException( "Could not calculate SHA1 sum for " + file );
+ }
+ }
+
+ protected void write( final File outputFile, final String fileContent ) {
+ try ( final OutputStream outputStream = new FileOutputStream( outputFile ) ) {
+ outputStream.write( fileContent.getBytes( StandardCharsets.UTF_8 ) );
+ } catch ( final IOException exception ) {
+ throw new RuntimeException( "Could not write source code file " + outputFile, exception );
+ }
+ System.out.println( "Written " + outputFile );
+ }
+}
diff --git a/core/esmf-tree-sitter-turtle/src-buildtime/main/java/org/eclipse/esmf/buildtime/DownloadTurtleGrammar.java b/core/esmf-tree-sitter-turtle/src-buildtime/main/java/org/eclipse/esmf/buildtime/DownloadTurtleGrammar.java
new file mode 100644
index 000000000..3e56b6acc
--- /dev/null
+++ b/core/esmf-tree-sitter-turtle/src-buildtime/main/java/org/eclipse/esmf/buildtime/DownloadTurtleGrammar.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (c) 2026 Robert Bosch Manufacturing Solutions GmbH
+ *
+ * See the AUTHORS file(s) distributed with this work for additional
+ * information regarding authorship.
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ *
+ * SPDX-License-Identifier: MPL-2.0
+ */
+
+package org.eclipse.esmf.buildtime;
+
+import java.io.File;
+import java.net.URL;
+import java.nio.file.Path;
+
+public class DownloadTurtleGrammar extends BuildTimeTool {
+ private static final String GRAMMAR_JS_SHA1 = "6146fa24f0dd601a34025ef3618c3cd4d5dfdb4b";
+
+ public DownloadTurtleGrammar( final Path cacheLocation ) {
+ super( cacheLocation );
+ }
+
+ public void downloadTurtleGrammar() {
+ final URL url = url( "https://raw.githubusercontent.com/GordianDziwis/tree-sitter-turtle/refs/heads/main/grammar.js" );
+ final File grammarJs = getOrDownloadFile( url );
+ if ( !grammarJs.exists() ) {
+ throw new BuildTimeException( "Downloading Turtle grammar failed" );
+ }
+ final String sha1 = sha1( grammarJs );
+ if ( !sha1.equals( GRAMMAR_JS_SHA1 ) ) {
+ throw new BuildTimeException( "Invalid SHA1 sum for grammar.js. Expected: " + GRAMMAR_JS_SHA1 + ", found: " + sha1 );
+ }
+ }
+
+ static void main( final String[] args ) {
+ final Path cacheLocation = Path.of( args[0] );
+ final DownloadTurtleGrammar downloadTurtleGrammar = new DownloadTurtleGrammar( cacheLocation );
+ downloadTurtleGrammar.downloadTurtleGrammar();
+ }
+}
diff --git a/core/esmf-tree-sitter-turtle/src-buildtime/main/java/org/eclipse/esmf/buildtime/DownloadZig.java b/core/esmf-tree-sitter-turtle/src-buildtime/main/java/org/eclipse/esmf/buildtime/DownloadZig.java
new file mode 100644
index 000000000..f0832d849
--- /dev/null
+++ b/core/esmf-tree-sitter-turtle/src-buildtime/main/java/org/eclipse/esmf/buildtime/DownloadZig.java
@@ -0,0 +1,242 @@
+/*
+ * Copyright (c) 2026 Robert Bosch Manufacturing Solutions GmbH
+ *
+ * See the AUTHORS file(s) distributed with this work for additional
+ * information regarding authorship.
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ *
+ * SPDX-License-Identifier: MPL-2.0
+ */
+
+package org.eclipse.esmf.buildtime;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Optional;
+import java.util.stream.Collectors;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipInputStream;
+
+import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
+import org.apache.commons.compress.archivers.tar.TarArchiveInputStream;
+import org.apache.commons.compress.compressors.gzip.GzipCompressorInputStream;
+import org.apache.commons.compress.compressors.xz.XZCompressorInputStream;
+import org.apache.commons.io.FileUtils;
+import org.apache.commons.io.IOUtils;
+
+import org.eclipse.esmf.util.process.BinaryLauncher;
+import org.eclipse.esmf.util.process.ProcessLauncher;
+
+public class DownloadZig extends ZigContext {
+ private static final String ZIG_PUB_KEY = "RWSGOq2NVecA2UPNdBUZykf1CCb147pkmdtYxgb3Ti+JO/wCYvhbAb/U";
+ private final URL mirrorsListUrl;
+
+ private final String minisignVersion;
+
+ public DownloadZig( final Path cacheLocation, final String zigVersion, final String minisignVersion ) {
+ super( cacheLocation, zigVersion );
+ this.minisignVersion = minisignVersion;
+ mirrorsListUrl = url( "https://ziglang.org/download/community-mirrors.txt" );
+ }
+
+ private String zigReleaseFileName() {
+ return switch ( currentOs ) {
+ case WINDOWS -> "zig-x86_64-windows-%s.zip".formatted( zigVersion );
+ case MACOS, LINUX -> "zig-%s-%s-%s.tar.xz".formatted( currentArchitecture, currentOs, zigVersion );
+ };
+ }
+
+ private String minisignReleaseFileName() {
+ return switch ( currentOs ) {
+ case WINDOWS -> "minisign-%s-win64.zip".formatted( minisignVersion );
+ case MACOS -> "minisign-%s-macos.zip".formatted( minisignVersion );
+ case LINUX -> "minisign-%s-linux.tar.gz".formatted( minisignVersion );
+ };
+ }
+
+ private Path minisignExecutablePath() {
+ return switch ( currentOs ) {
+ case WINDOWS -> Path.of( "minisign-win64", "minisign.exe" );
+ case MACOS -> Path.of( "minisign" );
+ case LINUX -> Path.of( "minisign-linux", currentArchitecture.toString(), "minisign" );
+ };
+ }
+
+ private List zigMirrorUrls() {
+ final File mirrors = getOrDownloadFile( mirrorsListUrl );
+ try {
+ return Files.readAllLines( mirrors.toPath() ).stream().collect( Collectors.collectingAndThen( Collectors.toList(), collected -> {
+ Collections.shuffle( collected );
+ return collected;
+ } ) );
+ } catch ( final IOException exception ) {
+ throw new BuildTimeException( exception );
+ }
+ }
+
+ private File getOrDownloadFileFromMirror( final List mirrorUrls, final File outputFile ) {
+ if ( outputFile.exists() ) {
+ return outputFile;
+ }
+ final List triedLocations = new ArrayList<>();
+ for ( final String mirrorUrl : mirrorUrls ) {
+ try {
+ final URL url = url( mirrorUrl + "/" + outputFile.getName() );
+ triedLocations.add( url.toString() );
+ return getOrDownloadFile( url );
+ } catch ( final Exception exception ) {
+ // try the next mirror
+ }
+ }
+ throw new BuildTimeException( "Could not retrieve " + outputFile.getName() + " from any known mirror. Tried:"
+ + triedLocations.stream().map( url -> " - " + url )
+ .collect( Collectors.joining( "\n" ) ) );
+ }
+
+ private void extractZip( final File zipFile, final Path outputDir ) {
+ if ( outputDir.toFile().exists() ) {
+ return;
+ } else {
+ mkdir( outputDir );
+ }
+
+ try ( final ZipInputStream zipInputStream = new ZipInputStream( new FileInputStream( zipFile ) ) ) {
+ for ( ZipEntry entry = zipInputStream.getNextEntry(); entry != null; entry = zipInputStream.getNextEntry() ) {
+ final Path output = outputDir.resolve( entry.getName() );
+ if ( entry.isDirectory() ) {
+ mkdir( output );
+ } else {
+ try ( final FileOutputStream outputStream = new FileOutputStream( output.toFile() ) ) {
+ IOUtils.copy( zipInputStream, outputStream );
+ }
+ }
+ }
+ } catch ( final IOException exception ) {
+ throw new BuildTimeException( exception );
+ }
+ }
+
+ private void extractTar( final File tarFile, final Path outputDir ) {
+ if ( outputDir.toFile().exists() ) {
+ return;
+ } else {
+ mkdir( outputDir );
+ }
+
+ final String name = tarFile.getName().toLowerCase();
+ if ( !name.endsWith( ".tar.gz" ) && !name.endsWith( ".tar.xz" ) && !name.endsWith( ".tgz" ) ) {
+ throw new BuildTimeException( "Only .tar.gz, .tgz, and .tar.xz files are supported" );
+ }
+
+ try ( final FileInputStream fileInputStream = new FileInputStream( tarFile ) ) {
+ final InputStream compressorInputStream = name.endsWith( ".tar.xz" )
+ ? new XZCompressorInputStream( fileInputStream )
+ : new GzipCompressorInputStream( fileInputStream );
+ try ( final TarArchiveInputStream tarInputStream = new TarArchiveInputStream( compressorInputStream ) ) {
+ for ( TarArchiveEntry entry = tarInputStream.getNextEntry(); entry != null; entry = tarInputStream.getNextEntry() ) {
+ final Path output = outputDir.resolve( entry.getName() );
+ if ( entry.isDirectory() ) {
+ mkdir( output );
+ } else {
+ try ( final FileOutputStream outputStream = new FileOutputStream( output.toFile() ) ) {
+ IOUtils.copy( tarInputStream, outputStream );
+ }
+ }
+ }
+ }
+
+ } catch ( final IOException exception ) {
+ throw new BuildTimeException( exception );
+ }
+ }
+
+ private void extractArchive( final File archiveFile, final Path targetDirectory ) {
+ if ( archiveFile.getName().endsWith( ".zip" ) ) {
+ extractZip( archiveFile, targetDirectory );
+ } else if ( archiveFile.getName().endsWith( ".tar.xz" ) || archiveFile.getName().endsWith( ".tar.gz" )
+ || archiveFile.getName().endsWith( ".tgz" ) ) {
+ extractTar( archiveFile, targetDirectory );
+ } else {
+ throw new BuildTimeException( "Do not know how to extract archive: " + archiveFile );
+ }
+ }
+
+ private File downloadAndExtractMinisign() {
+ final Path minisignDir = cacheLocation().resolve( "minisign" );
+ final File minisignExe = minisignDir.resolve( minisignExecutablePath() ).toFile();
+ if ( minisignExe.exists() ) {
+ return minisignExe;
+ }
+ final File minisignArchive = getOrDownloadFile( url( "https://github.com/jedisct1/minisign/releases/download/" + minisignVersion
+ + "/" + minisignReleaseFileName() ) );
+ extractArchive( minisignArchive, minisignDir );
+ if ( currentOs == OperatingSystem.LINUX || currentOs == OperatingSystem.MACOS ) {
+ minisignExe.setExecutable( true );
+ }
+ try {
+ FileUtils.delete( minisignArchive );
+ } catch ( final IOException exception ) {
+ // ignore, since it's only the cache
+ }
+ return minisignExe;
+ }
+
+ private File downloadAndExtractZig( final File minisignExe ) {
+ final File zigExe = zigExe();
+ if ( zigExe.exists() ) {
+ return zigExe;
+ }
+
+ final List mirrors = zigMirrorUrls();
+ final String fileName = zigReleaseFileName();
+ final File signatureOutputFile = cacheLocation().resolve( fileName + ".minisig" ).toFile();
+ final File zigReleaseArchive = getOrDownloadFileFromMirror( mirrors, cacheLocation().resolve( fileName ).toFile() );
+ getOrDownloadFileFromMirror( mirrors, signatureOutputFile );
+ validateZigReleaseSignature( minisignExe, zigReleaseArchive );
+
+ if ( !zigDir().toFile().exists() ) {
+ extractArchive( zigReleaseArchive, zigDir() );
+ if ( currentOs == OperatingSystem.LINUX || currentOs == OperatingSystem.MACOS ) {
+ zigExe.setExecutable( true );
+ }
+ }
+ try {
+ FileUtils.delete( zigReleaseArchive );
+ FileUtils.delete( signatureOutputFile );
+ } catch ( final IOException exception ) {
+ // ignore, since it's only the cache
+ }
+ return zigExe;
+ }
+
+ private void validateZigReleaseSignature( final File minisignExe, final File zigReleaseArchive ) {
+ final ProcessLauncher.ExecutionResult minisignExecutionResult = new BinaryLauncher( minisignExe ).apply(
+ List.of( "-qVm", zigReleaseArchive.getAbsolutePath(), "-P", ZIG_PUB_KEY ), Optional.empty(),
+ zigReleaseArchive.getParentFile() );
+ if ( minisignExecutionResult.exitStatus() != 0 ) {
+ throw new BuildTimeException( "Signature of " + zigReleaseArchive + " does not match" );
+ }
+ }
+
+ static void main( final String[] args ) {
+ final Path cacheLocation = Path.of( args[0] );
+ final String zigVersion = args[1];
+ final String minisignVersion = args[2];
+
+ final DownloadZig downloadZig = new DownloadZig( cacheLocation, zigVersion, minisignVersion );
+ final File minisignExe = downloadZig.downloadAndExtractMinisign();
+ downloadZig.downloadAndExtractZig( minisignExe );
+ }
+}
diff --git a/core/esmf-tree-sitter-turtle/src-buildtime/main/java/org/eclipse/esmf/buildtime/GenerateParserTokenType.java b/core/esmf-tree-sitter-turtle/src-buildtime/main/java/org/eclipse/esmf/buildtime/GenerateParserTokenType.java
new file mode 100644
index 000000000..2553d2736
--- /dev/null
+++ b/core/esmf-tree-sitter-turtle/src-buildtime/main/java/org/eclipse/esmf/buildtime/GenerateParserTokenType.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright (c) 2026 Robert Bosch Manufacturing Solutions GmbH
+ *
+ * See the AUTHORS file(s) distributed with this work for additional
+ * information regarding authorship.
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ *
+ * SPDX-License-Identifier: MPL-2.0
+ */
+
+package org.eclipse.esmf.buildtime;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Path;
+import java.util.Collection;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.common.collect.Streams;
+
+public class GenerateParserTokenType extends BuildTimeTool {
+ private static final String CLASS_NAME = "ParserTokenType";
+
+ public void generate( final File nodeTypesJson, final Path srcGen ) throws IOException {
+ final Path packagePath = srcGen.resolve( "main" ).resolve( "java" )
+ .resolve( "org" ).resolve( "eclipse" ).resolve( "esmf" ).resolve( "treesitterturtle" );
+ mkdir( packagePath );
+
+ final ObjectMapper objectMapper = new ObjectMapper();
+ final Set tokens = getNodeTypes( objectMapper.readTree( nodeTypesJson ) )
+ .map( String::toLowerCase )
+ .collect( Collectors.toSet() );
+ final String declarations = javaConstantsForTokens( tokens ).entrySet().stream()
+ .sorted( Map.Entry.comparingByKey() )
+ .map( entry -> " public static final String %s = \"%s\";%n".formatted( entry.getKey(), entry.getValue() ) )
+ .collect( Collectors.joining() );
+ final String classContent = """
+ package org.eclipse.esmf.treesitterturtle;
+
+ import javax.annotation.processing.Generated;
+
+ @Generated( "org.eclipse.esmf.buildtime.GenerateParserTokenType" )
+ public class %s {
+ %s
+ }
+ """.formatted( CLASS_NAME, declarations );
+ write( packagePath.resolve( CLASS_NAME + ".java" ).toFile(), classContent );
+ }
+
+ private Map javaConstantsForTokens( final Collection tokens ) {
+ return tokens.stream().collect( Collectors.toMap( token -> {
+ if ( token.length() == 1 && !Character.isLetterOrDigit( token.charAt( 0 ) ) ) {
+ return "SYMBOL_" + Character.getName( token.charAt( 0 ) )
+ .replace( '-', '_' )
+ .replace( ' ', '_' ).toUpperCase();
+ }
+ return switch ( token ) {
+ case "\"\"" -> "SYMBOL_DOUBLE_QUOTE";
+ case "\"\"\"" -> "SYMBOL_TRIPLE_QUOTE";
+ case "''" -> "SYMBOL_DOUBLE_SINGLE_QUOTE";
+ case "'''" -> "SYMBOL_TRIPLE_SINGLE_QUOTE";
+ case "@base" -> "AT_BASE";
+ case "@prefix" -> "AT_PREFIX";
+ case "^^" -> "SYMBOL_DOUBLE_CARET";
+ case "_:" -> "BLANK_NODE_PREFIX";
+ default -> token.toUpperCase();
+ };
+ }, token -> switch ( token ) {
+ case "\"" -> "\\\"";
+ case "\"\"" -> "\\\"\\\"";
+ case "\"\"\"" -> "\\\"\\\"\\\"";
+ default -> token;
+ } ) );
+ }
+
+ private Stream getNodeTypes( final JsonNode node ) {
+ return node.isArray()
+ ? Streams.stream( node.elements() ).flatMap( this::getNodeTypes )
+ : Optional.ofNullable( node.get( "type" ) ).map( JsonNode::asText ).stream();
+ }
+
+ static void main( final String[] args ) {
+ try {
+ new GenerateParserTokenType().generate( new File( args[0] ), Path.of( args[1] ) );
+ } catch ( final Exception exception ) {
+ throw new BuildTimeException( exception );
+ }
+ }
+
+}
diff --git a/core/esmf-tree-sitter-turtle/src-buildtime/main/java/org/eclipse/esmf/buildtime/NativeCompile.java b/core/esmf-tree-sitter-turtle/src-buildtime/main/java/org/eclipse/esmf/buildtime/NativeCompile.java
new file mode 100644
index 000000000..39bf7795b
--- /dev/null
+++ b/core/esmf-tree-sitter-turtle/src-buildtime/main/java/org/eclipse/esmf/buildtime/NativeCompile.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright (c) 2026 Robert Bosch Manufacturing Solutions GmbH
+ *
+ * See the AUTHORS file(s) distributed with this work for additional
+ * information regarding authorship.
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ *
+ * SPDX-License-Identifier: MPL-2.0
+ */
+
+package org.eclipse.esmf.buildtime;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+import java.util.stream.Stream;
+
+import org.eclipse.esmf.util.process.BinaryLauncher;
+import org.eclipse.esmf.util.process.ProcessLauncher;
+
+public class NativeCompile extends ZigContext {
+ private static final String LIB_NAME = "tree-sitter-turtle";
+
+ private final Path libOutputDirectory;
+ private final Path nativeSourcesDirectory;
+
+ public NativeCompile( final Path cacheLocation, final String zigVersion, final Path libOutputDirectory,
+ final Path nativeSourcesDirectory ) {
+ super( cacheLocation, zigVersion );
+ this.libOutputDirectory = libOutputDirectory;
+ this.nativeSourcesDirectory = nativeSourcesDirectory;
+ }
+
+ private record Target(
+ OperatingSystem operatingSystem,
+ Architecture architecture
+ ) {
+ public String getTargetName() {
+ return operatingSystem == OperatingSystem.LINUX
+ ? "%s-linux-gnu".formatted( architecture.toString() )
+ : "%s-%s".formatted( architecture().toString(), operatingSystem().toString() );
+ }
+ }
+
+ private List getTargets() {
+ return List.of(
+ new Target( OperatingSystem.WINDOWS, Architecture.X86_64 ),
+ new Target( OperatingSystem.MACOS, Architecture.X86_64 ),
+ new Target( OperatingSystem.MACOS, Architecture.AARCH64 ),
+ new Target( OperatingSystem.LINUX, Architecture.X86_64 ),
+ new Target( OperatingSystem.LINUX, Architecture.AARCH64 ) );
+ }
+
+ private String libNameForTarget( final Target target ) {
+ return switch ( target.operatingSystem() ) {
+ case WINDOWS -> "%s-%s.dll".formatted( target.getTargetName(), LIB_NAME );
+ case LINUX -> "%s-%s.so".formatted( target.getTargetName(), LIB_NAME );
+ case MACOS -> "%s-%s.dylib".formatted( target.getTargetName(), LIB_NAME );
+ };
+ }
+
+ private void runZig() {
+ final File zigExe = zigExe();
+
+ for ( final Target target : getTargets() ) {
+ final List args = new ArrayList<>();
+ final File libFilename = libOutputDirectory.resolve( libNameForTarget( target ) ).toFile();
+
+ if ( libFilename.exists() ) {
+ continue;
+ }
+ args.add( "c++" );
+ args.add( "-g0" );
+ args.add( "-fno-sanitize=undefined" );
+ args.add( "-fdeclspec" );
+ args.add( "-shared" );
+ args.add( "-target" );
+ args.add( target.getTargetName() );
+ args.add( "-I" );
+ args.add( zigDir().resolve( "lib" ).resolve( "include" ).toString() );
+ args.add( "-I" );
+ args.add( nativeSourcesDirectory.toString() );
+ args.add( "-I" );
+ args.add( nativeSourcesDirectory.resolve( "include" ).toString() );
+ args.add( "-I" );
+ args.add( nativeSourcesDirectory.resolve( "include" ).resolve( currentOs.toString() ).toString() );
+ args.add( "-o" );
+ args.add( libFilename.getAbsolutePath() );
+ try ( final Stream subpaths = Files.list( nativeSourcesDirectory ) ) {
+ subpaths.filter( sourceFile -> sourceFile.toFile().isFile() )
+ .filter( sourceFile -> sourceFile.getFileName().toString().endsWith( ".c" ) )
+ .forEach( sourceFile -> args.add( sourceFile.toString() ) );
+ } catch ( final IOException exception ) {
+ throw new BuildTimeException( exception );
+ }
+
+ System.out.println( zigExe().getAbsolutePath() + " " + String.join( " ", args ) );
+ mkdir( libOutputDirectory );
+ final ProcessLauncher.ExecutionResult zigExecutionResult =
+ new BinaryLauncher( zigExe ).apply( args, Optional.empty(), libOutputDirectory.toFile() );
+ if ( zigExecutionResult.exitStatus() != 0 ) {
+ System.err.println( zigExecutionResult.stderr() );
+ throw new BuildTimeException( "Compilation of native lib failed" );
+ }
+ System.out.println( "Native lib compiled: " + libFilename.getAbsolutePath() );
+ }
+ }
+
+ static void main( final String[] args ) {
+ final Path cacheLocation = Path.of( args[0] );
+ final String zigVersion = args[1];
+ final Path libOutputDirectory = Path.of( args[2] );
+ final Path nativeSourcesDirectory = Path.of( args[3] );
+ final NativeCompile nativeCompile = new NativeCompile( cacheLocation, zigVersion, libOutputDirectory, nativeSourcesDirectory );
+ nativeCompile.runZig();
+ }
+}
diff --git a/core/esmf-tree-sitter-turtle/src-buildtime/main/java/org/eclipse/esmf/buildtime/ZigContext.java b/core/esmf-tree-sitter-turtle/src-buildtime/main/java/org/eclipse/esmf/buildtime/ZigContext.java
new file mode 100644
index 000000000..77bd155ab
--- /dev/null
+++ b/core/esmf-tree-sitter-turtle/src-buildtime/main/java/org/eclipse/esmf/buildtime/ZigContext.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright (c) 2026 Robert Bosch Manufacturing Solutions GmbH
+ *
+ * See the AUTHORS file(s) distributed with this work for additional
+ * information regarding authorship.
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ *
+ * SPDX-License-Identifier: MPL-2.0
+ */
+
+package org.eclipse.esmf.buildtime;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+import org.apache.commons.lang3.ArchUtils;
+import org.apache.commons.lang3.SystemUtils;
+import org.apache.commons.lang3.arch.Processor;
+
+public class ZigContext extends BuildTimeTool {
+ protected final OperatingSystem currentOs;
+ protected final Architecture currentArchitecture;
+ protected final String zigVersion;
+
+ protected enum OperatingSystem {
+ WINDOWS, MACOS, LINUX;
+
+ @Override
+ public String toString() {
+ return super.toString().toLowerCase();
+ }
+ }
+
+ protected enum Architecture {
+ X86_64, AARCH64;
+
+ @Override
+ public String toString() {
+ return super.toString().toLowerCase();
+ }
+ }
+
+ public ZigContext( final Path cacheLocation, final String zigVersion ) {
+ super( cacheLocation );
+ this.zigVersion = zigVersion;
+
+ final Processor processor = ArchUtils.getProcessor();
+ if ( processor.isX86() && processor.is64Bit() ) {
+ currentArchitecture = Architecture.X86_64;
+ } else if ( processor.isAarch64() && processor.is64Bit() ) {
+ currentArchitecture = Architecture.AARCH64;
+ } else {
+ throw new BuildTimeException( "Unsupported architecture: " + processor.getType().getLabel()
+ + "/" + processor.getArch().getLabel() );
+ }
+
+ if ( SystemUtils.IS_OS_WINDOWS ) {
+ currentOs = OperatingSystem.WINDOWS;
+ if ( !processor.isX86() ) {
+ throw new BuildTimeException( "Unsupported architecture: " + processor.getType().getLabel()
+ + "/" + processor.getArch().getLabel() );
+ }
+ } else if ( SystemUtils.IS_OS_MAC ) {
+ currentOs = OperatingSystem.MACOS;
+ } else if ( SystemUtils.IS_OS_LINUX ) {
+ currentOs = OperatingSystem.LINUX;
+ } else {
+ throw new BuildTimeException( "Unsupported operating system: " + SystemUtils.OS_NAME );
+ }
+
+ try {
+ Files.createDirectories( cacheLocation );
+ } catch ( final IOException exception ) {
+ throw new BuildTimeException( "Could not create cache directory: " + cacheLocation );
+ }
+ }
+
+ protected Path zigDir() {
+ return cacheLocation().resolve( "zig" );
+ }
+
+ protected File zigExe() {
+ final Path zigExecutablePath = switch ( currentOs ) {
+ case WINDOWS -> Path.of( "zig-x86_64-windows-" + zigVersion, "zig.exe" );
+ case MACOS, LINUX -> Path.of( "zig-%s-%s-%s".formatted( currentArchitecture, currentOs, zigVersion ), "zig" );
+ };
+ return zigDir().resolve( zigExecutablePath ).toFile();
+ }
+}
diff --git a/core/esmf-tree-sitter-turtle/src/main/c/org_eclipse_esmf_treesitterturtle_TreeSitterTurtle.c b/core/esmf-tree-sitter-turtle/src/main/c/org_eclipse_esmf_treesitterturtle_TreeSitterTurtle.c
new file mode 100644
index 000000000..70ccff3d1
--- /dev/null
+++ b/core/esmf-tree-sitter-turtle/src/main/c/org_eclipse_esmf_treesitterturtle_TreeSitterTurtle.c
@@ -0,0 +1,24 @@
+/*
+ * Copyright (c) 2026 Robert Bosch Manufacturing Solutions GmbH
+ *
+ * See the AUTHORS file(s) distributed with this work for additional
+ * information regarding authorship.
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ *
+ * SPDX-License-Identifier: MPL-2.0
+ */
+
+#include
+void *tree_sitter_turtle();
+/*
+ * Class: org_treesitter_TreeSitterTurtle
+ * Method: tree_sitter_turtle
+ * Signature: ()J
+ */
+JNIEXPORT jlong JNICALL Java_org_eclipse_esmf_treesitterturtle_TreeSitterTurtle_tree_1sitter_1turtle
+ (JNIEnv *env, jclass clz){
+ return (jlong) tree_sitter_turtle();
+}
diff --git a/core/esmf-tree-sitter-turtle/src/main/java/org/eclipse/esmf/treesitterturtle/TreeSitterTurtle.java b/core/esmf-tree-sitter-turtle/src/main/java/org/eclipse/esmf/treesitterturtle/TreeSitterTurtle.java
new file mode 100644
index 000000000..fb861059e
--- /dev/null
+++ b/core/esmf-tree-sitter-turtle/src/main/java/org/eclipse/esmf/treesitterturtle/TreeSitterTurtle.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (c) 2026 Robert Bosch Manufacturing Solutions GmbH
+ *
+ * See the AUTHORS file(s) distributed with this work for additional
+ * information regarding authorship.
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ *
+ * SPDX-License-Identifier: MPL-2.0
+ */
+
+package org.eclipse.esmf.treesitterturtle;
+
+import org.treesitter.TSLanguage;
+import org.treesitter.TSParser;
+import org.treesitter.utils.NativeUtils;
+
+/**
+ * Language definition for RDF/Turtle to be used with {@link TSParser}:
+ * {@code
+ * TSParser parser = new TSParser();
+ * parser.setLanguage( new TreeSitterTurtle() );
+ * }
+ */
+public class TreeSitterTurtle extends TSLanguage {
+ static {
+ NativeUtils.loadLib( "lib/tree-sitter-turtle" );
+ }
+
+ private static native long tree_sitter_turtle();
+
+ public TreeSitterTurtle() {
+ super( tree_sitter_turtle() );
+ }
+
+ private TreeSitterTurtle( final long ptr ) {
+ super( ptr );
+ }
+
+ @Override
+ public TSLanguage copy() {
+ return new TreeSitterTurtle( copyPtr() );
+ }
+}
diff --git a/core/esmf-tree-sitter-turtle/src/main/java/org/eclipse/esmf/treesitterturtle/TreeSitterUtil.java b/core/esmf-tree-sitter-turtle/src/main/java/org/eclipse/esmf/treesitterturtle/TreeSitterUtil.java
new file mode 100644
index 000000000..14f47e26f
--- /dev/null
+++ b/core/esmf-tree-sitter-turtle/src/main/java/org/eclipse/esmf/treesitterturtle/TreeSitterUtil.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright (c) 2026 Robert Bosch Manufacturing Solutions GmbH
+ *
+ * See the AUTHORS file(s) distributed with this work for additional
+ * information regarding authorship.
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ *
+ * SPDX-License-Identifier: MPL-2.0
+ */
+
+package org.eclipse.esmf.treesitterturtle;
+
+import org.jspecify.annotations.Nullable;
+import org.treesitter.TSNode;
+import org.treesitter.TSTree;
+
+public class TreeSitterUtil {
+ private TreeSitterUtil() {}
+
+ public static String print( final TSTree tree ) {
+ return print( tree.getRootNode() );
+ }
+
+ public static String print( final TSNode node ) {
+ final StringBuilder builder = new StringBuilder();
+ print( node, builder, 0, null );
+ return builder.toString();
+ }
+
+ public static String print( final TSTree tree, final TurtleSyntaxTree.TokenProvider tokenProvider ) {
+ return print( tree.getRootNode(), tokenProvider );
+ }
+
+ public static String print( final TSNode node, final TurtleSyntaxTree.TokenProvider tokenProvider ) {
+ final StringBuilder builder = new StringBuilder();
+ print( node, builder, 0, tokenProvider );
+ return builder.toString();
+ }
+
+ private static void print( final TSNode node, final StringBuilder builder, final int indentLevel,
+ final TurtleSyntaxTree.@Nullable TokenProvider tokenProvider ) {
+ builder.repeat( " ", indentLevel );
+ builder.append( "- '" );
+ builder.append( node.getType() );
+ builder.append( "'" );
+ if ( node.hasError() ) {
+ builder.append( " (ERROR)" );
+ } else if ( tokenProvider != null && node.getStartPoint().getRow() == node.getEndPoint().getRow() ) {
+ final TurtleSyntaxTree.Location location = new TurtleSyntaxTree.Location(
+ node.getStartPoint().getRow(),
+ node.getStartPoint().getColumn(),
+ node.getEndPoint().getRow(),
+ node.getEndPoint().getColumn() );
+ final String nodeContent = tokenProvider.apply( location );
+ if ( !nodeContent.equals( node.getType() ) ) {
+ builder.append( " (" );
+ builder.append( nodeContent );
+ builder.append( ")" );
+ }
+ }
+ builder.append( "\n" );
+ for ( int i = 0; i < node.getChildCount(); i++ ) {
+ print( node.getChild( i ), builder, indentLevel + 1, tokenProvider );
+ }
+ }
+}
diff --git a/core/esmf-tree-sitter-turtle/src/main/java/org/eclipse/esmf/treesitterturtle/TurtleSyntaxTree.java b/core/esmf-tree-sitter-turtle/src/main/java/org/eclipse/esmf/treesitterturtle/TurtleSyntaxTree.java
new file mode 100644
index 000000000..66649ad48
--- /dev/null
+++ b/core/esmf-tree-sitter-turtle/src/main/java/org/eclipse/esmf/treesitterturtle/TurtleSyntaxTree.java
@@ -0,0 +1,184 @@
+/*
+ * Copyright (c) 2026 Robert Bosch Manufacturing Solutions GmbH
+ *
+ * See the AUTHORS file(s) distributed with this work for additional
+ * information regarding authorship.
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ *
+ * SPDX-License-Identifier: MPL-2.0
+ */
+
+package org.eclipse.esmf.treesitterturtle;
+
+import java.util.Comparator;
+import java.util.List;
+import java.util.Objects;
+import java.util.function.Function;
+import java.util.function.Supplier;
+import java.util.stream.IntStream;
+import java.util.stream.Stream;
+
+import org.treesitter.TSNode;
+import org.treesitter.TSTree;
+
+/**
+ * Represents the concrete syntax tree of a turtle document
+ */
+public class TurtleSyntaxTree {
+ private final Supplier sourceRepresentationSupplier;
+ private final Node rootNode;
+
+ public sealed interface Node {
+ String type();
+
+ Location location();
+
+ default boolean isError() {
+ return false;
+ }
+
+ default List children() {
+ return List.of();
+ }
+ }
+
+ /**
+ * Represents one node in the concrete syntax tree in the source document
+ *
+ * @param type the type of token, expected to be one of the constants in {@link ParserTokenType}
+ * @param content the actual content of the token
+ * @param location the location in the source document
+ * @param children children of the node
+ */
+ public record Token(
+ String type,
+ String content,
+ Location location,
+ List children
+ ) implements Node {}
+
+ public record Error(
+ String type,
+ Location location,
+ boolean isMissing,
+ boolean isExtra
+ ) implements Node {
+ @Override
+ public boolean isError() {
+ return true;
+ }
+ }
+
+ public record Location(
+ int fromLine,
+ int fromColumn,
+ int toLine,
+ int toColumn
+ ) {}
+
+ /**
+ * Provides the token (substring) for a given location
+ */
+ public interface TokenProvider extends Function {
+ }
+
+ /**
+ * TokenProvider implementation for an input document given as a string
+ */
+ public static class StringTokenProvider implements TokenProvider {
+ private final String sourceDocument;
+
+ public StringTokenProvider( final String sourceDocument ) {
+ this.sourceDocument = sourceDocument;
+ }
+
+ @Override
+ public String apply( final Location location ) {
+ if ( sourceDocument == null || sourceDocument.isEmpty() ) {
+ return "";
+ }
+
+ int startIndex = 0;
+ int currentLine = 0;
+ for ( int i = 0; i < sourceDocument.length() && currentLine < location.fromLine(); i++ ) {
+ if ( sourceDocument.charAt( i ) == '\n' ) {
+ currentLine++;
+ }
+ startIndex = i + 1;
+ }
+ startIndex += location.fromColumn();
+ int endIndex = 0;
+ currentLine = 0;
+
+ for ( int i = 0; i < sourceDocument.length() && currentLine < location.toLine(); i++ ) {
+ if ( sourceDocument.charAt( i ) == '\n' ) {
+ currentLine++;
+ }
+ endIndex = i + 1;
+ }
+ endIndex += location.toColumn();
+ startIndex = Math.clamp( startIndex, 0, sourceDocument.length() );
+ endIndex = Math.clamp( endIndex, 0, sourceDocument.length() );
+
+ if ( startIndex > endIndex ) {
+ return "";
+ }
+
+ return sourceDocument.substring( startIndex, endIndex );
+ }
+ }
+
+ private TurtleSyntaxTree( final Node rootNode, final Supplier sourceRepresentationSupplier ) {
+ this.rootNode = rootNode;
+ this.sourceRepresentationSupplier = sourceRepresentationSupplier;
+ }
+
+ public Node rootNode() {
+ return rootNode;
+ }
+
+ public Supplier sourceRepresentationSupplier() {
+ return sourceRepresentationSupplier;
+ }
+
+ public static TurtleSyntaxTree fromConcreteSyntaxTree( final TSTree syntaxTree, final Supplier sourceRepresentationSupplier,
+ final TokenProvider tokenProvider ) {
+ return new TurtleSyntaxTree( nodeForTsNode( syntaxTree.getRootNode(), tokenProvider ), sourceRepresentationSupplier );
+ }
+
+ private static Node nodeForTsNode( final TSNode inputNode, final TokenProvider tokenProvider ) {
+ final Location location = new Location(
+ inputNode.getStartPoint().getRow(),
+ inputNode.getStartPoint().getColumn(),
+ inputNode.getEndPoint().getRow(),
+ inputNode.getEndPoint().getColumn() );
+ if ( inputNode.isError() ) {
+ return new Error( inputNode.getType(), location, inputNode.isMissing(), inputNode.isExtra() );
+ }
+ final String token = tokenProvider.apply( location );
+ final List children = IntStream.range( 0, inputNode.getChildCount() )
+ .mapToObj( inputNode::getChild )
+ .filter( Objects::nonNull )
+ .map( child -> nodeForTsNode( child, tokenProvider ) )
+ .toList();
+ return new Token( inputNode.getType(), token, location, children );
+ }
+
+ private Stream nodes( final Node fromNode ) {
+ final Stream children = fromNode.children().stream().flatMap( this::nodes );
+ return Stream.concat( Stream.of( fromNode ), children )
+ .sorted( Comparator.comparingInt( node -> node.location().fromLine() )
+ .thenComparingInt( node -> node.location().fromColumn() ) );
+ }
+
+ public Stream nodes() {
+ return nodes( rootNode() );
+ }
+
+ public Stream tokens() {
+ return nodes().filter( Token.class::isInstance ).map( Token.class::cast );
+ }
+}
diff --git a/core/esmf-tree-sitter-turtle/src/main/js/tree-sitter.json b/core/esmf-tree-sitter-turtle/src/main/js/tree-sitter.json
new file mode 100644
index 000000000..4b6137bde
--- /dev/null
+++ b/core/esmf-tree-sitter-turtle/src/main/js/tree-sitter.json
@@ -0,0 +1,37 @@
+{
+ "$schema": "https://tree-sitter.github.io/tree-sitter/assets/schemas/config.schema.json",
+ "grammars": [
+ {
+ "name": "turtle",
+ "camelcase": "Turtle",
+ "title": "RDF/Turtle Parser",
+ "scope": "source.turtle",
+ "file-types": [
+ "ttl"
+ ],
+ "injection-regex": "^turtle$",
+ "class-name": "TreeSitterTurtle"
+ }
+ ],
+ "metadata": {
+ "version": "0.0.0",
+ "license": "EPL-2.0",
+ "description": "Turtle grammar for tree-sitter",
+ "authors": [
+ {
+ "name": "Eclipse Semantic Modeling Framework Team",
+ "email": "esmf-dev@eclipse.org",
+ "url": "https://eclipse-esmf.github.io/"
+ }
+ ]
+ },
+ "bindings": {
+ "c": false,
+ "go": false,
+ "node": false,
+ "python": false,
+ "rust": false,
+ "swift": false,
+ "zig": false
+ }
+}
diff --git a/core/esmf-tree-sitter-turtle/src/test/java/org/eclipse/esmf/treesitterturtle/TreeSitterTurtleTest.java b/core/esmf-tree-sitter-turtle/src/test/java/org/eclipse/esmf/treesitterturtle/TreeSitterTurtleTest.java
new file mode 100644
index 000000000..64dd88861
--- /dev/null
+++ b/core/esmf-tree-sitter-turtle/src/test/java/org/eclipse/esmf/treesitterturtle/TreeSitterTurtleTest.java
@@ -0,0 +1,389 @@
+/*
+ * Copyright (c) 2026 Robert Bosch Manufacturing Solutions GmbH
+ *
+ * See the AUTHORS file(s) distributed with this work for additional
+ * information regarding authorship.
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ *
+ * SPDX-License-Identifier: MPL-2.0
+ */
+
+package org.eclipse.esmf.treesitterturtle;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.treesitter.TSLanguage;
+import org.treesitter.TSNode;
+import org.treesitter.TSParser;
+import org.treesitter.TSTree;
+
+@SuppressWarnings( "HttpUrlsUsage" )
+public class TreeSitterTurtleTest {
+ private TSParser parser;
+
+ @BeforeEach
+ void setUp() {
+ parser = new TSParser();
+ final TSLanguage turtle = new TreeSitterTurtle();
+ parser.setLanguage( turtle );
+ }
+
+ @Test
+ void testBasicPrefixAndTriple() {
+ final String content = """
+ @prefix : .
+ :a a :b .
+ """;
+ final TSTree tree = parser.parseString( null, content );
+ final TSNode rootNode = tree.getRootNode();
+ assertThat( TreeSitterUtil.print( rootNode, new TurtleSyntaxTree.StringTokenProvider( content ) ) ).doesNotContain( "ERROR" );
+ assertThat( rootNode.hasError() ).isFalse();
+ assertThat( rootNode.getChild( 0 ).getChild( 0 ).getChild( 0 ).getGrammarType() ).isEqualTo( "@prefix" );
+ }
+
+ @Test
+ void testNumericLiterals() {
+ final String content = """
+ @prefix ex: .
+
+ ex:entity ex:intValue 42 .
+ ex:entity ex:decimalValue 3.14 .
+ ex:entity ex:doubleValue 1.23e10 .
+ ex:entity ex:negativeInt -100 .
+ ex:entity ex:positiveDouble +2.5 .
+ """;
+
+ final TSTree tree = parser.parseString( null, content );
+ final TSNode rootNode = tree.getRootNode();
+
+ assertThat( rootNode.hasError() ).isFalse();
+ assertThat( TreeSitterUtil.print( rootNode, new TurtleSyntaxTree.StringTokenProvider( content ) ) ).doesNotContain( "ERROR" );
+
+ // Verify the document parses successfully with numeric literals
+ final String treeString = TreeSitterUtil.print( rootNode, new TurtleSyntaxTree.StringTokenProvider( content ) );
+ assertThat( treeString ).contains( "42" );
+ assertThat( treeString ).contains( "3.14" );
+ assertThat( treeString ).contains( "1.23e10" );
+ }
+
+ @Test
+ void testStringLiterals() {
+ final String content = """
+ @prefix ex: .
+
+ ex:entity ex:simpleString "Hello World" .
+ ex:entity ex:stringWithEscapes "Line 1\\nLine 2\\tTabbed" .
+ ex:entity ex:multilineString \"\"\"This is a
+ multiline
+ string literal\"\"\" .
+ ex:entity ex:singleQuote 'Single quoted string' .
+ """;
+
+ final TSTree tree = parser.parseString( null, content );
+ final TSNode rootNode = tree.getRootNode();
+
+ assertThat( rootNode.hasError() ).isFalse();
+ assertThat( TreeSitterUtil.print( rootNode, new TurtleSyntaxTree.StringTokenProvider( content ) ) ).doesNotContain( "ERROR" );
+ }
+
+ @Test
+ void testBooleanLiterals() {
+ final String content = """
+ @prefix ex: .
+
+ ex:entity ex:isTrue true .
+ ex:entity ex:isFalse false .
+ """;
+
+ final TSTree tree = parser.parseString( null, content );
+ final TSNode rootNode = tree.getRootNode();
+
+ assertThat( rootNode.hasError() ).isFalse();
+ assertThat( TreeSitterUtil.print( rootNode, new TurtleSyntaxTree.StringTokenProvider( content ) ) ).doesNotContain( "ERROR" );
+ assertThat( TreeSitterUtil.print( rootNode, new TurtleSyntaxTree.StringTokenProvider( content ) ) ).contains( "true" );
+ assertThat( TreeSitterUtil.print( rootNode, new TurtleSyntaxTree.StringTokenProvider( content ) ) ).contains( "false" );
+ }
+
+ @Test
+ void testLanguageTaggedLiterals() {
+ final String content = """
+ @prefix ex: .
+ @prefix rdfs: .
+
+ ex:entity rdfs:label "Hello"@en .
+ ex:entity rdfs:label "Hallo"@de .
+ ex:entity rdfs:label "Bonjour"@fr .
+ ex:entity rdfs:label "こんにちは"@ja .
+ ex:entity rdfs:comment "Multi-word label"@en-US .
+ """;
+
+ final TSTree tree = parser.parseString( null, content );
+ final TSNode rootNode = tree.getRootNode();
+
+ assertThat( rootNode.hasError() ).isFalse();
+ assertThat( TreeSitterUtil.print( rootNode, new TurtleSyntaxTree.StringTokenProvider( content ) ) ).doesNotContain( "ERROR" );
+ assertThat( TreeSitterUtil.print( rootNode, new TurtleSyntaxTree.StringTokenProvider( content ) ) ).contains( "@en" );
+ assertThat( TreeSitterUtil.print( rootNode, new TurtleSyntaxTree.StringTokenProvider( content ) ) ).contains( "@de" );
+ assertThat( TreeSitterUtil.print( rootNode, new TurtleSyntaxTree.StringTokenProvider( content ) ) ).contains( "@fr" );
+ }
+
+ @Test
+ void testTypedLiterals() {
+ final String content = """
+ @prefix ex: .
+ @prefix xsd: .
+
+ ex:entity ex:dateValue "2026-04-30"^^xsd:date .
+ ex:entity ex:intValue "42"^^xsd:integer .
+ ex:entity ex:boolValue "true"^^xsd:boolean .
+ ex:entity ex:customType "custom value"^^ex:MyType .
+ ex:entity ex:fullUri "value"^^ .
+ """;
+
+ final TSTree tree = parser.parseString( null, content );
+ final TSNode rootNode = tree.getRootNode();
+
+ assertThat( rootNode.hasError() ).isFalse();
+ assertThat( TreeSitterUtil.print( rootNode, new TurtleSyntaxTree.StringTokenProvider( content ) ) ).doesNotContain( "ERROR" );
+ assertThat( TreeSitterUtil.print( rootNode, new TurtleSyntaxTree.StringTokenProvider( content ) ) ).contains( "^^" );
+ }
+
+ @Test
+ void testRdfListSyntax() {
+ final String content = """
+ @prefix ex: .
+ @prefix rdf: .
+
+ ex:entity ex:emptyList () .
+ ex:entity ex:numberList (1 2 3 4 5) .
+ ex:entity ex:stringList ("a" "b" "c") .
+ ex:entity ex:mixedList (1 "two" 3.0 true ex:resource) .
+ ex:entity ex:nestedList (1 (2 3) 4) .
+ """;
+
+ final TSTree tree = parser.parseString( null, content );
+ final TSNode rootNode = tree.getRootNode();
+
+ assertThat( rootNode.hasError() ).isFalse();
+ assertThat( TreeSitterUtil.print( rootNode, new TurtleSyntaxTree.StringTokenProvider( content ) ) ).doesNotContain( "ERROR" );
+ assertThat( TreeSitterUtil.print( rootNode, new TurtleSyntaxTree.StringTokenProvider( content ) ) ).contains( "(" );
+ assertThat( TreeSitterUtil.print( rootNode, new TurtleSyntaxTree.StringTokenProvider( content ) ) ).contains( ")" );
+ }
+
+ @Test
+ void testAnonymousNodes() {
+ final String content = """
+ @prefix ex: .
+
+ ex:entity ex:hasBlankNode [
+ ex:property1 "value1" ;
+ ex:property2 "value2"
+ ] .
+
+ ex:entity ex:simpleBlank [ ex:prop "val" ] .
+
+ ex:entity ex:nestedBlank [
+ ex:inner [
+ ex:nested "deeply"
+ ]
+ ] .
+
+ # Blank node with multiple predicates and objects
+ ex:person
+ ex:firstName "John" ;
+ ex:lastName "Doe" ;
+ ex:age 30 ;
+ ex:knows [
+ ex:firstName "Jane" ;
+ ex:lastName "Smith"
+ ] .
+ """;
+
+ final TSTree tree = parser.parseString( null, content );
+ final TSNode rootNode = tree.getRootNode();
+
+ assertThat( rootNode.hasError() ).isFalse();
+ assertThat( TreeSitterUtil.print( rootNode, new TurtleSyntaxTree.StringTokenProvider( content ) ) ).doesNotContain( "ERROR" );
+ assertThat( TreeSitterUtil.print( rootNode, new TurtleSyntaxTree.StringTokenProvider( content ) ) ).contains( "[" );
+ assertThat( TreeSitterUtil.print( rootNode, new TurtleSyntaxTree.StringTokenProvider( content ) ) ).contains( "]" );
+ }
+
+ @Test
+ void testComplexDocument() {
+ final String content = """
+ @prefix ex: .
+ @prefix rdf: .
+ @prefix xsd: .
+ @base .
+
+ ex:Person a ex:Class ;
+ ex:name "Person"@en ;
+ ex:properties (
+ ex:firstName
+ ex:lastName
+ ex:age
+ ) .
+
+ ex:john a ex:Person ;
+ ex:firstName "John" ;
+ ex:lastName "Doe" ;
+ ex:age "30"^^xsd:integer ;
+ ex:active true ;
+ ex:salary 50000.50 ;
+ ex:address [
+ ex:street "123 Main St" ;
+ ex:city "Anytown" ;
+ ex:country "USA"@en
+ ] ;
+ ex:hobbies ("reading" "coding" "music") .
+ """;
+
+ final TSTree tree = parser.parseString( null, content );
+ final TSNode rootNode = tree.getRootNode();
+
+ assertThat( rootNode.hasError() ).isFalse();
+ assertThat( TreeSitterUtil.print( rootNode, new TurtleSyntaxTree.StringTokenProvider( content ) ) ).doesNotContain( "ERROR" );
+ }
+
+ @Test
+ void testBrokenSyntaxMissingDot() {
+ final String content = """
+ @prefix ex:
+
+ ex:entity ex:property "value" .
+ """;
+
+ final TSTree tree = parser.parseString( null, content );
+ final TSNode rootNode = tree.getRootNode();
+
+ // Should have errors due to missing dot after prefix declaration
+ assertThat( rootNode.hasError() ).isTrue();
+ assertThat( TreeSitterUtil.print( rootNode, new TurtleSyntaxTree.StringTokenProvider( content ) ) ).contains( "ERROR" );
+ }
+
+ @Test
+ void testBrokenSyntaxInvalidUri() {
+ final String content = """
+ @prefix ex: .
+
+ ex:entity ex:property .
+ """;
+
+ final TSTree tree = parser.parseString( null, content );
+ final TSNode rootNode = tree.getRootNode();
+
+ // Should have errors due to invalid URI
+ assertThat( rootNode.hasError() ).isTrue();
+ assertThat( TreeSitterUtil.print( rootNode, new TurtleSyntaxTree.StringTokenProvider( content ) ) ).contains( "ERROR" );
+ }
+
+ @Test
+ void testBrokenSyntaxUnterminatedString() {
+ final String content = """
+ @prefix ex: .
+
+ ex:entity ex:property "unterminated string .
+ """;
+
+ final TSTree tree = parser.parseString( null, content );
+ final TSNode rootNode = tree.getRootNode();
+
+ // Should have errors due to unterminated string
+ assertThat( rootNode.hasError() ).isTrue();
+ assertThat( TreeSitterUtil.print( rootNode, new TurtleSyntaxTree.StringTokenProvider( content ) ) ).contains( "ERROR" );
+ }
+
+ @Test
+ void testBrokenSyntaxMismatchedBrackets() {
+ final String content = """
+ @prefix ex: .
+
+ ex:entity ex:property [
+ ex:nested "value"
+ .
+ """;
+
+ final TSTree tree = parser.parseString( null, content );
+ final TSNode rootNode = tree.getRootNode();
+
+ // Should have errors due to mismatched brackets
+ assertThat( rootNode.hasError() ).isTrue();
+ assertThat( TreeSitterUtil.print( rootNode, new TurtleSyntaxTree.StringTokenProvider( content ) ) ).contains( "ERROR" );
+ }
+
+ @Test
+ void testBrokenSyntaxInvalidPrefix() {
+ final String content = """
+ @prefix 123invalid: .
+
+ 123invalid:entity ex:property "value" .
+ """;
+
+ final TSTree tree = parser.parseString( null, content );
+ final TSNode rootNode = tree.getRootNode();
+
+ // Should have errors due to invalid prefix name (starts with number)
+ assertThat( rootNode.hasError() ).isTrue();
+ assertThat( TreeSitterUtil.print( rootNode, new TurtleSyntaxTree.StringTokenProvider( content ) ) ).contains( "ERROR" );
+ }
+
+ @Test
+ void testBrokenSyntaxIncompleteTriple() {
+ final String content = """
+ @prefix ex: .
+
+ ex:entity ex:property .
+ """;
+
+ final TSTree tree = parser.parseString( null, content );
+ final TSNode rootNode = tree.getRootNode();
+
+ // Should have errors due to missing object
+ assertThat( rootNode.hasError() ).isTrue();
+ assertThat( TreeSitterUtil.print( rootNode, new TurtleSyntaxTree.StringTokenProvider( content ) ) ).contains( "ERROR" );
+ }
+
+ @Test
+ void testSemicolonAndCommaSyntax() {
+ final String content = """
+ @prefix ex: .
+
+ ex:entity
+ ex:prop1 "value1" ;
+ ex:prop2 "value2" , "value3" , "value4" ;
+ ex:prop3 "value5" .
+ """;
+
+ final TSTree tree = parser.parseString( null, content );
+ final TSNode rootNode = tree.getRootNode();
+
+ assertThat( rootNode.hasError() ).isFalse();
+ assertThat( TreeSitterUtil.print( rootNode, new TurtleSyntaxTree.StringTokenProvider( content ) ) ).doesNotContain( "ERROR" );
+ }
+
+ @Test
+ void testComments() {
+ final String content = """
+ # This is a comment
+ @prefix ex: .
+
+ # Another comment
+ ex:entity ex:property "value" . # Inline comment
+
+ # Multi-line comments
+ # are also supported
+ ex:entity2 ex:property2 "value2" .
+ """;
+
+ final TSTree tree = parser.parseString( null, content );
+ final TSNode rootNode = tree.getRootNode();
+
+ assertThat( rootNode.hasError() ).isFalse();
+ assertThat( TreeSitterUtil.print( rootNode, new TurtleSyntaxTree.StringTokenProvider( content ) ) ).doesNotContain( "ERROR" );
+ }
+}
diff --git a/core/esmf-turtle-language-server/pom.xml b/core/esmf-turtle-language-server/pom.xml
new file mode 100644
index 000000000..2d91f8a30
--- /dev/null
+++ b/core/esmf-turtle-language-server/pom.xml
@@ -0,0 +1,82 @@
+
+
+
+
+
+ org.eclipse.esmf
+ esmf-sdk-parent
+ DEV-SNAPSHOT
+ ../../pom.xml
+
+ 4.0.0
+
+ esmf-turtle-language-server
+ ESMF RDF/Turtle Language Server
+ jar
+
+
+ 1.0.0
+
+
+
+
+ org.eclipse.esmf
+ esmf-aspect-meta-model-java
+
+
+ org.eclipse.esmf
+ esmf-aspect-model-validator
+
+
+ org.eclipse.esmf
+ esmf-tree-sitter-turtle
+
+
+ org.slf4j
+ slf4j-nop
+
+
+
+
+ org.eclipse.lsp4j
+ org.eclipse.lsp4j
+ ${lsp4j.version}
+
+
+ org.slf4j
+ slf4j-api
+
+
+ ch.qos.logback
+ logback-classic
+
+
+
+ org.junit.jupiter
+ junit-jupiter
+ test
+
+
+ org.assertj
+ assertj-core
+ test
+
+
+ net.jqwik
+ jqwik
+
+
+
diff --git a/core/esmf-turtle-language-server/src/main/java/org/eclipse/esmf/turtle/languageserver/TurtleLanguageServer.java b/core/esmf-turtle-language-server/src/main/java/org/eclipse/esmf/turtle/languageserver/TurtleLanguageServer.java
new file mode 100644
index 000000000..8c9ddf4f8
--- /dev/null
+++ b/core/esmf-turtle-language-server/src/main/java/org/eclipse/esmf/turtle/languageserver/TurtleLanguageServer.java
@@ -0,0 +1,182 @@
+/*
+ * Copyright (c) 2026 Robert Bosch Manufacturing Solutions GmbH
+ *
+ * See the AUTHORS file(s) distributed with this work for additional
+ * information regarding authorship.
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ *
+ * SPDX-License-Identifier: MPL-2.0
+ */
+
+package org.eclipse.esmf.turtle.languageserver;
+
+import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.nio.channels.AsynchronousServerSocketChannel;
+import java.nio.channels.AsynchronousSocketChannel;
+import java.nio.channels.Channels;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Executors;
+import java.util.function.Function;
+
+import org.eclipse.esmf.turtle.languageserver.aspect.request.ValidateDocumentParams;
+import org.eclipse.esmf.turtle.languageserver.diagnostic.DiagnosticReport;
+import org.eclipse.esmf.turtle.languageserver.lsp.text.TurtleTextDocumentService;
+import org.eclipse.esmf.turtle.languageserver.lsp.workspace.TurtleWorkspaceService;
+import org.eclipse.esmf.turtle.languageserver.structure.TurtleTokenService;
+
+import org.eclipse.lsp4j.InitializeParams;
+import org.eclipse.lsp4j.InitializeResult;
+import org.eclipse.lsp4j.SaveOptions;
+import org.eclipse.lsp4j.SemanticTokensWithRegistrationOptions;
+import org.eclipse.lsp4j.ServerCapabilities;
+import org.eclipse.lsp4j.TextDocumentSyncKind;
+import org.eclipse.lsp4j.TextDocumentSyncOptions;
+import org.eclipse.lsp4j.jsonrpc.Launcher;
+import org.eclipse.lsp4j.jsonrpc.services.JsonRequest;
+import org.eclipse.lsp4j.services.LanguageClient;
+import org.eclipse.lsp4j.services.LanguageClientAware;
+import org.eclipse.lsp4j.services.LanguageServer;
+import org.eclipse.lsp4j.services.TextDocumentService;
+import org.eclipse.lsp4j.services.WorkspaceService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class TurtleLanguageServer implements LanguageServer, LanguageClientAware {
+ // R=18, D=4, F=6
+ public static final int DEFAULT_PORT = 1846;
+
+ private static final Logger LOG = LoggerFactory.getLogger( TurtleLanguageServer.class );
+ private static volatile boolean serverRunning = true;
+ private final TurtleTextDocumentService textDocumentService;
+ private final TurtleWorkspaceService workspaceService;
+
+ public TurtleLanguageServer() {
+ textDocumentService = new TurtleTextDocumentService();
+ workspaceService = new TurtleWorkspaceService( textDocumentService );
+ }
+
+ @Override
+ public CompletableFuture initialize( final InitializeParams params ) {
+ final ServerCapabilities capabilities = new ServerCapabilities();
+ final TextDocumentSyncOptions syncOptions = new TextDocumentSyncOptions();
+ syncOptions.setOpenClose( true );
+ syncOptions.setChange( TextDocumentSyncKind.Full );
+ syncOptions.setSave( new SaveOptions( true ) );
+ capabilities.setTextDocumentSync( syncOptions );
+ capabilities.setDefinitionProvider( true );
+ capabilities.setSemanticTokensProvider(
+ new SemanticTokensWithRegistrationOptions( TurtleTokenService.SUPPORTED_TOKEN_TYPES, true, false ) );
+ return CompletableFuture.completedFuture( new InitializeResult( capabilities ) );
+ }
+
+ @Override
+ public CompletableFuture