diff --git a/.github/workflows/publish-prerelease.yml b/.github/workflows/publish-prerelease.yml index ef7278521..16861ada5 100644 --- a/.github/workflows/publish-prerelease.yml +++ b/.github/workflows/publish-prerelease.yml @@ -22,11 +22,11 @@ jobs: - name: Checkout code uses: actions/checkout@v4 - - name: Set up JDK 11 + - name: Set up JDK 21 uses: actions/setup-java@v4 with: distribution: 'temurin' - java-version: '11' + java-version: '21' cache: 'gradle' - name: Cache Gradle @@ -38,7 +38,11 @@ jobs: ${{ runner.os }}-gradle- - name: Build project - run: ./gradlew build + run: ./gradlew shadowJar + + - name: Generate timestamp + id: timestamp + run: echo "timestamp=$(date '+%d %B %Y, %H:%M:%S')" >> $GITHUB_OUTPUT - name: Publish single pre-release uses: ncipollo/release-action@v1 @@ -49,6 +53,12 @@ jobs: body: | 🚧 This is the latest build from the `develop` branch. Not intended for production use. + + **Last Updated:** ${{ steps.timestamp.outputs.timestamp }} + **Build Info:** + - Branch: `${{ github.ref_name }}` + - Commit: `${{ github.sha }}` + - Build Time: `${{ steps.timestamp.outputs.timestamp }}` prerelease: true allowUpdates: true replacesArtifacts: true diff --git a/.github/workflows/release-github.yml b/.github/workflows/release-github.yml index 65b807c81..d5d6a5b14 100644 --- a/.github/workflows/release-github.yml +++ b/.github/workflows/release-github.yml @@ -23,11 +23,11 @@ jobs: with: ref: ${{ github.event.inputs.ref || github.ref_name }} - - name: Set up JDK 11 + - name: Set up JDK 21 uses: actions/setup-java@v4 with: distribution: 'temurin' - java-version: '11' + java-version: '21' cache: 'gradle' - name: Cache Gradle diff --git a/.github/workflows/release-maven-central.yml b/.github/workflows/release-maven-central.yml index aad043336..849cc4f8a 100644 --- a/.github/workflows/release-maven-central.yml +++ b/.github/workflows/release-maven-central.yml @@ -1,10 +1,15 @@ name: Publish Maven Central -run-name: ${{ github.actor }} triggered Maven Central release on ${{ github.ref_name }} +run-name: ${{ github.actor }} triggered Maven Central release on ${{ github.event.inputs.ref || github.ref_name }} on: push: tags: - "v*.*.*" + workflow_dispatch: + inputs: + ref: + description: 'Branch or tag to publish (e.g. develop or v4.6.4)' + required: true jobs: release-maven-central: @@ -13,12 +18,14 @@ jobs: steps: - name: Checkout code uses: actions/checkout@v4 + with: + ref: ${{ github.event.inputs.ref || github.ref }} - - name: Set up JDK 11 + - name: Set up JDK 21 uses: actions/setup-java@v4 with: distribution: 'temurin' - java-version: '11' + java-version: '21' cache: 'gradle' - name: Cache Gradle @@ -29,13 +36,13 @@ jobs: restore-keys: | ${{ runner.os }}-gradle- - - name: Build and publish to Sonatype (staging only) + - name: Publish to Maven Central via Vanniktech env: - OSSRH_USERNAME: ${{ secrets.OSSRH_USERNAME }} - OSSRH_PASSWORD: ${{ secrets.OSSRH_PASSWORD }} - GPG_SIGNING_KEY: ${{ secrets.GPG_SIGNING_KEY }} - GPG_SIGNING_PASSPHRASE: ${{ secrets.GPG_SIGNING_PASSPHRASE }} - run: ./gradlew publishToSonatype + ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.SONATYPE_USERNAME }} + ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.SONATYPE_PASSWORD }} + ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.GPG_SIGNING_KEY }} + ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{ secrets.GPG_SIGNING_PASSPHRASE }} + run: ./gradlew publishToMavenCentral - name: Manual Release Reminder - run: echo "The artifacts have been staged on Sonatype. Please log in to Sonatype Nexus to close and release the staging repository manually." + run: echo "Upload complete. Please finalize the deployment at https://central.sonatype.com/publishing/deployments" diff --git a/.github/workflows/sphinx-dev.yml b/.github/workflows/sphinx-dev.yml index fd90e6fc7..09c066e96 100644 --- a/.github/workflows/sphinx-dev.yml +++ b/.github/workflows/sphinx-dev.yml @@ -1,5 +1,5 @@ -name: "Sphinx: Render development docs" -run-name: ${{ github.actor }} has launched CI process on ${{ github.ref_name }} +name: "Docs: Render and Deploy Development Documentation" +run-name: ${{ github.actor }} triggered a documentation build on branch ${{ github.ref_name }} on: push: diff --git a/CHANGELOG.md b/CHANGELOG.md index 4b033eff2..52c165495 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,7 @@ # Corese Changelog -## Version 4.6.4-SNAPSHOT – +## Version 4.6.4 – ### Changed diff --git a/INSTALL.md b/INSTALL.md deleted file mode 100644 index d3463d8dd..000000000 --- a/INSTALL.md +++ /dev/null @@ -1,82 +0,0 @@ -# Installation Guide for Corese-Core - -This document provides step-by-step instructions to install and build **Corese-Core** from source. - -## Prerequisites - -Before installing Corese-Core, make sure you have the following installed: - -- **Java 21** or higher - → Check with: `java -version` -- **Git** (to clone the repository) -- **Gradle 8+** (optional, recommended) - → If not installed, the Gradle Wrapper (`./gradlew`) will be used. -- **Internet access** (to fetch dependencies) - -## Clone the Repository - -```bash -git clone https://github.com/corese-stack/corese-core.git -cd corese-core -``` - -## Build Corese-Core - -You can build the project using the Gradle wrapper: - -```bash -./gradlew build -``` - -This will: - -- Compile all modules -- Run tests -- Generate the main JAR file (in `build/libs/`) -- Publish the library to the local Maven repository if needed - -If you're only interested in building without tests: - -```bash -./gradlew assemble -``` - -## Run Tests - -```bash -./gradlew test -``` - -You can view the test reports in: - -```text -corese-core/build/reports/tests/test/index.html -``` - -## Publish to Local Maven (optional) - -To publish Corese-Core locally for use in other modules (like `corese-gui`, `corese-server`, etc.): - -```bash -./gradlew publishToMavenLocal -``` - -The artifact will be installed under: - -```text -~/.m2/repository/fr/inria/corese/corese-core/ -``` - -## Clean Build - -```bash -./gradlew clean -``` - ---- - -## Troubleshooting - -- *Gradle not found?* → Use `./gradlew` instead of `gradle` -- *Java version too low?* → Corese requires Java 21+. You can install it via SDKMAN, Homebrew, or your package manager. -- *Tests failing due to RDF line endings or hashes?* → Make sure to normalize line endings (`\n`) and verify data hashes if you're running tests on Windows. diff --git a/README.md b/README.md index 86dc04f96..9ad3bc1a3 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ [![License: CECILL-C](https://img.shields.io/badge/License-CECILL--C-blue.svg)](https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) [![Discussions](https://img.shields.io/badge/Discussions-GitHub-blue)](https://github.com/orgs/corese-stack/discussions) -## ✨ Features +## Features - Manipulate RDF graphs (parse, serialize, transform) - Execute SPARQL 1.1 queries and updates @@ -21,7 +21,7 @@ - Apply logic-based rules with SPARQL Rules - Extend functionality and scripting with LDScript -## 🚀 Getting Started +## Getting Started Integrate Corese-Core into your Java project using your preferred build tool. @@ -31,26 +31,30 @@ Integrate Corese-Core into your Java project using your preferred build tool. fr.inria.corese corese-core - 4.6.4-SNAPSHOT + 4.6.4 ``` ### Gradle ```groovy -implementation 'fr.inria.corese:corese-core:4.6.4-SNAPSHOT' +implementation 'fr.inria.corese:corese-core:4.6.4' ``` ### Manual JAR Download the latest `.jar` file from: -- [🔗 GitHub Releases](https://github.com/corese-stack/corese-core/releases) -- [📦 Maven Central](https://central.sonatype.com/artifact/fr.inria.corese/corese-core) + + Get it on GitHub + + + Get it on Maven Central + -## 📖 Documentation +## Documentation -- [Corese-Core api documentation](https://corese-stack.github.io/corese-core/v4.6.3/java_api/library_root.html) +- [Corese-Core api documentation](https://corese-stack.github.io/corese-core/v4.6.4/java_api/library_root.html) **W3C Standards:** @@ -67,7 +71,7 @@ Download the latest `.jar` file from: - [SPARQL Rule Engine](https://files.inria.fr/corese/doc/rule.html) - [LDScript Reference](https://files.inria.fr/corese/doc/ldscript.html) -## 🤝 Contributing +## Contributing We welcome contributions! Here’s how to get involved: @@ -75,7 +79,7 @@ We welcome contributions! Here’s how to get involved: - [Issue Tracker](https://github.com/corese-stack/corese-core/issues) - [Pull Requests](https://github.com/corese-stack/corese-core/pulls) -## 🔗 Useful Links +## Useful Links - [Corese Website](https://corese-stack.github.io/corese-core) - Mailing List: diff --git a/build.gradle.kts b/build.gradle.kts index b8e91e7e1..16a91eb6d 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -8,8 +8,8 @@ plugins { // Publishing plugins signing // Signs artifacts for Maven Central `maven-publish` // Enables publishing to Maven repositories - id("io.github.gradle-nexus.publish-plugin") version "2.0.0" // Automates Nexus publishing - + id("com.vanniktech.maven.publish") version "0.34.0" // Automates Maven publishing tasks + // Tooling plugins `jacoco` // For code coverage reports id("com.gradleup.shadow") version "8.3.7" @@ -73,7 +73,7 @@ object Meta { // Project coordinates const val groupId = "fr.inria.corese" const val artifactId = "corese-core" - const val version = "4.6.4-SNAPSHOT" + const val version = "4.6.4" // Project description const val desc = "Corese is a Semantic Web Factory (triple store and SPARQL endpoint) implementing RDF, RDFS, SPARQL 1.1 Query and Update, Shacl. STTL. LDScript." @@ -82,10 +82,6 @@ object Meta { // License information const val license = "CeCILL-C License" const val licenseUrl = "https://opensource.org/licenses/CeCILL-C" - - // Sonatype OSSRH publishing settings - const val release = "https://oss.sonatype.org/service/local/staging/deploy/maven2/" - const val snapshot = "https://oss.sonatype.org/content/repositories/snapshots/" } //////////////////////// @@ -96,9 +92,9 @@ object Meta { java { withJavadocJar() // Include Javadoc JAR in publications withSourcesJar() // Include sources JAR in publications - toolchain { - languageVersion.set(JavaLanguageVersion.of(21)) - } + toolchain { + languageVersion.set(JavaLanguageVersion.of(21)) + } } ///////////////////////// @@ -118,7 +114,7 @@ dependencies { runtimeOnly("org.apache.logging.log4j:log4j-slf4j2-impl:2.25.0") // SLF4J binding for Log4j2 (runtime) // === Core dependencies === - api("fr.com.hp.hpl.jena.rdf.arp:arp:2.2.b") // RDF/XML parser (Jena ARP) + api("fr.com.hp.hpl.jena.rdf.arp:arp:2.2.b") // RDF/XML parser (Jena ARP) implementation("fr.inria.corese.org.semarglproject:semargl-rdfa:0.7.2") // RDFa parser (Semargl) implementation("com.github.jsonld-java:jsonld-java:0.13.4") // JSON-LD processing @@ -126,25 +122,27 @@ dependencies { antlr("org.antlr:antlr4:4.13.2") // Antlr for parsing (ANTLR 4) implementation("org.antlr:antlr4-runtime:4.13.2") // Antlr runtime for parsing - - // === JSONLD - implementation("com.apicatalog:titanium-json-ld:1.6.0") - implementation("com.apicatalog:titanium-rdf-api:1.0.0") - implementation("org.eclipse.parsson:parsson:1.1.7") - implementation("jakarta.json:jakarta.json-api:2.1.3") - // === HTTP and XML === implementation("org.glassfish.jersey.core:jersey-client:3.1.10") // HTTP client (Jersey) implementation("org.glassfish.jersey.inject:jersey-hk2:3.1.10") // Dependency injection for Jersey implementation("com.sun.activation:jakarta.activation:2.0.1") // MIME type handling (Jakarta Activation) + // === JSONLD Parsing === + implementation("com.apicatalog:titanium-json-ld:1.6.0") // JSON-LD processing library + implementation("com.apicatalog:titanium-rdf-api:1.0.0") // Titanium RDF API for JSON-LD processing + implementation("org.eclipse.parsson:parsson:1.1.7") // JSON parser for JSON-LD + implementation("jakarta.json:jakarta.json-api:2.1.3") // Jakarta JSON API for JSON processing + + + // === XML parsing === + implementation("com.typesafe.akka:akka-stream_2.13:2.6.20") // Akka Streams for reactive streams processing + implementation("com.lightbend.akka:akka-stream-alpakka-xml_2.13:3.0.4") // Alpakka XML for XML processing with Akka Streams + // === Utilities === implementation("org.apache.commons:commons-text:1.13.1") // Text manipulation utilities (Commons Text) implementation("org.json:json:20250517") // JSON processing implementation("com.typesafe:config:1.4.3") // Configuration library (Typesafe Config) - - // === Test dependencies === testImplementation(platform("org.junit:junit-bom:5.13.2")) // JUnit BOM for consistent test versions testImplementation("org.junit.jupiter:junit-jupiter:5.13.2") // JUnit Jupiter API and engine @@ -157,115 +155,61 @@ dependencies { // Publishing settings // ///////////////////////// -// Publication configuration for Maven repositories -publishing { - publications { - create("mavenJava") { - - // Configure the publication to include JAR, sources, and Javadoc - from(components["java"]) - - // Configures version mapping to control how dependency versions are resolved - // for different usage contexts (API and runtime). - versionMapping { - // Defines version mapping for Java API usage. - // Sets the version to be resolved from the runtimeClasspath configuration. - usage("java-api") { - fromResolutionOf("runtimeClasspath") - } - - // Defines version mapping for Java runtime usage. - // Uses the result of dependency resolution to determine the version. - usage("java-runtime") { - fromResolutionResult() - } +mavenPublishing { + coordinates(Meta.groupId, Meta.artifactId, Meta.version) + + pom { + name.set(Meta.artifactId) + description.set(Meta.desc) + url.set("https://github.com/${Meta.githubRepo}") + licenses { + license { + name.set(Meta.license) + url.set(Meta.licenseUrl) + distribution.set("repo") } - - // Configure the publication metadata - groupId = Meta.groupId - artifactId = Meta.artifactId - version = Meta.version - - pom { - name.set(Meta.artifactId) - description.set(Meta.desc) - url.set("https://github.com/${Meta.githubRepo}") - licenses { - license { - name.set(Meta.license) - url.set(Meta.licenseUrl) - } - } - developers { - developer { - id.set("OlivierCorby") - name.set("Olivier Corby") - email.set("olivier.corby@inria.fr") - url.set("http://www-sop.inria.fr/members/Olivier.Corby") - organization.set("Inria") - organizationUrl.set("http://www.inria.fr/") - } - developer { - id.set("remiceres") - name.set("Rémi Cérès") - email.set("remi.ceres@inria.fr") - url.set("http://www-sop.inria.fr/members/Remi.Ceres") - organization.set("Inria") - organizationUrl.set("http://www.inria.fr/") - } - developer { - id.set("pierremaillot") - name.set("Pierre Maillot") - email.set("pierre.maillot@inria.fr") - url.set("https://w3id.org/people/pierremaillot") - organization.set("Inria") - organizationUrl.set("http://www.inria.fr/") - } - } - scm { - url.set("https://github.com/${Meta.githubRepo}.git") - connection.set("scm:git:git://github.com/${Meta.githubRepo}.git") - developerConnection.set("scm:git:git://github.com/${Meta.githubRepo}.git") - } - issueManagement { - url.set("https://github.com/${Meta.githubRepo}/issues") - } + } + developers { + developer { + id.set("OlivierCorby") + name.set("Olivier Corby") + email.set("olivier.corby@inria.fr") + url.set("http://www-sop.inria.fr/members/Olivier.Corby") + organization.set("Inria") + organizationUrl.set("http://www.inria.fr/") + } + developer { + id.set("remiceres") + name.set("Rémi Cérès") + email.set("remi.ceres@inria.fr") + url.set("http://www-sop.inria.fr/members/Remi.Ceres") + organization.set("Inria") + organizationUrl.set("http://www.inria.fr/") + } + developer { + id.set("pierremaillot") + name.set("Pierre Maillot") + email.set("pierre.maillot@inria.fr") + url.set("https://maillpierre.github.io/personal-page/") + organization.set("Inria") + organizationUrl.set("http://www.inria.fr/") } } + scm { + url.set("https://github.com/${Meta.githubRepo}/") + connection.set("scm:git:git://github.com/${Meta.githubRepo}.git") + developerConnection.set("scm:git:ssh://git@github.com/${Meta.githubRepo}.git") + } + issueManagement { + url.set("https://github.com/${Meta.githubRepo}/issues") + } } -} - -// Configure artifact signing -signing { - // Retrieve the GPG signing key and passphrase from environment variables for secure access. - val signingKey = providers.environmentVariable("GPG_SIGNING_KEY") - val signingPassphrase = providers.environmentVariable("GPG_SIGNING_PASSPHRASE") - - // Sign the publications if the GPG signing key and passphrase are available. - if (signingKey.isPresent && signingPassphrase.isPresent) { - useInMemoryPgpKeys(signingKey.get(), signingPassphrase.get()) - sign(publishing.publications) - } -} - -// Configure Nexus publishing and credentials -nexusPublishing { - repositories { - // Configure Sonatype OSSRH repository for publishing. - sonatype { - // Retrieve Sonatype OSSRH credentials from environment variables. - val ossrhUsername = providers.environmentVariable("OSSRH_USERNAME") - val ossrhPassword = providers.environmentVariable("OSSRH_PASSWORD") - - // Set the credentials for Sonatype OSSRH if they are available. - if (ossrhUsername.isPresent && ossrhPassword.isPresent) { - username.set(ossrhUsername.get()) - password.set(ossrhPassword.get()) - } - // Define the package group for this publication, typically following the group ID. - packageGroup.set(Meta.groupId) - } + publishToMavenCentral() + + // Only sign publications when GPG keys are available (CI environment) + if (project.hasProperty("signingInMemoryKey") || project.hasProperty("signing.keyId")) { + signAllPublications() } } @@ -281,11 +225,14 @@ tasks.withType { // Configure Javadoc tasks with UTF-8 encoding and disable failure on error. // This ensures that Javadoc generation won't fail due to minor issues. -tasks.withType { +tasks.withType().configureEach { options.encoding = "UTF-8" isFailOnError = false + // Configure Javadoc tasks to disable doclint warnings. + (options as CoreJavadocOptions).addBooleanOption("Xdoclint:none", true) } + // Configure the shadow JAR task to include dependencies in the output JAR. // This creates a single JAR file with all dependencies bundled. // The JAR file is named with the classifier "jar-with-dependencies". @@ -295,15 +242,6 @@ tasks { } } -// Configure Javadoc tasks to disable doclint warnings. -tasks { - javadoc { - options { - (this as CoreJavadocOptions).addBooleanOption("Xdoclint:none", true) - } - } -} - // Configure the build task to depend on the shadow JAR task. // This ensures that the shadow JAR is built when the project is built. tasks.build { @@ -392,4 +330,4 @@ tasks.named("generateGrammarSource") { tasks.named("javaccSparqlCorese") { dependsOn("createGeneratedDirs") -} +} \ No newline at end of file diff --git a/docs/.gitignore b/docs/.gitignore deleted file mode 100644 index 2fa6b261c..000000000 --- a/docs/.gitignore +++ /dev/null @@ -1,25 +0,0 @@ -# Generated files for docs -generated/ -bak/ -build/ - -source/* -!source/*.rst -!source/**/*.rst -!source/**/*.py - -!source/README.md -!source/install.md -!source/Doxyfile - - -!source/rest_api/ -!source/cli_ref/ - -!source/_templates/ -!source/_static/ - -!source/advanced/ -!source/federation/ -!source/getting_started/ -!source/storage/ diff --git a/docs/source/_static/js/favicon-theme.js b/docs/source/_static/js/favicon-theme.js new file mode 100644 index 000000000..d534a7d29 --- /dev/null +++ b/docs/source/_static/js/favicon-theme.js @@ -0,0 +1,12 @@ +function setFavicon(e) { + const dark = e.matches; + const favicon = document.getElementById("favicon"); + if (favicon) { + favicon.href = dark + ? "_static/logo/corese_fav_dark.svg" + : "_static/logo/corese_fav_light.svg"; + } +} + +const mql = window.matchMedia("(prefers-color-scheme: dark)"); +mql.addEventListener("change", setFavicon); diff --git a/docs/source/_static/logo/badge_github.svg b/docs/source/_static/logo/badge_github.svg index c6571839a..8e7e0b737 100644 --- a/docs/source/_static/logo/badge_github.svg +++ b/docs/source/_static/logo/badge_github.svg @@ -7,7 +7,7 @@ id="svg13" sodipodi:docname="badge_github.svg" xml:space="preserve" - inkscape:version="1.4 (e7c3feb100, 2024-10-09)" + inkscape:version="1.4.2 (ebf0e940d0, 2025-05-08)" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns="http://www.w3.org/2000/svg" @@ -20,15 +20,15 @@ inkscape:pageopacity="0.0" inkscape:pagecheckerboard="0" inkscape:deskcolor="#d1d1d1" - inkscape:zoom="2.5190679" - inkscape:cx="106.78553" - inkscape:cy="67.485279" + inkscape:zoom="1.78125" + inkscape:cx="-20.491228" + inkscape:cy="159.7193" inkscape:window-width="3072" inkscape:window-height="1659" inkscape:window-x="0" inkscape:window-y="0" inkscape:window-maximized="1" - inkscape:current-layer="g3" /> + style="fill:#ffffff" /> diff --git a/docs/source/_static/logo/badge_maven.svg b/docs/source/_static/logo/badge_maven.svg index b15bb1f5c..c8e9a20b1 100644 --- a/docs/source/_static/logo/badge_maven.svg +++ b/docs/source/_static/logo/badge_maven.svg @@ -7,7 +7,7 @@ id="svg13" sodipodi:docname="badge_maven.svg" xml:space="preserve" - inkscape:version="1.4 (e7c3feb100, 2024-10-09)" + inkscape:version="1.4.2 (ebf0e940d0, 2025-05-08)" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns="http://www.w3.org/2000/svg" @@ -21,8 +21,8 @@ inkscape:pagecheckerboard="0" inkscape:deskcolor="#d1d1d1" inkscape:zoom="2.5190679" - inkscape:cx="52.797306" - inkscape:cy="119.48864" + inkscape:cx="52.995793" + inkscape:cy="119.68713" inkscape:window-width="3072" inkscape:window-height="1659" inkscape:window-x="0" @@ -271,4 +271,10 @@ class="cls-1" d="m 293.8,31.2 c 0.3,0.1 0.8,0.2 1.1,0.2 0.9,0 1.5,-0.2 1.9,-1 l 0.5,-1.1 -5.4,-13.6 h 3.8 l 3.5,9.3 3.5,-9.3 h 3.8 l -6.3,15.6 c -1,2.5 -2.8,3.2 -5.1,3.2 -0.4,0 -1.3,0 -1.8,-0.2 l 0.5,-3.2 z" id="path22" - style="fill:#ffffff" /> + style="fill:#ffffff" /> diff --git a/docs/source/_static/logo/corese-core_doc_fav.svg b/docs/source/_static/logo/corese-core_doc_fav.svg deleted file mode 100644 index 693148118..000000000 --- a/docs/source/_static/logo/corese-core_doc_fav.svg +++ /dev/null @@ -1,82 +0,0 @@ - - diff --git a/docs/source/_static/logo/corese-core_doc_bar.svg b/docs/source/_static/logo/corese_fav_dark.svg similarity index 64% rename from docs/source/_static/logo/corese-core_doc_bar.svg rename to docs/source/_static/logo/corese_fav_dark.svg index 693148118..1ddda212f 100644 --- a/docs/source/_static/logo/corese-core_doc_bar.svg +++ b/docs/source/_static/logo/corese_fav_dark.svg @@ -5,9 +5,9 @@ width="16px" version="1.1" id="svg7" - sodipodi:docname="corese-core_doc_fav.svg" + sodipodi:docname="corese-core_doc_fav_dark.svg" xml:space="preserve" - inkscape:version="1.4 (e7c3feb100, 2024-10-09)" + inkscape:version="1.4.2 (ebf0e940d0, 2025-05-08)" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns="http://www.w3.org/2000/svg" @@ -36,7 +36,7 @@ offset="0" id="stop1258" /> + id="path6" + style="fill:none;stroke:#e0e0e0;stroke-opacity:1" /> diff --git a/docs/source/_static/logo/corese_fav_light.svg b/docs/source/_static/logo/corese_fav_light.svg new file mode 100644 index 000000000..9767a0c9c --- /dev/null +++ b/docs/source/_static/logo/corese_fav_light.svg @@ -0,0 +1,88 @@ + + diff --git a/docs/source/_templates/layout.html b/docs/source/_templates/layout.html new file mode 100644 index 000000000..3fe1e248c --- /dev/null +++ b/docs/source/_templates/layout.html @@ -0,0 +1,16 @@ +{% extends "!layout.html" %} + +{% block extrahead %} + +{{ super() }} +{% endblock %} \ No newline at end of file diff --git a/docs/source/advanced/SPARQL_Service_Extension.md b/docs/source/advanced/SPARQL_Service_Extension.md deleted file mode 100644 index a4e7f3f7c..000000000 --- a/docs/source/advanced/SPARQL_Service_Extension.md +++ /dev/null @@ -1,545 +0,0 @@ -# SPARQL Service Extension - -## Abstract - -This document presents extensions of SPARQL service implemented in Corese. - -## Introduction - -In a SERVICE clause, Corese allows for the addition of URL parameters to the service URL. These parameters are used to tune the behaviour of the service. For example, we can specify the format of the query result returned by the service. - -For example, the following query will make Corese write a trace of the execution of the query in its logs: -``` -http://corese.inria.fr/sparql?mode=debug&query=select * {?s ?p ?o} -``` - - - -### Prefix used in this document. - -```turtle -@prefix st: . -@prefix stm: . -@prefix d3: . -``` - - -## Server URL Parameter - -### Standard parameters -In this section, we consider endpoint URL parameters decoded by a SPARQL endpoint receiving an HTTP request. - -Standard endpoint URL parameters are available. - -``` -query -default-graph-uri -named-graph-uri -``` - - -Currently corese does not implement the Update parameters below. Users can use query parameters listed above instead. - -``` -update -using-graph-uri -using-named-graph-uri -``` - -### Shorthand format parameters -`format` specifies query result format when HTTP Accept header cannot be set. For example, `format=json` is equivalent to: `header "Accept: application/sparql-results+json"`. - -For SELECT and ASK queries: -``` -format = json | xml -``` -For SELECT, ASK, DESCRIBE and CONSTRUCT queries: -``` -format = jsonld | rdfxml | turtle -``` - -### Trace parameter -It is possible to specify several kinds of traces of execution. - -``` -mode = debug | trace -``` - -For example, for the query `SELECT * { ?s ?p ?o } LIMIT 1` the normal trace is: -``` -[datetime] INFO webservice.SPARQLRestAPI.getTriplesXMLForGet:311 [] - getTriplesXMLForGet -[datetime] INFO webservice.SPARQLResult.getResultFormat:90 [] - Endpoint URL: http://localhost:8080/sparql -[datetime] INFO webservice.SPARQLResult.getResultFormat:96 [] - Query: SELECT * { ?s ?p ?o } LIMIT 1 -[datetime] INFO webservice.EventManager.log:72 [] - Workflow Context: -st:remoteHost : "[0:0:0:0:0:0:0:1]" -st:service : "http://ns.inria.fr/sparql-template/default" -request : "[org.eclipse.jetty.server.Request:Request(GET http://localhost:8080/sparql?query=SELECT%20*%20%7B%20?s%20?p%20?o%20%7D%20LIMIT%201)@429f0053]"^^dt:pointer -url : -user query: true -level: PRIVATE - -[datetime] INFO webservice.EventManager.log:73 [] - {st:count="[Map: size=2]"^^dt:map, st:date="[Map: size=2]"^^dt:map, st:host="[Map: size=2]"^^dt:map, st:hostlite="[Map: size=1]"^^dt:map} -[datetime] INFO webservice.EventManager.log:74 [] - {st:sparql=2, "http://ns.inria.fr/sparql-template/default"=17} -[datetime] INFO webservice.EventManager.log:76 [] - {"127.0.0.1"=11} -``` - -The `debug` parameter change the trace in the following: -``` -[datetime] INFO webservice.SPARQLRestAPI.getTriplesXMLForGet:311 [] - getTriplesXMLForGet -[datetime] INFO webservice.SPARQLResult.getResultFormat:90 [] - Endpoint URL: http://localhost:8080/sparql -[datetime] INFO webservice.SPARQLResult.getResultFormat:96 [] - Query: SELECT * { ?s ?p ?o } LIMIT 1 -[datetime] INFO webservice.EventManager.log:72 [] - Workflow Context: -debug : true -st:remoteHost : "[0:0:0:0:0:0:0:1]" -st:service : "http://ns.inria.fr/sparql-template/default" -mode : "("debug" )"^^dt:list -request : "[org.eclipse.jetty.server.Request:Request(GET http://localhost:8080/sparql?query=SELECT%20*%20%7B%20?s%20?p%20?o%20%7D%20LIMIT%201&mode=debug)@5c192889]"^^dt:pointer -url : -user query: true -level: PRIVATE - -[datetime] INFO webservice.EventManager.log:73 [] - {st:count="[Map: size=2]"^^dt:map, st:date="[Map: size=2]"^^dt:map, st:host="[Map: size=2]"^^dt:map, st:hostlite="[Map: size=1]"^^dt:map} -[datetime] INFO webservice.EventManager.log:74 [] - {st:sparql=2, "http://ns.inria.fr/sparql-template/default"=18} -[datetime] INFO webservice.EventManager.log:76 [] - {"127.0.0.1"=11} -select [NODE {?s }, NODE {?p }, NODE {?o }] -QUERY { -AND { - EDGE {?s ?p ?o} } } -[datetime] WARN tool.Message.log:64 [] - Eval: 00 AND { -EDGE {?s ?p ?o} } -[datetime] WARN tool.Message.log:64 [] - Loop: 3 1 -[datetime] INFO webservice.SPARQLResult.getFormat:411 [] - transform: null -``` - -The `trace` parameter change the trace in the following: -``` -[datetime] INFO webservice.SPARQLRestAPI.getTriplesXMLForGet:311 [] - getTriplesXMLForGet -[datetime] INFO webservice.SPARQLResult.getResultFormat:90 [] - Endpoint URL: http://localhost:8080/sparql -[datetime] INFO webservice.SPARQLResult.getResultFormat:96 [] - Query: SELECT * { ?s ?p ?o } LIMIT 1 -Endpoint HTTP Request -header: Accept: */* -header: User-Agent: Wget/1.21.3 -header: Connection: keep-alive -header: Host: localhost:8080 -header: Accept-Encoding: identity -param: query=SELECT * { ?s ?p ?o } LIMIT 1 -param: mode=trace -[datetime] INFO webservice.EventManager.log:72 [] - Workflow Context: -st:remoteHost : "[0:0:0:0:0:0:0:1]" -st:service : "http://ns.inria.fr/sparql-template/default" -mode : "("trace" )"^^dt:list -request : "[org.eclipse.jetty.server.Request:Request(GET http://localhost:8080/sparql?query=SELECT%20*%20%7B%20?s%20?p%20?o%20%7D%20LIMIT%201&mode=trace)@11b799cb]"^^dt:pointer -trace : true -url : -user query: true -level: PRIVATE - -[datetime] INFO webservice.EventManager.log:73 [] - {st:count="[Map: size=2]"^^dt:map, st:date="[Map: size=2]"^^dt:map, st:host="[Map: size=2]"^^dt:map, st:hostlite="[Map: size=1]"^^dt:map} -[datetime] INFO webservice.EventManager.log:74 [] - {st:sparql=2, "http://ns.inria.fr/sparql-template/default"=19} -[datetime] INFO webservice.EventManager.log:76 [] - {"127.0.0.1"=11} -SPARQL endpoint -select * -where { - ?s ?p ?o . -} -limit 1 -01 ?s = ; ?p = rdf:type; ?o = owl:Class; - -service result: - - - - - - - - - -http://linkedgeodata.org/ontology/RailwayConstruction -http://www.w3.org/1999/02/22-rdf-syntax-ns#type -http://www.w3.org/2002/07/owl#Class - - - -``` - -## Client URL Parameter - -In this section we consider service URL parameters decoded by SPARQL service interpreter when calling a service. - -``` -service { BGP } -``` -Such parameters are used to modify the way the Corese server will interact with the remote service. For example, we can specify the format of the query result returned by the service. - -### Standard parameters -Standard dataset uri parameters are available. Hence we can specify a dataset for a service. - -``` -default-graph-uri = uri -named-graph-uri = uri -``` - -### HTTP method -`method` specifies HTTP GET or POST method for calling the service. -``` -method = get | post -``` - - -`header` cna be used to specify any HTTP header parameter. -``` -header=name:value -``` -For example, to specify the HTTP Accept header: -```sparql -SELECT * { - ?s ?p ?o . - SERVICE { - ?o a ?c - } -} LIMIT 1 -``` - -This previous query is equivalent to the usage of `format=json`. `format` can be used to specify the HTTP Accept header. -Specify the format of the service query result returned by the endpoint using content negotiation. -``` -format = xml | json -``` - -### Trace mode -`mode` in client URL parameter is equivalent to `mode` in server URL parameter. It specifies the trace mode of the service. It add to the trace trace intermediate results of service, and shows the string result returned by service. -``` -mode = debug | trace -``` - - -The `trap` mode "traps" syntax error when parsing service query results and in case of an error, return a subset of results if possible. -``` -mode = trap -``` - -### Bindings - -#### Bindings values transmission -`binding` specifies the syntax used for variable bindings sent with the service. Variable bindings are the results of intermediate statement evaluation that can be passed as argument of the service. - -``` -binding = filter | values -``` - -`binding=filter` generates bindings with the following syntax: -``` -filter (?x = x1 && ?y = y1) -``` - -`binding=values` generates bindings with the following syntax: -``` -values (?x ?y) { (x1 y1) } -``` - -For example, with the following data in the local endpoint: - -```turtle -@prefix : . - -ex:John :name "John" . -``` -if the following query is executed: - -```sparql -PREFIX : -SELECT ?x ?age { - ?x :name ?name . - SERVICE { - ?x :name ?name ; - :age ?age . - } -} -``` - -then the query sent to the remote endpoint will be: - -```sparql -PREFIX : -SELECT * { - VALUES ?name { "John" } - ?x :name ?name ; - :age ?age . -} -``` - -#### Bindings variable selection -`focus` and `skip` specify variables that must or must not be passed as variable bindings to the service. -``` -focus=x -skip=y -``` - -#### Bindings in-scope -In order to have their bindings passed into a service clause, variables must be [in-scope](https://www.w3.org/TR/sparql11-query/#variableScope) in the service statement. When a variable is not in-scope, it is possible to make it in-scope with a values expression in the service statement. -``` -values var { undef } -``` - -#### Binding slicing -Specify the size of the slice of intermediate results variable bindings sent with the service. Default is 20 sets of variable bindings (i.e. from 20 intermediate results). - -``` -slice = integer -``` - - -Specify a limit for the number of results returned by the service. - -``` -limit = integer -``` - - -Specify a timeout in millisecond for the service. - -``` -timeout = 123 -``` - - -### Exotic Extension - -Any parameter value can be a LDScript global variable. The SPARQL interpreter evaluates the variable at runtime and replaces it by its value. - -``` -param={?paramValue} -``` - - -Remote server return an RDF document instead of SPARQL query results. Parse the RDF document, create an RDF graph, evaluate service BGP locally on the RDF graph. - -``` -mode=construct -``` - - -Remote server return a document (e.g. JSON) instead of SPARQL query results. Parse the document using a LDScript function that returns an RDF graph. Evaluate service BGP locally on the graph returned by the function. - -``` -wrapper=functionNameURI -``` - - -## Service Log - -Obtain additional information about query execution and query results such as explanation, trace, etc. - -### Log - -Detailed log of federated query execution returned as Linked Result RDF/Turtle document, with source selection, rewritten query, intermediate query results. - -``` -mode=log -``` - - -Query string returned as Linked Result. - -``` -mode=logquery -``` - - -### Explain - -Explain why federated query fail. Save intermediate query and results as Linked Result documents. Return one JSON object that contains the list of URLs of these Linked Results. This mode is processed by corese GUI. - -``` -mode=why -``` - - -Show where query fail: display last executed statement. - -``` -mode=explain -``` - - -### Message - -Return a JSON object message as Linked Result. JSON message contain the Context, the date, execution time. It contains also endpoint exceptions and service that fail in case of federated query. Message is displayed by corese GUI. It is possible to obtain a message systematically by specifying the default mode as such (see below). - -``` -mode=message -``` - - -## Service Extension - -### Endpoint URL Default Parameter - -Define default parameter values for SPARQL endpoint URL in urlprofile.ttl. Mode *, if any, is applied to every service. Parameter "document" is an URL that is added in Query Results "link" tag. - -``` -[] st:mode "*" ; -st:param ( -("mode" "message") ("document" ) -) -. -``` - - -Define parameter values associated to specific mode. - -``` -[] st:mode "map" ; -st:param (("mode" "link") ("transform" stm:mapper)) -. -``` - - -Define service URL with predefined parameter values. - -``` - -st:param (("mode" "map")) -. -``` - - -### Federated SPARQL endpoint - -A federated SPARQL endpoint is an endpoint who dispatches a SPARQL query to several endpoints member of a federation. It processes and returns the union of the query results, processing aggregates, if any, on the union of the results. It is equivalent to a query with a union of service clauses on every endpoint of the federation. -A federation is an URL associated to a list of SPARQL endpoints. It is defined using a Turtle format configuration file, as shown below. - -``` - a st:Federation ; -st:definition ( - - - -) -``` - - -The idea is that a federation is hidden behind a single SPARQL endpoint URL. -The provenance parameter returns the URL of the target endpoint for each result. - -``` -http://corese.inria.fr/d2kab/sparql?mode=provenance& -query=select * where { ?s rdfs:label ?l filter regex(?l, "bio") } limit 10 -``` - - -A variant of federated SPARQL endpoint splits and rewrites the SPARQL query with appropriate service clauses. The endpoint URL is defined with /federate instead of /sparql. - -``` -http://corese.inria.fr/d2kab/federate?query=select where {} -``` - - -### Federated endpoint explain mode - -Federated endpoint with mode explain generate Linked Result for source selection query and results, rewritten federated query. Linked Result also for intermediate service call and service results. -It works for federated engine but also for sparql engine with a standard query with services, sent to corese server with /sparql?mode=why. -The interpreter logs intermediate services and results and at the end, in case of mode=why, it generates Linked Results. - -Corese GUI display Linked Results in several query panels with their results. -Intermediate service call can be executed again in GUI. -GUI can save and load query results with Linked Result. Hence we can keep track of federated query results during the lifetime of the endpoint because documents are managed on server side in temporary files. - -``` -mode=why -``` - - -### Compiler Service - -Compile a federated query as 1) select source query, 2) federated query with service clauses. Return result of select query. Generate two link href documents for select and federated query. - -``` -mode=compile - - - -``` - - -### Evaluation Report Service - -A report can be generated for federated query. Report is stored in a document and an URL for this document is stored in the link href tag of the query result. - -``` -mode=log - - -``` - - -### Transformation Service - -SPARQL endpoint where the result of the SPARQL query is transformed using an STTL transformation specified using a transform parameter. -There may be several transformation parameters. - -Specific transformation URI for sparql query result format. - -``` -transform=st:xml | st:json | st:rdf | st:all -``` - - -#### Linked Result - -The result of a transformation may be stored in a document and an URL for this document is stored in the link href tag of the query result. - -``` -mode=link - - -``` - - -#### Transformation - -The stm:mapper transformation generates a map when query solution contains variables "location", "lat", and "lon". - -``` -transform=stm:mapper -``` - - -Transformation d3:chart for graphic chart - -``` -transform=d3:chart -``` - - -Transformation d3:hierarchy for class hierarchy, d3:graphic for graph, d3:all for both. - -``` -transform=d3:hierarchy -transform=d3:graphic -transform=d3:all -``` - - -### SHACL Service - -Execution of SHACL shapes and execution of a SPARQL query on the SHACL validation report graph. Parameter shacl-shape-url is the URL of a SHACL document that contains the shapes to be evaluated. - -``` -/sparql? -mode=shacl& -uri=shacl-shape-url& -query=select * where { ?report sh:conforms ?b } -``` - - -### Service with Before & After - -Exemple of service where queries are executed before and after the main query. Parameters uri are URL of SPARQL query documents. - -``` -mode=before&uri=url1&mode=after&uri=url2 -``` diff --git a/docs/source/conf.py b/docs/source/conf.py index 74c551ce3..b959c5025 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -20,6 +20,24 @@ project = 'Corese' author = 'Wimmics' +version = "" +release = "" + +def setup(app): + def set_version(app, config): + smv = getattr(app.config, 'smv_current_version', None) + if smv: + config.version = smv + config.release = smv + html_theme_options["switcher"]["version_match"] = smv + else: + config.version = "dev" + config.release = "dev" + html_theme_options["switcher"]["version_match"] = "dev" + + app.connect("config-inited", set_version) + + # -- General configuration --------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration @@ -63,14 +81,16 @@ html_js_files = [] # Project logo, to place at the top of the sidebar. -html_logo = "_static/logo/corese-core_doc_bar.svg" +html_logo = "_static/logo/corese-core.svg" # Icon to put in the browser tab. -html_favicon = "_static/logo/corese-core_doc_fav.svg" +html_js_files = [ + "js/favicon-theme.js" +] # Modify the title to get good social-media links -html_title = "Corese" -html_short_title = "Corese" +html_title = "Corese-core Documentation" +html_short_title = "Corese-core Docs" # -- Theme Options ----------------------------------------------------------- # Theme options are theme-specific and customize the look and feel of a theme @@ -78,9 +98,9 @@ # documentation. html_theme_options = { "logo": { - "image_relative": "_static/logo/corese-core_doc_light.svg", - "image_light": "_static/logo/corese-core_doc_light.svg", - "image_dark": "_static/logo/corese-core_doc_dark.svg" + "image_relative": "_static/logo/corese-core.svg", + "image_light": "_static/logo/corese-core.svg", + "image_dark": "_static/logo/corese-core.svg" }, "theme_switcher": True, "navbar_center": [ "navbar-nav" ], @@ -92,11 +112,12 @@ "icon": "fab fa-github-square" } ], - "switcher": {"json_url": "https://corese-stack.github.io/corese-core/switcher.json", "version_match": r"v\d+\.\d+\.\d+"} + "switcher": {"json_url": "https://corese-stack.github.io/corese-core/switcher.json", "version_match": version} } html_sidebars = { - "install": [], + "install": [], + "user_guide": [], } # -- MySt-parcer extension Options ------------------------------------------- @@ -149,8 +170,6 @@ # Tell sphinx what the pygments highlight language should be. highlight_language = 'java' -# Setup the sphinx.ext.todo extension - # Set to false in the final version todo_include_todos = True diff --git a/docs/source/federation/federated_and_distributed_queries.md b/docs/source/federation/federated_and_distributed_queries.md deleted file mode 100644 index 5e0b804f0..000000000 --- a/docs/source/federation/federated_and_distributed_queries.md +++ /dev/null @@ -1,92 +0,0 @@ -# Federated Queries and Federation - -Corese facilitates Federated Queries, enabling users to execute SPARQL queries seamlessly across multiple data sources or endpoints. This document guides you through utilizing Federated Queries and Federation in Corese and details the configuration necessary. - -1. [Federated Queries and Federation](#federated-queries-and-federation) - 1. [1. Federated Queries](#1-federated-queries) - 2. [2. Federations](#2-federations) - -## 1. Federated Queries - -**Basic syntax:** - -Use the `@federate` directive to specify different endpoints. Below is the basic syntax: - -```sparql -@federate … -select * where {?x ?p ?y} -``` - -**Retrieving Provenance Information:** - -To obtain additional details on the origin of the data, include the `@provenance` keyword: - -```sparql -@federate … -@provenance -select * where {?x ?p ?y} -``` - -**Configuration for Corese-Server:** - -In Corese-Server, it's necessary to explicitly specify the list of allowed endpoints. For more details, refer to [Restrict Access to External Endpoints](https://github.com/Wimmics/corese/blob/master/docs/getting%20started/Getting%20Started%20With%20Corese-server.md#4-restrict-access-to-external-endpoints). - -For non-public servers, the `-su` option can be used to allow all endpoints: - -```shell -java -jar corese-server.jar -su -``` - -This option executes the server in superuser mode, allowing connections to all endpoints. This setting is not recommended for public servers due to security concerns. - -## 2. Federations - -A Federation in Corese is a named set of endpoints, defined in a file to avoid the repetition of listing endpoints in each query. - -**Defining a Federation:** - -1. Create a `federation.ttl` file with the following content: - -```turtle -# Define a federation - a st:Federation ; - st:definition ( - - - ). -``` - -2. Next, create a `config.properties` file with the following line - -```properties -FEDERATION = /path/to/federation.ttl -``` - -3. Launch Corese using the `config.properties` file: - -```shell -java -jar corese-server.jar -init config.properties -``` - -```shell -java -jar corese-gui.jar -init config.properties -``` - -```shell -echo "" | java -jar corese-command.jar sparql -if turtle -q ./query.rq --init config.properties -``` - -> Note: `echo ""` and `-if` turtle are workaround methods as this command is not designed to function without input. - -4. Finally, execute a federated query using the federation: - -```sparql -@federation -select * where {?x ?p ?y} -``` - - diff --git a/docs/source/index.rst b/docs/source/index.rst index 80a27df4b..2cc8a5c68 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -7,6 +7,13 @@ .. image:: _static/logo/corese-core_doc_light.svg :align: center :width: 400px + :class: only-light + +.. image:: _static/logo/corese-core_doc_dark.svg + :align: center + :width: 400px + :class: only-dark + Corese is a software platform that implements and extends the standards of the Semantic Web. It enables users to create, manipulate, parse, serialize, query, reason about, and validate RDF data. @@ -58,9 +65,9 @@ Corese-Core is a Java library that provides the core functionalities of Corese. Corese offers several interfaces ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ * `corese-core `_: Java library to process RDF data and use Corese features via an API. - * `corese-server `_: Tool to easily create, configure and manage SPARQL endpoints. - * `corese-gui `_: Graphical interface that allows an easy and visual use of Corese features. * `corese-command `_: Command Line Interface for Corese that allows users to interact with Corese features from the terminal. + * `corese-gui `_: Graphical interface that allows an easy and visual use of Corese features. + * `corese-server `_: Tool to create, configure and manage SPARQL endpoints. * `corese-python (beta) `_: Python wrapper for accessing and manipulating RDF data with Corese features using py4j. .. raw:: html @@ -71,7 +78,7 @@ Corese-Core is a Java library that provides the core functionalities of Corese. .. _issue reports: https://github.com/corese-stack/corese-core/issues .. _pull requests: https://github.com/corese-stack/corese-core/pulls -For support questions, comments, and any ideas for improvements you`d like to discuss, please use our `discussion forum`_. We welcome everyone to contribute to `issue reports`_, suggest new features, and create `pull requests`_. +For any questions, comments, or improvement ideas, please use our `discussion forum`_. We welcome everyone to contribute via `issue reports`_, suggest new features, and create `pull requests`_. .. ############################################################################# @@ -94,6 +101,6 @@ Home Page .. toctree:: :hidden: - Installation + Installation User Guide Java API diff --git a/docs/source/install.md b/docs/source/install.md deleted file mode 100644 index 09e3f8f5a..000000000 --- a/docs/source/install.md +++ /dev/null @@ -1,57 +0,0 @@ -# Installation - -## Corese Core - -[![Badge Maven Central](./_static/logo/badge_maven.svg)](https://central.sonatype.com/artifact/fr.inria.corese/corese-core) [![Badge Github Release](./_static/logo/badge_github.svg)](https://github.com/corese-stack/corese-core/releases) - -```xml - - - fr.inria.corese - corese-core - X.Y.Z - -``` - -```Groovy -// Gradle -implementation 'fr.inria.corese:corese-core:X.Y.Z' // replace X.Y.Z with the version number -``` - -## Storage Modules - -**Jena storage:** - -[![Badge Maven Central](./_static/logo/badge_maven.svg)](https://central.sonatype.com/artifact/fr.inria.corese/corese-jena) [![Badge Github Release](./_static/logo/badge_github.svg)](https://github.com/corese-stack/corese-plugin-storage-jena/releases) - -```xml - - - fr.inria.corese - corese-jena - X.Y.Z - -``` - -```Groovy -// Gradle -implementation 'fr.inria.corese:corese-jena:X.Y.Z' // replace X.Y.Z with the version number -``` - -**RDF4J storage:** - -[![Badge Maven Central](./_static/logo/badge_maven.svg)](https://central.sonatype.com/artifact/fr.inria.corese/corese-rdf4j) [![Badge Github Release](./_static/logo/badge_github.svg)](https://github.com/corese-stack/corese-plugin-storage-rdf4j/releases) - -```xml - - - fr.inria.corese - corese-rdf4j - X.Y.Z - -``` - -```Groovy -// Gradle -implementation 'fr.inria.corese:corese-rdf4j:X.Y.Z' // replace X.Y.Z with the version number -``` diff --git a/docs/source/install.rst b/docs/source/install.rst new file mode 100644 index 000000000..66ebff30f --- /dev/null +++ b/docs/source/install.rst @@ -0,0 +1,41 @@ +.. _installation: + +Installation +============ + +You can use **Corese-Core** with Maven, Gradle, or by downloading the JAR directly from GitHub or Maven Central. + +Maven +----- + +Add the following dependency to your ``pom.xml`` file: + +.. code-block:: xml + + + fr.inria.corese + corese-core + 4.6.4 + + +Gradle +------ + +Add this line to the ``dependencies`` section of your ``build.gradle`` file: + +.. code-block:: groovy + + implementation 'fr.inria.corese:corese-core:4.6.4' + +Direct Download +--------------- + +.. raw:: html + diff --git a/docs/source/storage/configuring_and_connecting_to_different_storage_systems_in_corese.md b/docs/source/storage/configuring_and_connecting_to_different_storage_systems_in_corese.md deleted file mode 100644 index 2ec5da9ab..000000000 --- a/docs/source/storage/configuring_and_connecting_to_different_storage_systems_in_corese.md +++ /dev/null @@ -1,230 +0,0 @@ -# Configuring and Connecting to Different Storage Systems in Corese - -Corese offers the possibility to connect to a range of storage systems for storing and managing your RDF data. In this document, you'll find information on how to use these storage systems with Corese, including instructions on configuring and utilizing them in the GUI, Server, and Library. - -## 1. Introduction - -In Corese versions prior to 4.4, graphs were loaded and manipulated in RAM. With the introduction of Data Manager in version 4.4, the Corese engine and storage systems are now fully decoupled. This offers several advantages such as : - -- The ability to easily add new storage systems -- The use of persistent storage systems -- The simultaneous use of multiple storage systems -- The flexibility to choose a storage system that meets specific needs - -## 2. What is a Data Manager? - -A data manager in Corese is a bridge between the Corese engine and a storage system, enabling the engine to store and manage graph data in a variety of storage systems while abstracting away the underlying details of those systems. - -A data manager is implemented as an interface called `Datamanager`, and concrete implementations such as `JenaTdb1DataManager` and `Rdf4jModelDataManager` are provided for specific storage systems. By implementing the `Datamanager` interface, it is possible to use the Corese engine with any storage structure. - -Currently, there are three implementations of `Datamanager` available: - -- `JenaTdb1DataManager` for Jena TDB1 storage -- `Rdf4jModelDataManager` for RDF4J model -- `CoreseGraphDataManager` for Corese graph - -## 3. Available Data Manager Implementations - -### 3.1. Jena TDB1 - -[Jena](https://jena.apache.org/) is an open-source Semantic Web framework written in Java and developed by the [Apache Jena project](https://jena.apache.org/). It provides a set of libraries and tools for building applications that process and manipulate RDF data. - -[TDB](https://jena.apache.org/documentation/tdb/) is a native triple store for Jena, designed to efficiently store and query large amounts of RDF data. It supports the full range of RDF languages and standards. - -The `JenaTdb1DataManager` allows the Corese engine to connect to a TDB1 database. Corese can connect to an existing Jena TDB1 database or create a new one. - -TDB1 is a persistent storage system that supports transactions and native concurrent access. In our larger tests, it has been able to efficiently load and manage graphs with up to 600 million triples. However, it is likely capable of handling even larger graph sizes. - -Here is a summary of TDB1's characteristics: - -| Characteristic | Description | -| ------------------------ | ----------- | -| Persistence of data | Yes | -| Native concurrent access | Yes | - -> You can use all the native tools and [Apache Jena - TDB Command-line Utilities](https://jena.apache.org/documentation/tdb/commands.html) . For example, you can use `tdbloader`, a software, to efficiently create a TDB1 database from serialized RDF data. - -### 3.2. Corese Graph - -Corese Graph is the historical API of Corese for storing and manipulating RDF data in memory. It is stable and optimized, and capable of handling large graphs within the limits of available RAM. - -However, Corese Graph does not offer persistence of data and supports native concurrent access. - -Here is a summary of Corese Graph's characteristics: - -| Characteristic | Description | -| ------------------------ | ----------- | -| Persistence of data | No | -| Native concurrent access | Yes | - -### 3.3. RDF4J Model - -[RDF4J](https://rdf4j.org/) is an open-source Java library for working with RDF data. It provides a set of APIs for parsing and serializing RDF, querying with SPARQL, and modeling RDF data with RDFS and OWL. - -[The RDF Model API](https://rdf4j.org/documentation/programming/model/) is a Java interface for storing and manipulating RDF data in memory (It does not store data on disk). This API provides a high-level, abstract representation of an RDF graph. The `Rdf4JModelDataManager` allows the Corese engine to connect to an existing RDF4J `Model` object or create a new one. - -This implementation is not optimized for storing large amounts of data and does not support persistence of data, concurrency, or transactions. It was our first implementation as a proof-of-concept and is not recommended for use in production environments. However, it may still be useful for small-scale testing and development purposes. - -Here is a summary of the RDF4J Model's characteristics: - -| Characteristic | Description | -| ------------------------ | ----------- | -| Persistence of data | No | -| Native concurrent access | No | - -## 4. Configuring Storage Systems in Corese-GUI and Corese-Server - -To configure storage systems in the Corese GUI or Server, it is necessary to create a properties file. This file should include the `STORAGE` configuration property, which specifies the storage systems to use. - -To run Corese-GUI or Corese-Server with a property file, the `-init` option must be used. For instance, the following bash command runs Corese-GUI using the `gui.properties` file: - -```bash -java -jar corese.jar -init "config.properties" -``` - -This will load the storage systems specified in the `STORAGE` property in the `config.properties` file. - -> If no configuration file is specified, Corese will use the default configuration, which is to use a single Corese graph storage system in memory. This behavior is the same as in versions prior to 4.4. - -### 4.1. Configuring One Storage System - -To configure a single storage system, you need to specify the type and ID of the system in the `STORAGE` property. You can also include optional parameters for the system. - -```properties -STORAGE = TYPE_BD1,ID_DB1,PARAM_BD1 -``` - -The fields are as follows: - -`TYPE_BD1`: The type of storage system to use. Possible values are `jena_tdb1`, `rdf4j_model`, and `corese_graph`. - -`ID_DB1`: The ID of the storage system. This ID will be used to reference the storage system in SPARQL queries. - -`PARAM_BD1`: (Optional) Constructor parameter for the storage system. - -| DB type | Parameter | -| ------------ | ---------------------------------------------------------------------------- | -| jena_tdb1 | Empty (use JenaTDB in memory) or path of TDB1 database (use JenaTDB in a DB) | -| rdf4j_model | Empty | -| corese_graph | Empty | - -For example, to configure a Jena TDB1 storage system with ID `musicDB` and the `/…/music` directory as the storage location, the following `STORAGE` property should be specified: - -```properties -STORAGE = jena_tdb1,musicDB,/…/music -``` - -### 4.2. Configuring Multiple Storage Systems - -To configure multiple storage systems in Corese, simply separate the configurations for each storage system with a semicolon (`;`). This is similar to configuring a single storage system, as described in the previous section. - -```properties -STORAGE = TYPE_BD1,ID_DB1,PARAM_BD1;TYPE_BD2,ID_DB2,PARAM_BD2;… -``` - -In the case where multiple storage systems are configured, the first storage system listed is the default storage system. It is accessible directly in SPARQL queries, while the other storage systems must be accessed using the `SERVICE` keyword. - -For example, given the following configuration: - -```properties -STORAGE = corese_graph,friend;jena_tdb1,mélomane;jena_tdb1,music -``` - -The following SPARQL query retrieves information about a person's friends and the music they like: - -```sparql -PREFIX music: -PREFIX person: -PREFIX foaf: - -SELECT ?friendName ?artistName ?description -WHERE { - # Select Casandra's friends from the "friend" database (default dataset) - person:Casandra foaf:knows ?friend . - - # Retrieve the Casandra's friend's name and the artist they like from the "mélomane" database - SERVICE { - ?friend person:likeMusic ?artist . - ?friend foaf:firstName ?friendName . - } - - # Retrieve the artist's name and a description of their music from the "music" database - SERVICE { - ?artist music:name ?artistName . - ?artist music:description ?description . - } -} -``` - -To execute a query in the GUI, open the Corese GUI and enter the query in the SPARQL Query tab. - -To execute in the Server, send a request to the endpoint `http://localhost:8080/sparql` if you are running it locally. - -### 4.3. [Optional] Assign storage to a specific SPARQL endpoint with Corese Server - -Corese Server allows you to assign storage to a specific SPARQL endpoint by define a properties file (eg: `server.properties`) and a profile file (eg: `profile.ttl`). The properties file defines the storage systems available and their locations, while the profile file assigns a storage system to a specific endpoint. - -To create two storage systems, `db1` and `db2`, using the Jena TDB1 storage system and located at `/…/album` and `/…/music`, respectively, you can use the following in the properties file: - -```properties -STORAGE = jena_tdb1,db1,/…/album;jena_tdb1,db2,/…/music -``` - -To assign the `db1` and `db2` storage systems to the `album` (``) and `music` (``) endpoints, respectively, you can use the following in the profile file: - -```turtle -prefix st: - -# Album endpoint, available at http://localhost:8080/album/sparql -<#_1> a st:Server; - st:service "album"; # Assigns the name "album" to this endpoint - st:storage "db1". # Assigns the "db1" storage system to this endpoint - -# Music endpoint, available at http://localhost:8080/music/sparql -<#_2> a st:Server; - st:service "music"; # Assigns the name "music" to this endpoint - st:storage "db2". # Assigns the "db2" storage system to this endpoint -``` - -With this configuration, the endpoint `` will use `db1` data, and `` will use `db2` data. - -To start the server with these configurations, run the following command: - -```bash -java -jar corese-server.jar -init "server.properties" -pp "profile.ttl" -``` - -> You can learn more about profile files here: [Getting Started With corese-server](https://github.com/Wimmics/corese/blob/master/docs/getting%20started/Getting%20Started%20With%20Corese-server.md#4-to-go-deeper) - -## 5. Use Storage Systems in Corese-Library - -To build a `dataManager` using the Corese-Library, you can use a `dataManager` builder class to configure and create the `dataManager`. There are different types of `dataManager` builders available, depending on the type of `dataManager` you want to create. - -For example, the `JenaTdb1DataManagerBuilder` can be used to build a `JenaTdb1DataManager`. To build a `JenaTdb1DataManager` with a specific storage path, you can use the following code: - -```java -JenaTdb1DataManagerBuilder builder = new JenaTdb1DataManagerBuilder(); -builder.setStoragePath("storage/path"); -JenaTdb1DataManager dataManager = builder.build(); -``` - -Similarly, you can use the `CoreseGraphDataManagerBuilder` to build a `CoreseGraphDataManager` or the `Rdf4jModelDataManagerBuilder` to build an `Rdf4jModelDataManager`. - -To execute a query on the `dataManager`, you can use the `QueryProcess` class as follows: - -```java -// Create a QueryProcess using the dataManager -QueryProcess exec = QueryProcess.create(dataManager); - -// Execute a SPARQL query and retrieve the result as a Mappings object -Mappings map = exec.query("select * where { ?s ?p ?o }"); - -// Print the results of the query -for (Mapping m : map) { - System.out.println(m); -} -``` - -This will execute the specified SPARQL query on the `dataManager` and print the results. - -> You can learn more about Corese-Library here: [Getting Started With Corese-library]([corese/Getting Started With Corese-library.md at master · Wimmics/corese · GitHub](https://github.com/Wimmics/corese/blob/master/docs/getting%20started/Getting%20Started%20With%20Corese-library.md)) diff --git a/docs/source/getting_started/getting_started_with_corese-core.md b/docs/source/user_guide.md similarity index 100% rename from docs/source/getting_started/getting_started_with_corese-core.md rename to docs/source/user_guide.md diff --git a/docs/source/user_guide.rst b/docs/source/user_guide.rst deleted file mode 100644 index d6dfd0bbf..000000000 --- a/docs/source/user_guide.rst +++ /dev/null @@ -1,31 +0,0 @@ -User Guide -========== - -.. toctree:: - :hidden: - - Getting started - Storage systems - Federated and distributed queries - SPARQL service extension - -Welcome to the Corese-Core User Guide! This guide will help you get started with Corese-Core, configure storage systems, perform federated queries, and extend SPARQL functionalities. - -Contents: ---------- - -1. Getting Started with Corese-Core - Learn how to use main features of Corese-Core. - `Getting started with Corese-Core `_ - -2. Configuring Storage Systems - Guide to configuring and connecting to different storage systems. - `Configuring and connecting to different storage systems in Corese `_ - -3. Federated Queries - Run distributed and federated queries seamlessly. - `Federated and distributed queries `_ - -4. Advanced SPARQL Features - Explore advanced SPARQL extensions for enhanced query capabilities. - `SPARQL service extension `_ diff --git a/docs/switcher_generator.sh b/docs/switcher_generator.sh index df73bcb0b..63f69306d 100755 --- a/docs/switcher_generator.sh +++ b/docs/switcher_generator.sh @@ -10,7 +10,7 @@ fi json_output_file="$1" html_output_file="$2" -# Set minimal version (before which no documentation would be generated in a compatible way) +# Set minimal version (before which no documentation would be generated in a compatible way) minimal_version="4.6.0" # Get all Git tags and filter by the form vX.Y.Z (semantic versioning) @@ -53,19 +53,19 @@ for tag in $tags; do is_first=false else preferred="false" - name="$tag (stable)" + name="$tag" fi # Create a JSON object for the tag - json_object=$(cat < skip + ; + +// Terminals + +Graph_w options { caseInsensitive=true; } + : 'GRAPH' + ; + +Base_w options { caseInsensitive=true; } + : 'BASE' + ; + +Prefix_w options { caseInsensitive=true; } + : 'PREFIX' + ; + +BooleanLiteral + : 'true' + | 'false' + ; + +IRIREF + : '<' (PN_CHARS | '.' | ':' | '#' | '@' | '%' | '&' | '$' | '!' | '\'' | '*' | '+' | '/' | '(' | ')' | '-' | ',' | '?' | '~' | UCHAR)* '>' + ; + +PNAME_NS + : PN_PREFIX? ':' + ; + +PNAME_LN + : PNAME_NS PN_LOCAL + ; + +BLANK_NODE_LABEL + : '_:' (PN_CHARS_U | '0' .. '9') ((PN_CHARS | '.')* PN_CHARS)? + ; + +LANGTAG + : '@' ('a'.. 'z' | 'A' .. 'Z')+ ('-' ('a'.. 'z' | 'A' .. 'Z' | '0' .. '9')* )* + ; + +INTEGER + : ('+' | '-' )? ('0' .. '9')+ + ; + +DECIMAL + : ('+' | '-' )? ('0' .. '9')* '.' ('0' .. '9')+ + ; + +DOUBLE + : ('+' | '-' )? (('0' .. '9')+ '.' ('0' .. '9')* EXPONENT + | '.' ('0' .. '9')+ EXPONENT + | ('0' .. '9')+ EXPONENT) + ; + +EXPONENT + : ('e' | 'E') ('+' | '-' )? ('0' .. '9')+ + ; + +STRING_LITERAL_QUOTE + : '"' ((~[\u0022\u005C\u0010\u0013]) | ECHAR | UCHAR)* '"' + ; + +STRING_LITERAL_SINGLE_QUOTE + : '\'' ((~[\u0027\u005C\u0010\u0013]) | ECHAR | UCHAR)* '\'' + ; + +STRING_LITERAL_LONG_SINGLE_QUOTE + : '\'\'\'' (('\'' | '\'\'')? ( (~['\\] ) | ECHAR | UCHAR))* '\'\'\'' + ; + +STRING_LITERAL_LONG_QUOTE + : '"""' (('"' | '""')? ( (~["'] ) | ECHAR | UCHAR))* '"""' + ; + +UCHAR + : '\\u' HEX HEX HEX HEX + | '\\U' HEX HEX HEX HEX HEX HEX HEX HEX + ; + +ECHAR options { caseInsensitive=true; } + : '\\' [tbnrf"'\\] + ; + +WHITESPACE + : [\u0020\u0009\u000A\u000D] + ; + +ANON + : '[' WHITESPACE* ']' + ; + +PN_CHARS_BASE + : 'A' .. 'Z' + | 'a' .. 'z' + | '\u00C0' .. '\u00D6' + | '\u00D8' .. '\u00F6' + | '\u00F8' .. '\u02FF' + | '\u0370' .. '\u037D' + | '\u037F' .. '\u1FFF' + | '\u200C' .. '\u200D' + | '\u2070' .. '\u218F' + | '\u2C00' .. '\u2FEF' + | '\u3001' .. '\uD7FF' + | '\uF900' .. '\uFDCF' + | '\uFDF0' .. '\uFFFD' +// | '\u10000' .. '\uEFFFF' + ; + +PN_CHARS_U + : PN_CHARS_BASE + | '_' + ; + +PN_CHARS + : PN_CHARS_U + | '-' + | [0-9] + | [\u00B7] + | [\u0300-\u036F] + | [\u203F-\u2040] + ; + +PN_PREFIX + : PN_CHARS_BASE ((PN_CHARS | '.')* PN_CHARS)? + ; + +PN_LOCAL + : (PN_CHARS_U | ':' | [0-9] | PLX) ((PN_CHARS | '.' | ':' | PLX)* (PN_CHARS | ':' | PLX))? + ; + +PLX + : PERCENT + | PN_LOCAL_ESC + ; + +PERCENT + : '%' HEX HEX + ; + +HEX + : [0-9a-fA-F] + ; + +PN_LOCAL_ESC + : '\\' ( + '_' + | '~' + | '.' + | '-' + | '!' + | '$' + | '&' + | '\'' + | '(' + | ')' + | '*' + | '+' + | ',' + | ';' + | '=' + | '/' + | '?' + | '#' + | '@' + | '%' + ) + ; + +LC + : '#' ~[\r\n]+ -> channel(HIDDEN) + ; \ No newline at end of file diff --git a/src/main/java/fr/inria/corese/core/next/api/base/model/AbstractStatement.java b/src/main/java/fr/inria/corese/core/next/api/base/model/AbstractStatement.java index 19707d6c7..3b5a5749a 100644 --- a/src/main/java/fr/inria/corese/core/next/api/base/model/AbstractStatement.java +++ b/src/main/java/fr/inria/corese/core/next/api/base/model/AbstractStatement.java @@ -59,10 +59,10 @@ public int hashCode() { @Override public String toString() { return "(" - + getSubject() - + ", " + getPredicate() - + ", " + getObject() - + (getContext() == null ? "" : ", " + getContext()) + + getSubject().stringValue() + + ", " + getPredicate().stringValue() + + ", " + getObject().stringValue() + + (getContext() == null ? "" : ", " + getContext().stringValue()) + ")"; } } \ No newline at end of file diff --git a/src/main/java/fr/inria/corese/core/next/impl/common/util/IRIUtils.java b/src/main/java/fr/inria/corese/core/next/impl/common/util/IRIUtils.java index bb1f83146..7c4a819b3 100644 --- a/src/main/java/fr/inria/corese/core/next/impl/common/util/IRIUtils.java +++ b/src/main/java/fr/inria/corese/core/next/impl/common/util/IRIUtils.java @@ -10,9 +10,10 @@ */ public class IRIUtils { - private static final Pattern IRI_PATTERN = Pattern.compile("^(([\\w\\-]+:\\/\\/([\\w\\-_:]+\\.)*[\\w\\-_:]*)(\\/([\\w\\-\\._\\:]+\\/)*))([\\w\\-\\._\\:]+)?(\\?[\\w\\-_\\:\\?\\=]+)?((\\#)?([\\w\\-_]+))?$"); + private static final Pattern IRI_PATTERN = Pattern.compile("^(?(?[\\w\\-]+):(?\\/\\/)?(?([\\w\\-_:@]+\\.)*[\\w\\-_:]*))((?\\/([\\w\\-\\._\\:]+\\/)*)(?[\\w\\-\\._\\:]+)?(?\\?[\\w\\-_\\:\\?\\=]+)?(\\#)?(?([\\w\\-_]+))?)?$"); private static final Pattern STANDARD_IRI_PATTERN = Pattern.compile("^(([^:/?#\\s]+):)(\\/\\/([^/?#\\s]*))?([^?#\\s]*)(\\?([^#\\s]*))?(#(.*))?"); + /** * Prevent instantiation of the utility class. */ @@ -29,15 +30,24 @@ public static String guessNamespace(String iri) { Matcher matcher = IRI_PATTERN.matcher(iri); if(matcher.matches()) { - if((matcher.group(8) == null) || (matcher.group(6) == null && matcher.group(9) == null) ) { // If the IRI has no fragment or ends with a slash - - return matcher.group(1); - } else { - // 1: Domain and path ending with a slash, 6: final path element without slash, 9: final # if there is a fragment - return matcher.group(1) + matcher.group(6) + matcher.group(9); + if(matcher.group("protocol") != null && matcher.group("protocol").equals("_")) { + return ""; + } + StringBuilder namespace = new StringBuilder(); + namespace.append(matcher.group("protocol")).append(":"); + if(matcher.group("dblSlashes") != null) { + namespace.append(matcher.group("dblSlashes")); } + namespace.append(matcher.group("domain")); + if(matcher.group("path") != null) { + namespace.append(matcher.group("path")); + } + if(matcher.group("fragment") != null && matcher.group("finalPath") != null) { + namespace.append(matcher.group("finalPath")).append("#"); + } + return namespace.toString(); } else { - return ""; + throw new IllegalStateException("No namespace found for the given IRI: " + iri + "."); } } catch (IllegalStateException e) { return ""; @@ -54,10 +64,10 @@ public static String guessLocalName(String iri) { Matcher matcher = IRI_PATTERN.matcher(iri); if(matcher.matches()) { - if(matcher.group(10) != null){ // If the IRI has a fragment - return matcher.group(10); - } else if(matcher.group(6) != null ) { // If the IRI has no fragment but do not ends with a slash - return matcher.group(6); + if(matcher.group("fragment") != null){ // If the IRI has a fragment + return matcher.group("fragment"); + } else if(matcher.group("finalPath") != null ) { // If the IRI has no fragment but do not ends with a slash + return matcher.group("finalPath"); } else { // If the URI ends with a slash return ""; } diff --git a/src/main/java/fr/inria/corese/core/next/impl/io/parser/ParserFactory.java b/src/main/java/fr/inria/corese/core/next/impl/io/parser/ParserFactory.java index 8b43d930a..f49af0569 100644 --- a/src/main/java/fr/inria/corese/core/next/impl/io/parser/ParserFactory.java +++ b/src/main/java/fr/inria/corese/core/next/impl/io/parser/ParserFactory.java @@ -9,6 +9,7 @@ import fr.inria.corese.core.next.impl.io.parser.jsonld.JSONLDParser; import fr.inria.corese.core.next.impl.io.parser.nquads.ANTLRNQuadsParser; import fr.inria.corese.core.next.impl.io.parser.ntriples.ANTLRNTriplesParser; +import fr.inria.corese.core.next.impl.io.parser.rdfxml.RDFXMLParser; import fr.inria.corese.core.next.impl.io.parser.turtle.ANTLRTurtleParser; /** @@ -44,6 +45,8 @@ public RDFParser createRDFParser(RDFFormat format, Model model, ValueFactory fac return new ANTLRNTriplesParser(model, factory, config); } else if (format == RDFFormat.NQUADS) { return new ANTLRNQuadsParser(model, factory, config); + } else if (format == RDFFormat.RDFXML) { + return new RDFXMLParser(model, factory, config); } throw new IllegalArgumentException("Unsupported format: " + format); } @@ -65,6 +68,8 @@ public RDFParser createRDFParser(RDFFormat format, Model model, ValueFactory fac return new ANTLRNTriplesParser(model, factory); } else if (format == RDFFormat.NQUADS) { return new ANTLRNQuadsParser(model, factory); + } else if (format == RDFFormat.RDFXML) { + return new RDFXMLParser(model, factory); } throw new IllegalArgumentException("Unsupported format: " + format); } diff --git a/src/main/java/fr/inria/corese/core/next/impl/io/parser/rdfxml/RDFXMLParser.java b/src/main/java/fr/inria/corese/core/next/impl/io/parser/rdfxml/RDFXMLParser.java new file mode 100644 index 000000000..917fa0793 --- /dev/null +++ b/src/main/java/fr/inria/corese/core/next/impl/io/parser/rdfxml/RDFXMLParser.java @@ -0,0 +1,459 @@ +package fr.inria.corese.core.next.impl.io.parser.rdfxml; + +import fr.inria.corese.core.next.api.*; +import fr.inria.corese.core.next.api.base.io.RDFFormat; +import fr.inria.corese.core.next.api.base.io.parser.AbstractRDFParser; +import fr.inria.corese.core.next.api.io.IOOptions; +import fr.inria.corese.core.next.impl.common.vocabulary.RDF; +import fr.inria.corese.core.next.impl.exception.ParsingErrorException; +import fr.inria.corese.core.next.impl.io.parser.rdfxml.context.RDFXMLContext; +import org.xml.sax.Attributes; +import org.xml.sax.InputSource; +import org.xml.sax.helpers.DefaultHandler; + +import javax.xml.parsers.SAXParser; +import javax.xml.parsers.SAXParserFactory; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; + +import static fr.inria.corese.core.next.impl.io.parser.rdfxml.RDFXMLUtils.*; + +/** + * SAX-based RDF/XML parser using a shared parsing context ({@link RDFXMLContext}). + * + *

This parser processes RDF/XML documents using the SAX streaming API. + * It tracks RDF constructs (resources, properties, literals, containers, collections) + * using an explicit stack-based context, and supports features like xml:lang, + * rdf:datatype, rdf:parseType, and property attributes.

+ * + *

The parser adds RDF statements to the provided {@link Model} using + * the supplied {@link ValueFactory}. This parser supports nested nodes, + * blank nodes, typed nodes, and RDF collections.

+ */ +public class RDFXMLParser extends AbstractRDFParser { + + /** RDF/XML format identifier for this parser. */ + private final RDFFormat format = RDFFormat.RDFXML; + + /** Buffer for accumulating character data between start and end tags. */ + private StringBuilder characters = new StringBuilder(); + + /** Shared state across SAX callbacks. */ + private RDFXMLContext ctx; + + private final RDFXMLStatementEmitter emitter; + + /** + * Creates a new parser with a target RDF model and factory. + * + * @param model the RDF model to populate + * @param factory the RDF value factory for term creation + */ + public RDFXMLParser(Model model, ValueFactory factory) { + this(model, factory, null); + } + + /** + * Creates a new parser with a target RDF model, factory, and configuration options. + * + * @param model the RDF model to populate + * @param factory the RDF value factory for term creation + * @param config optional configuration options for the parser + */ + public RDFXMLParser(Model model, ValueFactory factory, IOOptions config) { + super(model, factory, config); + this.ctx = new RDFXMLContext(getModel(), getValueFactory()); + this.emitter = new RDFXMLStatementEmitter(model, factory); + } + + @Override + public RDFFormat getRDFFormat() { + return format; + } + + @Override + public void parse(InputStream in) throws ParsingErrorException { + parse(new InputStreamReader(in, StandardCharsets.UTF_8), null); + } + + @Override + public void parse(InputStream in, String baseURI) throws ParsingErrorException { + parse(new InputStreamReader(in, StandardCharsets.UTF_8), baseURI); + } + + public void parse(Reader reader) throws ParsingErrorException { + parse(reader, null); + } + + @Override + public void parse(Reader reader, String baseURI) throws ParsingErrorException { + ctx.baseURI = baseURI; + try { + SAXParserFactory factory = SAXParserFactory.newInstance(); + factory.setNamespaceAware(true); + SAXParser saxParser = factory.newSAXParser(); + InputSource inputSource = new InputSource(reader); + saxParser.parse(inputSource, new RdfXmlSaxHandler()); + } + catch (IOException e) { + throw new ParsingErrorException("Failed to parse RDF/XML input stream: " + e.getMessage() , e); + } catch (Exception e) { + throw new ParsingErrorException("Unexpected error during RDF/XML parsing: " + e.getMessage(), e); + } + } + + /** + * Internal SAX handler that delegates to the parser's methods + */ + private class RdfXmlSaxHandler extends DefaultHandler { + + @Override + public void characters(char[] ch, int start, int length) { + RDFXMLParser.this.handleCharacters(ch, start, length); + } + + @Override + public void startElement(String uri, String localName, String qName, Attributes attrs) { + RDFXMLParser.this.handleStartElement(uri, localName, qName, attrs); + } + + @Override + public void endElement(String uri, String localName, String qName) { + RDFXMLParser.this.handleEndElement(uri, localName, qName); + } + } + + /** + * Handles character data between XML elements + */ + private void handleCharacters(char[] ch, int start, int length) { + characters.append(ch, start, length); + } + + /** + * Handles opening of an XML element. + * Identifies node elements, container constructs, properties, + * and special parseType attributes, updating the parsing context accordingly. + */ + private void handleStartElement(String uri, String localName, String qName, Attributes attrs) { + + // Skip the top-level rdf:RDF wrapper element + if (RDFXMLUtils.isRdfRDF(uri, localName)) return; + + // Reset character buffer + characters.setLength(0); + + // Handle xml:base (change base URI dynamically) + updateBase(attrs); + + // Handle xml:lang + updateLang(attrs); + + // Handle rdf:datatype (applies to property literal values) + updateDatatype(attrs); + + if (processContainerElement(localName, uri, qName, attrs)) return; + if (processCollectionStart(localName, uri, qName, attrs)) return; + if (processCollectionItem(localName, uri, attrs)) return; + if (processNodeElement(localName, uri, qName, attrs)) return; + processPropertyElement(localName, uri, qName, attrs); + } + + /** + * Handles the end of an XML element, emitting a literal or cleaning up context stacks. + */ + private void handleEndElement(String uri, String localName, String qName) { + String text = characters.toString().trim(); + characters.setLength(0); + + if (!ctx.predicateStack.isEmpty() && !text.isEmpty()) { + IRI predicate = ctx.predicateStack.pop(); + Resource subject = ctx.subjectStack.peek(); + String datatypeUri = ctx.datatypeStack.isEmpty() ? null : ctx.datatypeStack.pop(); + //emitLiteral(subject, predicate, text, datatypeUri); + String lang = ctx.langStack.isEmpty() ? null : ctx.langStack.peek(); + emitter.emitLiteral(subject, predicate, text, datatypeUri, lang); + return; + } + cleanEndElement(uri, localName); + } + + /** + * Updates the base URI for IRI resolution using the xml:base attribute if present. + * + * @param attrs the XML attributes of the current element + */ + private void updateBase(Attributes attrs) { + String xmlBase = attrs.getValue("xml:base"); + if (xmlBase != null) ctx.baseURI = xmlBase; + } + + /** + * Updates the language context using the xml:lang attribute if present. + * The language value is pushed onto a stack to support nested scope. + * + * @param attrs the XML attributes of the current element + */ + private void updateLang(Attributes attrs) { + String xmlLang = attrs.getValue("xml:lang"); + if (xmlLang != null) ctx.langStack.push(xmlLang); + } + + + /** + * Updates the datatype context using the rdf:datatype attribute if present. + * The datatype URI is pushed onto a stack to support nested scope. + * + * @param attrs the XML attributes of the current element + */ + private void updateDatatype(Attributes attrs) { + String datatype = attrs.getValue(RDF.type.getNamespace(), "datatype"); + if (datatype != null) { + ctx.datatypeStack.push(datatype); + } + } + + /** + * Processes the start of an RDF collection indicated by parseType="Collection". + * Initializes the internal collection structures and returns true if this is a collection. + * + * @param localName the local name of the element + * @param uri the namespace URI + * @param qName the qualified name + * @param attrs the attributes of the element + * @return true if this element starts a collection, false otherwise + */ + private boolean processCollectionStart(String localName, String uri, String qName, Attributes attrs) { + if (!"Collection".equals(getParseType(attrs))) return false; + IRI predicate = ctx.factory.createIRI(RDFXMLUtils.expandQName(uri, localName, qName)); + prepareCollection(predicate); + return true; + } + + /** + * Prepares internal context to collect RDF list elements for a collection. + * + * @param predicate the predicate that points to the collection + */ + private void prepareCollection(IRI predicate) { + ctx.predicateStack.push(predicate); + ctx.collectionSubject = ctx.subjectStack.peek(); + ctx.collectionPredicate = predicate; + ctx.collectionBuilder = new ArrayList<>(); + ctx.inCollection = true; + } + + /** + * Processes an item inside an RDF collection. Adds the extracted subject to the collection list. + * + * @param localName the local name of the element + * @param uri the namespace URI + * @param attrs the attributes of the element + * @return true if the element is processed as a collection item, false otherwise + */ + private boolean processCollectionItem(String localName, String uri, Attributes attrs) { + if (!ctx.inCollection || !RDFXMLUtils.isDescription(localName, uri)) return false; + + Resource item = extractSubject(attrs, ctx.factory, ctx.baseURI); + ctx.collectionBuilder.add(item); + ctx.suppressSubject = true; + + return true; + } + + /** + * Processes RDF container elements like rdf:Bag, rdf:Seq, and code rdf:Alt, + * as well as container items like rdf:li and rdf:_n. + * + * @param localName the local name of the element + * @param uri the namespace URI + * @param qName the qualified name + * @param attrs the attributes of the element + * @return true if the element is a container or container item, false otherwise + */ + private boolean processContainerElement(String localName, String uri, String qName, Attributes attrs) { + // --- RDF Container Element --- + if (isContainer(localName, uri)) { + Resource subject = extractSubject(attrs, ctx.factory, ctx.baseURI); + ctx.subjectStack.push(subject); + ctx.inContainer = true; + ctx.liIndex = 1; + emitter.emitType(subject, expandQName(uri, localName, qName)); + + return true; + } + + // --- Container Items (rdf:li, rdf:_n) --- + + if (ctx.inContainer && RDF.type.getNamespace().equals(uri)) { + String pred = switch (localName) { + case "li" -> RDF.type.getNamespace() + "_" + ctx.liIndex++; + default -> localName.matches("_\\d+") ? RDF.type.getNamespace() + localName : null; + }; + + if (pred != null) { + IRI predicate = ctx.factory.createIRI(pred); + String resource = attrs.getValue("rdf:resource"); + if (resource != null) { + emitter.emitResourceTriple(ctx.subjectStack.peek(), predicate, resource, ctx.baseURI); + } + return true; + } + } + return false; + } + + /** + * Processes an RDF node element such as rdf:Description or a typed node. + * Handles subject creation, optional rdf:type triple emission, and property attributes. + * + * @param localName the local name of the element + * @param uri the namespace URI + * @param qName the qualified name + * @param attrs the element's attributes + * @return true if the element is processed as an RDF node, false otherwise + */ + private boolean processNodeElement(String localName, String uri, String qName, Attributes attrs) { + boolean isNode = isDescription(localName, uri) + || (ctx.subjectStack.isEmpty() && RDFXMLUtils.isNodeElement(attrs)); + + if (!isNode) return false; + + Resource newSubject = RDFXMLUtils.extractSubject(attrs, ctx.factory, ctx.baseURI); + + // Add triple if nested in another node as object + if (!ctx.predicateStack.isEmpty() && !ctx.subjectStack.isEmpty()) { + Resource parent = ctx.subjectStack.peek(); + IRI predicate = ctx.predicateStack.pop(); + emitter.emitTriple(parent, predicate, newSubject); + } + + ctx.subjectStack.push(newSubject); + + // Emit rdf:type if typed node + if (!isDescription(localName, uri)) { + emitter.emitType(newSubject, expandQName(uri, localName, qName)); + } + + // Handle non-syntax attributes + emitter.emitPropertyAttributes(newSubject, attrs); + return true; + } + + /** + * Processes an RDF property element and emits triples accordingly. + * Handles {@code rdf:resource}, {@code rdf:nodeID}, {@code parseType="Resource"}, + * and inline property attributes. + * + * @param localName the local name of the property element + * @param uri the namespace URI + * @param qName the qualified name + * @param attrs the element's attributes + * + * @return true if the element is processed as an RDF property element, false otherwise + */ + private boolean processPropertyElement(String localName, String uri, String qName, Attributes attrs) { + IRI predicate = ctx.factory.createIRI(RDFXMLUtils.expandQName(uri, localName, qName)); + ctx.predicateStack.push(predicate); + + String resource = attrs.getValue(RDF.type.getNamespace(), "resource"); + String nodeID = attrs.getValue(RDF.type.getNamespace(), "nodeID"); + + if (resource != null) { + emitter.emitResourceTriple(ctx.subjectStack.peek(), predicate, resource, ctx.baseURI); + ctx.predicateStack.pop(); + return true; + } + + if (nodeID != null) { + emitter.emitBNodeTriple(ctx.subjectStack.peek(), predicate, nodeID); + ctx.predicateStack.pop(); + return true; + } + + // parseType="Resource" + String parseType = getParseType(attrs); + if ("Resource".equals(parseType)) { + Resource bnode = emitBnodePredicateObject(predicate); + ctx.subjectStack.push(bnode); + return true; + } + + // Inline attributes + if (hasNonSyntaxAttributes(attrs)) { + Resource bnode = emitBnodePredicateObject(predicate); + emitter.emitPropertyAttributes(bnode, attrs); + ctx.predicateStack.pop(); + return true; + } + return false; + } + + /** + * Checks if the given attributes contain any non-syntax (i.e., user-defined) attributes. + * + * @param attrs the XML attributes to inspect + * @return true if at least one attribute is not a reserved RDF or XML syntax attribute + */ + private boolean hasNonSyntaxAttributes(Attributes attrs) { + for (int i = 0; i < attrs.getLength(); i++) { + if (!isSyntaxAttribute(attrs.getURI(i), attrs.getLocalName(i), attrs.getQName(i))) { + return true; + } + } + return false; + } + + /** + * Emits a blank node as the object of the current predicate and links it to the subject. + * + * @param predicate the predicate of the triple + * @return the newly created blank node + */ + private Resource emitBnodePredicateObject(IRI predicate) { + Resource parent = ctx.subjectStack.peek(); + Resource bnode = ctx.factory.createBNode(); + emitter.emitTriple(parent, predicate, bnode); + return bnode; + } + + + + /** + * Cleans up parsing context stacks when an XML end element is encountered. + * @param uri the namespace URI of the element + * @param localName the local name of the element + */ + private void cleanEndElement(String uri, String localName) { + if (!ctx.langStack.isEmpty()) ctx.langStack.pop(); + if (!ctx.predicateStack.isEmpty()) ctx.predicateStack.pop(); + if (RDFXMLUtils.isContainer(localName, uri)) { + if (!ctx.subjectStack.isEmpty()) ctx.subjectStack.pop(); + ctx.inContainer = false; + ctx.liIndex = 1; + return; + } + if (ctx.inCollection && localName.equals(ctx.collectionPredicate.getLocalName())) { + Resource listHead = createRdfCollection(ctx.collectionBuilder, ctx.model, ctx.factory); + ctx.model.add(ctx.factory.createStatement(ctx.collectionSubject, ctx.collectionPredicate, listHead)); + ctx.inCollection = false; + ctx.collectionBuilder.clear(); + return; + } + if (ctx.inCollection && RDFXMLUtils.isDescription(localName, uri)) { + if (!ctx.subjectStack.isEmpty()) ctx.subjectStack.pop(); + return; + } + if (RDFXMLUtils.isDescription(localName, uri)) { + if (!ctx.subjectStack.isEmpty()) ctx.subjectStack.pop(); + } + if (!ctx.subjectStack.isEmpty() && !ctx.predicateStack.isEmpty()) { + ctx.subjectStack.pop(); + ctx.predicateStack.pop(); + } + } +} \ No newline at end of file diff --git a/src/main/java/fr/inria/corese/core/next/impl/io/parser/rdfxml/RDFXMLStatementEmitter.java b/src/main/java/fr/inria/corese/core/next/impl/io/parser/rdfxml/RDFXMLStatementEmitter.java new file mode 100644 index 000000000..b8e502446 --- /dev/null +++ b/src/main/java/fr/inria/corese/core/next/impl/io/parser/rdfxml/RDFXMLStatementEmitter.java @@ -0,0 +1,130 @@ +package fr.inria.corese.core.next.impl.io.parser.rdfxml; + +import fr.inria.corese.core.next.api.*; +import fr.inria.corese.core.next.impl.common.literal.XSD; +import fr.inria.corese.core.next.impl.common.vocabulary.RDF; +import org.xml.sax.Attributes; + +import java.util.Optional; + +import static fr.inria.corese.core.next.impl.io.parser.rdfxml.RDFXMLUtils.*; + +/** + * Emits RDF statements from parsed RDF/XML constructs using a given RDF Model + * and ValueFactory. + */ +public class RDFXMLStatementEmitter { + + private final Model model; + private final ValueFactory factory; + + /** + * Constructs a new emitter for the given RDF model and value factory. + * + * @param model the RDF model where statements will be added + * @param factory the RDF value factory used to create RDF terms + */ + public RDFXMLStatementEmitter(Model model, ValueFactory factory) { + this.model = model; + this.factory = factory; + } + + /** + * Emits a literal statement with optional datatype or language. + * + * @param subject the subject of the statement + * @param predicate the predicate of the statement + * @param text the literal value + * @param datatypeUri the datatype URI (optional, may be null) + * @param lang the language tag (optional, may be null) + */ + public void emitLiteral(Resource subject, IRI predicate, String text, String datatypeUri, String lang) { + Value literal; + if (datatypeUri != null && !datatypeUri.isEmpty()) { + Optional known = RDFXMLUtils.resolveDatatype(datatypeUri); + IRI dtype = known.map(XSD::getIRI).orElseGet(() -> { + System.err.printf("[Warning] Unknown datatype: %s%n", datatypeUri); + return factory.createIRI(datatypeUri); + }); + literal = factory.createLiteral(text, dtype); + } else if (lang != null && !lang.equals("__NO_LANG__")) { + literal = factory.createLiteral(text, lang); + } else { + literal = factory.createLiteral(text); + } + model.add(factory.createStatement(subject, predicate, literal)); + } + + + /** + * Emits a rdf:type statement for the given subject and type URI. + * + * @param subject the subject resource + * @param expandedQName the fully expanded IRI for the type + */ + public void emitType(Resource subject, String expandedQName) { + model.add(factory.createStatement(subject, RDF.type.getIRI(), factory.createIRI(expandedQName))); + } + + /** + * Emits RDF statements for non-syntax XML attributes as predicate-object pairs. + * + * @param subject the subject resource + * @param attrs the XML attributes associated with the element + */ + public void emitPropertyAttributes(Resource subject, Attributes attrs) { + for (int i = 0; i < attrs.getLength(); i++) { + String attrURI = attrs.getURI(i); + String attrLocal = attrs.getLocalName(i); + String attrQName = attrs.getQName(i); + String value = attrs.getValue(i); + + if (isSyntaxAttribute(attrURI, attrLocal, attrQName)) continue; + + IRI pred = factory.createIRI(expandQName(attrURI, attrLocal, attrQName)); + model.add(factory.createStatement(subject, pred, factory.createLiteral(value))); + } + } + + /** + * Emits a triple where the object is an IRI resolved against the base URI. + * + * @param subject the subject of the triple + * @param predicate the predicate of the triple + * @param resource the relative or absolute IRI string + * @param baseURI the base URI used to resolve the resource + */ + public void emitResourceTriple(Resource subject, IRI predicate, String resource, String baseURI) { + model.add(factory.createStatement( + subject, + predicate, + factory.createIRI(resolveAgainstBase(resource, baseURI)) + )); + } + + /** + * Emits a triple where the object is a blank node identified by node ID. + * + * @param subject the subject of the triple + * @param predicate the predicate of the triple + * @param nodeID the blank node identifier + */ + public void emitBNodeTriple(Resource subject, IRI predicate, String nodeID) { + model.add(factory.createStatement( + subject, + predicate, + factory.createBNode("_:" + nodeID) + )); + } + + /** + * Emits a triple with a resource as object. + * + * @param subject the subject of the triple + * @param predicate the predicate of the triple + * @param object the object resource of the triple + */ + public void emitTriple(Resource subject, IRI predicate, Resource object) { + model.add(factory.createStatement(subject, predicate, object)); + } +} \ No newline at end of file diff --git a/src/main/java/fr/inria/corese/core/next/impl/io/parser/rdfxml/RDFXMLUtils.java b/src/main/java/fr/inria/corese/core/next/impl/io/parser/rdfxml/RDFXMLUtils.java new file mode 100644 index 000000000..e31e09eff --- /dev/null +++ b/src/main/java/fr/inria/corese/core/next/impl/io/parser/rdfxml/RDFXMLUtils.java @@ -0,0 +1,216 @@ +package fr.inria.corese.core.next.impl.io.parser.rdfxml; + +import fr.inria.corese.core.next.api.*; +import fr.inria.corese.core.next.impl.common.literal.XSD; +import fr.inria.corese.core.next.impl.common.vocabulary.RDF; +import fr.inria.corese.core.next.impl.exception.IncorrectFormatException; +import org.xml.sax.*; + +import java.util.List; +import java.util.Optional; + +/** + * Utility methods for processing RDF/XML constructs. + *

+ * This class provides helpers for handling RDF/XML syntax attributes, + * QName expansion, datatype resolution, subject extraction, and RDF collections. + *

+ */ +public class RDFXMLUtils { + private RDFXMLUtils() { + // Utility class; no instantiation. + } + + + /** + * Expands a QName using the given namespace URI and local name. + * + * @param uri the namespace URI + * @param localName the local name + * @param qName the qualified name (used as fallback) + * @return the expanded IRI, or the qName if the URI is null or empty + */ + public static String expandQName(String uri, String localName, String qName) { + return (uri != null && !uri.isEmpty()) ? uri + localName : qName; + } + + + /** + * Resolves a datatype URI to a known XSD enum constant. + * + * @param datatypeUri the datatype URI + * @return an Optional containing the matching XSD type if found + */ + public static Optional resolveDatatype(String datatypeUri) { + for (XSD xsd : XSD.values()) { + if (xsd.getIRI().stringValue().equals(datatypeUri)) return Optional.of(xsd); + } + return Optional.empty(); + } + + /** + * Extracts a subject resource from RDF/XML attributes. + * Supports rdf:about, rdf:nodeID, rdf:ID. + * + * @param attrs the XML attributes + * @param factory the value factory + * @param baseURI the base URI for resolving relative IRIs + * @return a Resource representing the subject + */ + public static Resource extractSubject(Attributes attrs, ValueFactory factory, String baseURI) { + String about = attrs.getValue(RDF.type.getNamespace(), "about"); + if (about != null) return factory.createIRI(resolveAgainstBase(about, baseURI)); + + String nodeID = attrs.getValue(RDF.type.getNamespace(), "nodeID"); + if (nodeID != null) return factory.createBNode("_:" + nodeID); + + String id = attrs.getValue(RDF.type.getNamespace(), "ID"); + if (id != null) return factory.createIRI(resolveAgainstBase("#" + id, baseURI)); + + // Default to blank node + return factory.createBNode(); + } + + /** + * Resolves a relative IRI against a base URI. + * + * @param iri the relative or absolute IRI + * @param baseURI the base URI + * @return the resolved IRI + * @throws IncorrectFormatException if URI resolution fails + */ + public static String resolveAgainstBase(String iri, String baseURI) { + if (iri == null) return null; + if (iri.isEmpty()) return baseURI; + if (baseURI == null || iri.matches("^[a-zA-Z][a-zA-Z0-9+.-]*:.*")) { + return iri; + } + try { + return new java.net.URI(baseURI).resolve(iri).toString(); + } catch (Exception e) { + throw new IncorrectFormatException("Failed to resolve IRI: " + iri + " against base: " + baseURI, e); + } + } + + /** + * Determines whether the element is a rdf:Description. + * + * @param localName the local name of the element + * @param uri the namespace URI + * @return {@code true} if it's an RDF description element + */ + public static boolean isDescription(String localName, String uri) { + return RDF.type.getNamespace().equals(uri) && "Description".equals(localName); + } + + + /** + * Checks if the attributes define a subject node (via about, nodeID, or ID). + * + * @param attrs the attributes to check + * @return true if any node-identifying attribute is present + */ + public static boolean isNodeElement(Attributes attrs) { + return attrs.getValue(RDF.type.getNamespace(), "about") != null || + attrs.getValue(RDF.type.getNamespace(), "nodeID") != null || + attrs.getValue(RDF.type.getNamespace(), "ID") != null; + } + + + /** + * Retrieves the value of rdf:parseType from attributes. + * + * @param attrs the attributes + * @return the parseType value, or null if not present + */ + public static String getParseType(Attributes attrs) { + return attrs.getValue(RDF.type.getNamespace(), "parseType"); + } + + + /** + * Determines whether a given attribute is an RDF/XML syntax attribute. + * + * @param uri the namespace URI + * @param localName the local name + * @param qName the qualified name + * @return true if the attribute is considered syntax-related + */ + public static boolean isSyntaxAttribute(String uri, String localName, String qName) { + if (uri != null && RDF.type.getNamespace().equals(uri)) { + return switch (localName) { + case "about", "ID", "nodeID", "resource", "parseType", "datatype" -> true; + default -> false; + }; + } + return qName.startsWith("xml:"); + } + + /** + * Resolves an XSD datatype from a URI. + * + * @param uri the datatype URI + * @return an Optional containing the XSD constant if matched + */ + public static Optional fromURI(String uri) { + for (XSD xsd : XSD.values()) { + if (xsd.getIRI().stringValue().equals(uri)) { + return Optional.of(xsd); + } + } + return Optional.empty(); + } + + /** + * Checks if an element is the top-level rdf:RDF wrapper. + * + * @param uri the namespace URI + * @param localName the local name + * @return true if the element is rdf:RDF + */ + public static boolean isRdfRDF(String uri, String localName) { + return RDF.type.equals(uri) && "RDF".equals(localName); + } + + /** + * Determines if an element is a recognized RDF container: Bag, Seq, or Alt. + * + * @param localName the local name + * @param uri the namespace URI + * @return true if the element is a container type + */ + public static boolean isContainer(String localName, String uri) { + return RDF.type.getNamespace().equals(uri) && + ("Seq".equals(localName) || "Bag".equals(localName) || "Alt".equals(localName)); + } + + /** + * Creates a linked RDF collection using rdf:first and rdf:rest. + * + * @param items the list of resource items + * @param model the RDF model to populate + * @param factory the RDF value factory + * @return the head resource of the RDF collection + */ + public static Resource createRdfCollection(List items, Model model, ValueFactory factory) { + Resource head = factory.createBNode(); + Resource current = head; + + for (int i = 0; i < items.size(); i++) { + Resource next = (i < items.size() - 1) + ? factory.createBNode() + : RDF.nil.getIRI(); // rdf:nil + + model.add(factory.createStatement(current, + RDF.first.getIRI(), + items.get(i))); + + model.add(factory.createStatement(current, + RDF.rest.getIRI(), + next)); + + current = next; + } + return head; + } +} \ No newline at end of file diff --git a/src/main/java/fr/inria/corese/core/next/impl/io/parser/rdfxml/context/RDFXMLContext.java b/src/main/java/fr/inria/corese/core/next/impl/io/parser/rdfxml/context/RDFXMLContext.java new file mode 100644 index 000000000..4807977d3 --- /dev/null +++ b/src/main/java/fr/inria/corese/core/next/impl/io/parser/rdfxml/context/RDFXMLContext.java @@ -0,0 +1,80 @@ +package fr.inria.corese.core.next.impl.io.parser.rdfxml.context; + +import fr.inria.corese.core.next.api.*; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Deque; +import java.util.List; + +/** + * Holds shared parsing state during RDF/XML parsing. + * + *

This class acts as a context holder for the SAX-based RDF/XML parser, + * allowing multiple elements and handlers to share and manipulate parsing state. + * It stores stacks for subjects, predicates, datatypes, and languages, + * as well as temporary collections used during the construction of RDF lists and containers.

+ * + *

This context is typically instantiated once per parsing session and passed + * throughout the parsing logic.

+ */ +public class RDFXMLContext { + + /** The RDF model to which parsed triples will be added. */ + public Model model; + + /** The factory used to create IRIs, literals, blank nodes, and statements. */ + public ValueFactory factory; + + /** The base URI against which relative IRIs are resolved. */ + public String baseURI; + + /** A single statement buffer (optional use). */ + public Statement statement; + + /** Builder list for rdf:parseType="Collection" elements. */ + public List collectionBuilder = new ArrayList<>(); + + /** The subject associated with the current RDF collection. */ + public Resource collectionSubject; + + /** The predicate that connects the collection subject to the list head. */ + public IRI collectionPredicate; + + /** Stack of subject resources to manage nesting of elements. */ + public final Deque subjectStack = new ArrayDeque<>(); + + /** Stack of predicates for tracking current RDF properties. */ + public final Deque predicateStack = new ArrayDeque<>(); + + /** Stack for xml:lang values scoped by element depth. */ + public final Deque langStack = new ArrayDeque<>(); + + /** Stack for rdf:datatype URIs associated with literals. */ + public final Deque datatypeStack = new ArrayDeque<>(); + + /** Temporary holder for RDF collection items (unused or optional). */ + public final Deque collectionItems = new ArrayDeque<>(); + + /** Whether the parser is currently inside an RDF container (rdf:Seq, rdf:Bag, rdf:Alt). */ + public boolean inContainer = false; + + /** Whether the parser is currently inside an RDF collection (rdf:parseType="Collection"). */ + public boolean inCollection = false; + + /** If true, skips pushing a subject onto the stack (used for collection items). */ + public boolean suppressSubject = false; + + /** Counter for rdf:li to rdf:_n expansion. */ + public int liIndex = 1; + + /** + * Constructs a new context for RDF/XML parsing. + * + * @param model the RDF model to populate with triples + * @param factory the value factory used to create RDF terms + */ + public RDFXMLContext(Model model, ValueFactory factory) { + this.model = model; + this.factory = factory; + } +} diff --git a/src/main/java/fr/inria/corese/core/next/impl/io/parser/trig/ANTLRTrigParser.java b/src/main/java/fr/inria/corese/core/next/impl/io/parser/trig/ANTLRTrigParser.java new file mode 100644 index 000000000..cddf9168c --- /dev/null +++ b/src/main/java/fr/inria/corese/core/next/impl/io/parser/trig/ANTLRTrigParser.java @@ -0,0 +1,96 @@ +package fr.inria.corese.core.next.impl.io.parser.trig; + +import fr.inria.corese.core.next.api.Model; +import fr.inria.corese.core.next.api.ValueFactory; +import fr.inria.corese.core.next.api.base.io.RDFFormat; +import fr.inria.corese.core.next.api.base.io.parser.AbstractRDFParser; +import fr.inria.corese.core.next.api.io.IOOptions; + +import fr.inria.corese.core.next.impl.exception.ParsingErrorException; +import fr.inria.corese.core.next.impl.parser.antlr.TriGLexer; +import fr.inria.corese.core.next.impl.parser.antlr.TriGParser; +import org.antlr.v4.runtime.CharStream; +import org.antlr.v4.runtime.CharStreams; +import org.antlr.v4.runtime.CommonTokenStream; +import org.antlr.v4.runtime.tree.ParseTree; +import org.antlr.v4.runtime.tree.ParseTreeListener; +import org.antlr.v4.runtime.tree.ParseTreeWalker; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.nio.charset.StandardCharsets; + +/** + * An ANTLR4-based parser for Trig format. + * This parser uses an ANTLR grammar to tokenize and parse Trig documents, + * then a listener to build the RDF model. + */ +public class ANTLRTrigParser extends AbstractRDFParser { + + /** + * Constructor for the ANTLRTrigParser. + * + * @param model The RDF model to populate. + * @param factory The ValueFactory for creating RDF resources. + */ + public ANTLRTrigParser(Model model, ValueFactory factory) { super(model, factory); } + + /** + * Constructor for the ANTLRTrigParser with configuration options. + * + * @param model The RDF model to populate. + * @param factory The ValueFactory for creating RDF resources. + * @param config The configuration options for parsing. + */ + public ANTLRTrigParser(Model model, ValueFactory factory, IOOptions config) {super(model, factory, config);} + + @Override + public RDFFormat getRDFFormat() { + return RDFFormat.TRIG; + } + + @Override + public void setConfig(IOOptions config) {} + + @Override + public void parse(InputStream in) throws ParsingErrorException { + parse(new InputStreamReader(in, StandardCharsets.UTF_8), null); + } + + @Override + public void parse(InputStream in, String baseURI) throws ParsingErrorException { + parse(new InputStreamReader(in, StandardCharsets.UTF_8), baseURI); + } + + @Override + public void parse(Reader reader) throws ParsingErrorException { + parse(reader, null); + } + + /** + * Parses Trig data from a Reader using ANTLR4. + * + * @param reader The Reader to read RDF data from. + * @param baseURI The base URI. + * @throws ParsingErrorException if a parsing or I/O error occurs. + */ + @Override + public void parse(Reader reader, String baseURI) throws ParsingErrorException { + try { + CharStream charStream = CharStreams.fromReader(reader); + TriGLexer triGLexer = new TriGLexer(charStream); + CommonTokenStream tokens = new CommonTokenStream(triGLexer); + TriGParser triGParser = new TriGParser(tokens); + ParseTreeWalker walker = new ParseTreeWalker(); + ParseTree tree = triGParser.trigDoc(); + TriGListerner listerner = new TriGListerner(getModel(), getValueFactory(), this.getConfig()); + walker.walk((ParseTreeListener) listerner, tree); + } catch (IOException e) { + throw new ParsingErrorException("Failed to parse TriG RDF: " + e.getMessage(), e); + } catch (Exception e) { + throw new ParsingErrorException("Unexpected error during TriG parsing: " + e.getMessage(), e); + } + } +} \ No newline at end of file diff --git a/src/main/java/fr/inria/corese/core/next/impl/io/parser/trig/TriGListerner.java b/src/main/java/fr/inria/corese/core/next/impl/io/parser/trig/TriGListerner.java new file mode 100644 index 000000000..a7f199392 --- /dev/null +++ b/src/main/java/fr/inria/corese/core/next/impl/io/parser/trig/TriGListerner.java @@ -0,0 +1,253 @@ +package fr.inria.corese.core.next.impl.io.parser.trig; + +import fr.inria.corese.core.next.api.*; +import fr.inria.corese.core.next.api.io.IOOptions; +import fr.inria.corese.core.next.api.io.parser.RDFParserBaseIRIOptions; +import fr.inria.corese.core.next.impl.common.literal.XSD; +import fr.inria.corese.core.next.impl.common.vocabulary.RDF; +import fr.inria.corese.core.next.impl.parser.antlr.TriGBaseListener; +import fr.inria.corese.core.next.impl.parser.antlr.TriGParser; +import fr.inria.corese.core.next.api.ValueFactory; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Listener for the ANTLR4 generated parser for TriG. + * This listener traverses the parse tree and builds the RDF model, + * supporting named graphs. It includes unescaping logic for URIs and literals. + */ +public class TriGListerner extends TriGBaseListener { + private final Model model; + private String baseURI; + private final Map prefixMap = new HashMap<>(); + private final ValueFactory factory; + + private Resource currentSubject; + private IRI currentPredicate; + private Resource currentGraph; + + + /** + * Constructor for the TriGListerner. + * + * @param model The RDF model to populate. + * @param factory The ValueFactory for creating RDF resources. + * @param options IOOptions for configuration (if any). + */ + public TriGListerner(Model model, ValueFactory factory, IOOptions options) { + this.model = model; + this.baseURI = baseURI != null ? baseURI : ""; + if (options != null && options instanceof RDFParserBaseIRIOptions); + this.factory = factory; + } + + @Override + public void exitPrefixID(TriGParser.PrefixIDContext ctx) { + String prefix = ctx.PNAME_NS().getText(); + String iri = ctx.IRIREF().getText(); + prefix = prefix.substring(0, prefix.length() - 1); + iri = iri.substring(1, iri.length() - 1); + prefixMap.put(prefix, iri); + model.setNamespace(prefix, iri); + } + + @Override + public void exitSparqlBase(TriGParser.SparqlBaseContext ctx) { + baseURI = ctx.IRIREF().getText().replaceAll("^<|>$", ""); + } + + @Override + public void enterBlock(TriGParser.BlockContext ctx) { + currentGraph = ctx.Graph_w() != null && ctx.labelOrSubject() != null + ? extractLabelOrSubject(ctx.labelOrSubject()) + : null; + } + + @Override + public void exitBlock(TriGParser.BlockContext ctx) { + currentGraph = null; + } + + @Override + public void enterTriplesOrGraph(TriGParser.TriplesOrGraphContext ctx) { + if (ctx.labelOrSubject() != null && ctx.predicateObjectList() != null) { + currentSubject = extractLabelOrSubject(ctx.labelOrSubject()); + processPredicateObjectList(ctx.predicateObjectList()); + } + } + + @Override + public void enterTriples(TriGParser.TriplesContext ctx) { + currentSubject = extractSubject(ctx.subject()); + processPredicateObjectList(ctx.predicateObjectList()); + } + + /** + * Processes a PredicateObjectList context, extracting verbs and corresponding object lists, + * and adding triples to the model for the current subject and graph. + * + * @param ctx the predicate-object list context from the parser + */ + private void processPredicateObjectList(TriGParser.PredicateObjectListContext ctx) { + List verbs = ctx.verb(); + List objLists = ctx.objectList(); + + for (int i = 0; i < verbs.size(); i++) { + currentPredicate = extractVerb(verbs.get(i)); + List objects = objLists.get(i).object(); + for (TriGParser.ObjectContext objCtx : objects) { + Value object = extractObject(objCtx); + model.add(currentSubject, currentPredicate, object, currentGraph); + } + } + } + + /** + * Extracts an RDF object from the ObjectContext. + * Supports IRIs, blank nodes, literals, and inline blank node property lists. + * + * @param ctx the object context + * @return the extracted RDF Value + */ + private Value extractObject(TriGParser.ObjectContext ctx) { + if (ctx.iri() != null) return factory.createIRI(resolveIRI(ctx.iri().getText())); + if (ctx.blank() != null) return extractBlank(ctx.blank()); + if (ctx.literal() != null) return extractLiteral(ctx.literal()); + if (ctx.blankNodePropertyList() != null) return processBlankNodePropertyList(ctx.blankNodePropertyList()); + throw new RuntimeException("Unsupported object: " + ctx.getText()); + } + + /** + * Processes an inline blank node with its property list, returning the blank node as a Resource. + * Temporarily updates the current subject to the new blank node during processing. + * + * @param ctx the blank node property list context + * @return the new blank node resource + */ + private Resource processBlankNodePropertyList(TriGParser.BlankNodePropertyListContext ctx) { + Resource bnode = factory.createBNode(); + Resource savedSubject = currentSubject; + currentSubject = bnode; + processPredicateObjectList(ctx.predicateObjectList()); + currentSubject = savedSubject; + return bnode; + } + + /** + * Extracts a subject from a SubjectContext, which can be an IRI or a blank node. + * + * @param ctx the subject context + * @return the extracted subject as a Resource + */ + private Resource extractSubject(TriGParser.SubjectContext ctx) { + if (ctx.iri() != null) return factory.createIRI(resolveIRI(ctx.iri().getText())); + if (ctx.blank() != null) return extractBlank(ctx.blank()); + throw new RuntimeException("Unsupported subject: " + ctx.getText()); + } + + /** + * Extracts a blank node from a BlankContext, supporting labeled (_:b) and anonymous ([]) forms. + * + * @param ctx the blank context + * @return the blank node as a Resource + */ + private Resource extractBlank(TriGParser.BlankContext ctx) { + TriGParser.BlankNodeContext node = ctx.blankNode(); + if (node != null) { + if (node.BLANK_NODE_LABEL() != null) + return factory.createBNode(node.BLANK_NODE_LABEL().getText()); + if (node.ANON() != null) + return factory.createBNode(); + } + throw new RuntimeException("Unsupported blank node structure: " + ctx.getText()); + } + + /** + * Extracts a graph label or subject from a LabelOrSubjectContext. + * Supports IRI and blank node. + * + * @param ctx the label or subject context + * @return the extracted resource + */ + private Resource extractLabelOrSubject(TriGParser.LabelOrSubjectContext ctx) { + if (ctx.iri() != null) return factory.createIRI(resolveIRI(ctx.iri().getText())); + if (ctx.blankNode() != null) return factory.createBNode(ctx.blankNode().getText()); + throw new RuntimeException("Unsupported labelOrSubject: " + ctx.getText()); + } + + /** + * Extracts a predicate IRI from a VerbContext. + * Handles the special keyword 'a' as rdf:type. + * + * @param ctx the verb context + * @return the extracted IRI + */ + private IRI extractVerb(TriGParser.VerbContext ctx) { + return factory.createIRI(resolveIRI(ctx.getText())); + } + + /** + * Extracts a Literal from a LiteralContext, handling typed, language-tagged, boolean, and numeric literals. + * + * @param ctx the literal context + * @return the extracted Literal + */ + private Literal extractLiteral(TriGParser.LiteralContext ctx) { + if (ctx.rDFLiteral() != null) { + String label = stripQuotes(ctx.rDFLiteral().string().getText()); + if (ctx.rDFLiteral().LANGTAG() != null) + return factory.createLiteral(label, ctx.rDFLiteral().LANGTAG().getText().substring(1)); + if (ctx.rDFLiteral().iri() != null) + return factory.createLiteral(label, factory.createIRI(resolveIRI(ctx.rDFLiteral().iri().getText()))); + return factory.createLiteral(label); + } + if (ctx.BooleanLiteral() != null) + return factory.createLiteral(ctx.BooleanLiteral().getText(), XSD.BOOLEAN.getIRI()); + if (ctx.numericLiteral() != null) { + if (ctx.numericLiteral().INTEGER() != null) + return factory.createLiteral(ctx.numericLiteral().INTEGER().getText(), XSD.INTEGER.getIRI()); + if (ctx.numericLiteral().DECIMAL() != null) + return factory.createLiteral(ctx.numericLiteral().DECIMAL().getText(), XSD.DECIMAL.getIRI()); + if (ctx.numericLiteral().DOUBLE() != null) + return factory.createLiteral(ctx.numericLiteral().DOUBLE().getText(), XSD.DOUBLE.getIRI()); + } + throw new RuntimeException("Unsupported literal: " + ctx.getText()); + } + + /** + * Resolves an IRI or QName into a full URI string. + * Handles full IRIs in angle brackets, QNames using prefixes, and special case "a". + * + * @param raw the raw string + * @return the resolved URI string + */ + private String resolveIRI(String raw) { + raw = raw.trim(); + if (raw.startsWith("<") && raw.endsWith(">")) return raw.substring(1, raw.length() - 1); + if (raw.equals("a")) return RDF.type.getIRI().stringValue(); + if (raw.contains(":")) { + String[] parts = raw.split(":", 2); + String ns = prefixMap.get(parts[0]); + if (ns != null) return ns + parts[1]; + throw new IllegalArgumentException("Undeclared prefix: " + parts[0]); + } + return baseURI + raw; + } + + /** + * Strips surrounding quotes from a string literal, including single, double, and multi-line forms. + * + * @param text the quoted string + * @return the unquoted string + */ + private String stripQuotes(String text) { + if (text == null || text.length() < 2) return text; + if ((text.startsWith("\"") && text.endsWith("\"")) || + (text.startsWith("\"\"\"") && text.endsWith("\"\"\"")) || + (text.startsWith("'''") && text.endsWith("'''"))) { + return text.substring(1, text.length() - 1); + } + return text; + } +} \ No newline at end of file diff --git a/src/main/java/fr/inria/corese/core/util/CoreseInfo.java b/src/main/java/fr/inria/corese/core/util/CoreseInfo.java index 16fb462b7..c202ac361 100644 --- a/src/main/java/fr/inria/corese/core/util/CoreseInfo.java +++ b/src/main/java/fr/inria/corese/core/util/CoreseInfo.java @@ -6,7 +6,7 @@ */ public class CoreseInfo { - private static final String VERSION = "4.6.4-SNAPSHOT"; + private static final String VERSION = "4.6.4"; /** * Retrieves the current version of the Corese application. diff --git a/src/test/java/fr/inria/corese/core/next/impl/io/parser/rdfxml/RDFXMLParserTest.java b/src/test/java/fr/inria/corese/core/next/impl/io/parser/rdfxml/RDFXMLParserTest.java new file mode 100644 index 000000000..f4677cdab --- /dev/null +++ b/src/test/java/fr/inria/corese/core/next/impl/io/parser/rdfxml/RDFXMLParserTest.java @@ -0,0 +1,582 @@ +package fr.inria.corese.core.next.impl.io.parser.rdfxml; + +import fr.inria.corese.core.next.api.Literal; +import fr.inria.corese.core.next.api.Model; +import fr.inria.corese.core.next.api.Value; +import fr.inria.corese.core.next.api.ValueFactory; +import fr.inria.corese.core.next.impl.temp.CoreseAdaptedValueFactory; +import fr.inria.corese.core.next.impl.temp.CoreseModel; +import org.junit.jupiter.api.Test; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * Unit tests for the RDFXMLParser class. + * These tests verify the parser's ability to correctly parse RDF/XML + * and interact with the Model and ValueFactory, including error handling + * and unescaping of IRIs and literals, and named graphs. + */ +public class RDFXMLParserTest { + /** + * Helper method to parse the RDF/XML String + * @param rdfXml + * @return model + * @throws Exception + */ + private Model parseRdfXml(String rdfXml) throws Exception { + Model model = new CoreseModel(); + ValueFactory valueFactory = new CoreseAdaptedValueFactory(); + try (InputStream inputStream = new ByteArrayInputStream(rdfXml.getBytes(StandardCharsets.UTF_8))) { + RDFXMLParser parser = new RDFXMLParser(model, valueFactory); + parser.parse(inputStream); + } + return model; + } + + /** + * Helper method to print the model. + * @param model + */ + private void printModel(Model model) { + model.stream().forEach(stmt -> { + Value obj = stmt.getObject(); + if (obj instanceof Literal literal) { + if (literal.getLanguage().isPresent()) { + System.out.printf("(%s, %s, \"%s\"@%s)%n", + stmt.getSubject().stringValue(), + stmt.getPredicate().stringValue(), + literal.getLabel(), + literal.getLanguage().get()); + } else { + System.out.printf("(%s, %s, \"%s\")%n", + stmt.getSubject().stringValue(), + stmt.getPredicate().stringValue(), + literal.getLabel()); + } + } else { + System.out.printf("(%s, %s, %s)%n", + stmt.getSubject().stringValue(), + stmt.getPredicate().stringValue(), + obj.stringValue()); + } + }); + } + + + /** + * Test node elements with IRIs + * @throws Exception + */ + @Test + public void testNodeElementsWithIRIs() throws Exception { + String rdfXml = """ + + + + + + + + + + + + + + """; + + Model model = parseRdfXml(rdfXml); + printModel(model); + assertEquals(2, model.size(), "Expected two RDF statements"); + + } + + /** + * Test a basic RDF/XML file + * @throws Exception + */ + @Test + public void testBasicRdfParsing() throws Exception { + String rdfXml = """ + + + + John Smith + 2025-07-07 + + + """; + Model model = parseRdfXml(rdfXml); + printModel(model); + assertEquals(2, model.size(), "Expected two RDF statements"); + } + + /** + * Test a RDF/XML file with Complete description of all graph paths + * @throws Exception + */ + @Test + public void testExample3CompleteDescriptionOfAllGraphPaths() throws Exception { + String rdfXml = """ + + + + + + + + + + + + + + + + Dave Beckett + + + + + + RDF 1.2 XML Syntax + + + """.trim(); + Model model = parseRdfXml(rdfXml); + printModel(model); + assertEquals(5, model.size(), "Expected five RDF statements"); + } + + /** + * Test RDF/XML File Using multiple property elements on a node element + * @throws Exception + */ + @Test + public void testExample4UsingMultiplePropertyElements() throws Exception { + String rdfXml = """ + + + + + + + + + + Dave Beckett + + + RDF 1.2 XML Syntax + + + """.trim(); + + Model model = parseRdfXml(rdfXml); + printModel(model); + assertEquals(4, model.size(), "Expected four RDF statements"); + } + + /** + * Test RDF/XML with Empty property elements + * @throws Exception + */ + @Test + public void testExample5EmptyPropertyElements() throws Exception { + String rdfXml = """ + + + + + + + Dave Beckett + + + RDF 1.2 XML Syntax + + + """.trim(); + + + Model model = parseRdfXml(rdfXml); + printModel(model); + assertEquals(4, model.size(), "Expected four RDF statements"); + + } + + /** + * Test a RDF/XML file with Replacing property elements with string literal content into property attributes + * @throws Exception + */ + @Test + public void testExample6ReplacingPropertyElementsWithStringLiteral() throws Exception { + String rdfXml = """ + + + + + + + + + + """.trim(); + + Model model = parseRdfXml(rdfXml); + printModel(model); + assertEquals(4, model.size(), "Expected four RDF statements"); + + } + + /** + * Test a Complete RDF/XML + * @throws Exception + */ + @Test + public void testExample7CompleteRDFXML() throws Exception { + String rdfXml = """ + + + + + + + + + + + + """.trim(); + Model model = parseRdfXml(rdfXml); + printModel(model); + assertEquals(4, model.size(), "Expected four RDF statements"); + } + + /** + * Test a Complete example of xml:lang + * @throws Exception + */ + @Test + public void testExample8CompleteExampleXmlLang() throws Exception { + String rdfXml = """ + + + + + RDF 1.2 XML Syntax + RDF 1.2 XML Syntax + RDF 1.2 XML Syntax + + + + Der Baum + Das Buch ist außergewöhnlich + The Tree + + + + """.trim(); + + Model model = parseRdfXml(rdfXml); + printModel(model); + assertEquals(6, model.size(), "Expected six RDF statements"); + } + + @Test + public void testExample11CompleteExamplerdfDatatype() throws Exception { + String rdfXml = """ + + + + + 123 + + + + """.trim(); + Model model = parseRdfXml(rdfXml); + printModel(model); + assertEquals(1, model.size(), "Expected four RDF statements"); + } + + /** + * Test a Complete RDF/XML file with a description of graph using rdf:nodeID + * @throws Exception + */ + @Test + public void testExample12CompleteRDFXMLUsingRdfNodeID() throws Exception { + + String rdfXml = """ + + + + + + + + + + + + + """.trim(); + + Model model = parseRdfXml(rdfXml); + printModel(model); + + // Assert or inspect the result + assertEquals(4, model.size(), "Expected five RDF statements"); + } + + /** + * Test a RDF/XML file with a Complete example using rdf:parseType=Resource + * @throws Exception + */ + @Test + public void testExample13CompleteExampleUsingRdfparseTypeResource() throws Exception { + String rdfXml = """ + + + + + Dave Beckett + + + + + """.trim(); + + Model model = parseRdfXml(rdfXml); + printModel(model); + assertEquals(4, model.size(), "Expected four RDF statements"); + + } + + /** + * Test a RDF/XML file with a Complete example of property attributes on an empty property element + * @throws Exception + */ + @Test + public void testExample14CompleteExampleOfPorpertyAttributesOnAnEmptyPropertyElement() throws Exception { + + String rdfXml = """ + + + + + + + + + + """.trim(); + + Model model = parseRdfXml(rdfXml); + printModel(model); + assertEquals(3, model.size(), "Expected three RDF statements"); + } + + /** + * Test a RDF/XML file with a Complete example with rdf:type + * @throws Exception + */ + @Test + public void testExample15CompleteExampleWithRdfType() throws Exception { + String rdfXml = """ + + + + + + A marvelous thing + + + """.trim(); + + Model model = parseRdfXml(rdfXml); + printModel(model); + assertEquals(2, model.size(), "Expected four RDF statements"); + } + + /** + * Test a RDF/XML file with a Complete example using a typed node element to replace an rdf:type + * @throws Exception + */ + @Test + public void testExample16CompleteExampleUsingATypedNodeElementToReplaceAnRdfType() throws Exception { + String rdfXml = """ + + + + + A marvelous thing + + + + """.trim(); + + Model model = parseRdfXml(rdfXml); + printModel(model); + assertEquals(2, model.size(), "Expected two RDF statements"); + + } + + @Test + /** + * Test a XML/RDF File using rdf:ID and xml:base + */ + public void testExample17CompleteExampleUsingRdfIDAndXmlbase() throws Exception { + String rdfXml = """ + + + + + + + + + """.trim(); + + Model model = parseRdfXml(rdfXml); + printModel(model); + assertEquals(1, model.size(), "Expected one RDF statement"); + + } + + /** + * Test a Complex example using RDF list properties + * @throws Exception + */ + @Test + public void testExample18ComplexExampleUsingRdfListProperties() throws Exception { + + String rdfXml = """ + + + + + + + + + + + """.trim(); + + Model model = parseRdfXml(rdfXml); + printModel(model); + assertEquals(4, model.size(), "Expected three RDF statements"); + } + + /** + * Test a Complete example using rdf:li + * @throws Exception + */ + @Test + public void testExample19CompleteExampleUsingRdfliProperties() throws Exception { + + String rdfXml = """ + + + + + + + + + + + """.trim(); + + Model model = parseRdfXml(rdfXml); + printModel(model); + assertEquals(4, model.size(), "Expected three RDF statements"); + + } + + /** + * Test a Complete example of a RDF collection + * @throws Exception + */ + @Test + public void testExample20CompleteExampleOfARdfCollectionOfNodes() throws Exception { + String rdfXml = """ + + + + + + + + + + + + + """.trim(); + + Model model = parseRdfXml(rdfXml); + printModel(model); + assertEquals(7, model.size(), "Expected three RDF statements"); + } + + /** + * Test a Complete example of rdf:ID reifying a property element + * @throws Exception + */ + @Test + public void testExample21CompleteExampleOfRdfID() throws Exception { + String rdfXml = """ + + + + blah + + + + """.trim(); + Model model = parseRdfXml(rdfXml); + printModel(model); + assertEquals(1, model.size(), "Expected one RDF statement"); + + } +} \ No newline at end of file diff --git a/src/test/java/fr/inria/corese/core/next/impl/io/parser/rdfxml/RDFXMLStatementEmitterTest.java b/src/test/java/fr/inria/corese/core/next/impl/io/parser/rdfxml/RDFXMLStatementEmitterTest.java new file mode 100644 index 000000000..62a5d9671 --- /dev/null +++ b/src/test/java/fr/inria/corese/core/next/impl/io/parser/rdfxml/RDFXMLStatementEmitterTest.java @@ -0,0 +1,164 @@ +package fr.inria.corese.core.next.impl.io.parser.rdfxml; + +import fr.inria.corese.core.next.api.*; +import fr.inria.corese.core.next.impl.common.literal.XSD; +import fr.inria.corese.core.next.impl.common.vocabulary.RDF; +import fr.inria.corese.core.next.impl.temp.CoreseAdaptedValueFactory; +import fr.inria.corese.core.next.impl.temp.CoreseModel; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.xml.sax.helpers.AttributesImpl; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Unit tests for the RDFXMLStatementEmitter class. + * + * This test suite verifies that the emitter correctly adds RDF statements to the provided + * Model based on various RDF/XML constructs including: + * - Plain literals + * - Typed literals + * - Language-tagged literals + * - Resource IRIs + * - Blank nodes + * - RDF types + * - Property attributes + */ +public class RDFXMLStatementEmitterTest { + + private Model model; + private ValueFactory factory; + private RDFXMLStatementEmitter emitter; + + @BeforeEach + public void setUp() { + model = new CoreseModel(); + factory = new CoreseAdaptedValueFactory(); + emitter = new RDFXMLStatementEmitter(model, factory); + } + + /** + * Test emitting a plain literal statement without language or datatype. + * Asserts that the triple is added to the model correctly. + */ + @Test + public void testEmitLiteral_plain() { + Literal literal = factory.createLiteral("hello"); + Resource subject = factory.createBNode(); + IRI predicate = factory.createIRI("http://example.org/predicate"); + emitter.emitLiteral(subject, predicate, "hello", null, null); + assertEquals(1, model.size()); + Iterable statements = model.getStatements(subject, predicate, literal); + boolean found = false; + for (Statement stmt : statements) { + if (stmt.getSubject().equals(subject) && + stmt.getPredicate().equals(predicate) && + stmt.getObject().stringValue().equals(literal.stringValue())) { + found = true; + break; + } + } + + assertTrue(found, "Expected statement not found in model"); + } + + /** + * Test emitting a literal with a language tag. + * Verifies that the correct literal is added to the model. + */ + @Test + public void testEmitLiteral_withLang() { + Resource subject = factory.createBNode(); + IRI predicate = factory.createIRI("http://example.org/predicate"); + emitter.emitLiteral(subject, predicate, "bonjour", null, "fr"); + + Value obj = model.objects().iterator().next(); + assertTrue(obj.isLiteral()); + assertEquals("bonjour", obj.stringValue()); + } + + /** + * Test emitting a literal with a datatype IRI. + * Verifies that the correct typed literal is added to the model. + */ + @Test + public void testEmitLiteral_withDatatype() { + Resource subject = factory.createBNode(); + IRI predicate = factory.createIRI("http://example.org/age"); + emitter.emitLiteral(subject, predicate, "42", XSD.INTEGER.getIRI().stringValue(), null); + + Value obj = model.objects().iterator().next(); + assertTrue(obj.isLiteral()); + assertEquals("42", obj.stringValue()); + } + + /** + * Test emitting a rdf:type statement for a subject. + * Verifies that the rdf:type triple is correctly created. + */ + @Test + public void testEmitType() { + Resource subject = factory.createIRI("http://example.org/Alice"); + emitter.emitType(subject, "http://example.org/Person"); + + assertTrue(model.contains(subject, RDF.type.getIRI(), factory.createIRI("http://example.org/Person"))); + } + + /** + * Test emitting a triple where the object is a resource IRI resolved against a base. + */ + @Test + public void testEmitResourceTriple() { + Resource subject = factory.createIRI("http://example.org/Alice"); + IRI predicate = factory.createIRI("http://example.org/knows"); + emitter.emitResourceTriple(subject, predicate, "Bob", "http://example.org/"); + + assertTrue(model.contains(subject, predicate, factory.createIRI("http://example.org/Bob"))); + } + + /** + * Test emitting a triple where the object is a blank node identified by nodeID. + */ + @Test + public void testEmitBNodeTriple() { + Resource subject = factory.createIRI("http://example.org/Alice"); + IRI predicate = factory.createIRI("http://example.org/knows"); + emitter.emitBNodeTriple(subject, predicate, "b123"); + + assertTrue(model.size() == 1); + Value obj = model.objects().iterator().next(); + assertTrue(obj.stringValue().contains("_:b123")); + } + + /** + * Test emitting a generic triple with subject, predicate, and object resources. + */ + @Test + public void testEmitTriple() { + Resource s = factory.createIRI("http://example.org/s"); + IRI p = factory.createIRI("http://example.org/p"); + Resource o = factory.createIRI("http://example.org/o"); + + emitter.emitTriple(s, p, o); + + assertTrue(model.contains(s, p, o)); + } + + /** + * Test emitting triples from XML attributes. + */ + @Test + public void testEmitPropertyAttributes() { + Resource s = factory.createIRI("http://example.org/thing"); + AttributesImpl attrs = new AttributesImpl(); + attrs.addAttribute("http://example.org/", "foo", "ex:foo", "CDATA", "val1"); + attrs.addAttribute("http://www.w3.org/1999/02/22-rdf-syntax-ns#", "about", "rdf:about", "CDATA", "ignored"); + + emitter.emitPropertyAttributes(s, attrs); + + assertEquals(1, model.size()); + Value object = model.objects().iterator().next(); + assertEquals("val1", object.stringValue()); + } +} \ No newline at end of file diff --git a/src/test/java/fr/inria/corese/core/next/impl/io/parser/rdfxml/RDFXMLUtilsTest.java b/src/test/java/fr/inria/corese/core/next/impl/io/parser/rdfxml/RDFXMLUtilsTest.java new file mode 100644 index 000000000..89321f2ee --- /dev/null +++ b/src/test/java/fr/inria/corese/core/next/impl/io/parser/rdfxml/RDFXMLUtilsTest.java @@ -0,0 +1,124 @@ +package fr.inria.corese.core.next.impl.io.parser.rdfxml; + +import fr.inria.corese.core.next.api.Model; +import fr.inria.corese.core.next.api.Resource; +import fr.inria.corese.core.next.api.ValueFactory; +import fr.inria.corese.core.next.impl.common.literal.XSD; +import fr.inria.corese.core.next.impl.common.vocabulary.RDF; +import fr.inria.corese.core.next.impl.temp.CoreseAdaptedValueFactory; +import fr.inria.corese.core.next.impl.temp.CoreseModel; +import org.junit.jupiter.api.Test; +import org.xml.sax.helpers.AttributesImpl; + +import java.util.List; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Unit tests for the RDFXMLUtils utility class. + * + * This test suite validates the correct behavior of various utility methods + * related to RDF/XML parsing, including QName expansion, datatype resolution, + * subject extraction, IRI resolution, container detection, syntax attribute recognition, + * and RDF collection creation. + */ +public class RDFXMLUtilsTest { + + private final ValueFactory factory = new CoreseAdaptedValueFactory(); + + /** + * Tests expansion of QNames into full IRIs using provided namespace and local name. + */ + @Test + public void testExpandQName() { + assertEquals("http://example.org/test", RDFXMLUtils.expandQName("http://example.org/", "test", "ex:test")); + assertEquals("ex:test", RDFXMLUtils.expandQName(null, null, "ex:test")); + } + + /** + * Tests resolution of known and unknown datatype URIs. + */ + @Test + public void testResolveDatatype() { + assertEquals(Optional.of(XSD.STRING), RDFXMLUtils.resolveDatatype(XSD.STRING.getIRI().stringValue())); + assertTrue(RDFXMLUtils.resolveDatatype("http://nonexistentdatatype").isEmpty()); + } + + /** + * Tests subject extraction using the rdf:about attribute. + */ + @Test + public void testExtractSubjectWithAbout() { + AttributesImpl attrs = new AttributesImpl(); + attrs.addAttribute(RDF.type.getNamespace(), "about", "", "CDATA", "http://example.org/subject"); + Resource subject = RDFXMLUtils.extractSubject(attrs, factory, null); + assertEquals("http://example.org/subject", subject.stringValue()); + } + + + /** + * Tests subject extraction using the rdf:nodeID attribute. + */ + @Test + public void testExtractSubjectWithNodeID() { + AttributesImpl attrs = new AttributesImpl(); + attrs.addAttribute(RDF.type.getNamespace(), "nodeID", "", "CDATA", "b123"); + Resource subject = RDFXMLUtils.extractSubject(attrs, factory, null); + assertTrue(subject.stringValue().contains("_:b123")); + } + + /** + * Tests subject extraction using the rdf:ID attribute with base URI resolution. + */ + @Test + public void testExtractSubjectWithID() { + AttributesImpl attrs = new AttributesImpl(); + attrs.addAttribute(RDF.type.getNamespace(), "ID", "", "CDATA", "id123"); + Resource subject = RDFXMLUtils.extractSubject(attrs, factory, "http://example.org/"); + assertEquals("http://example.org/id123", subject.stringValue()); + } + + /** + * Tests resolving a relative IRI against a base URI. + */ + @Test + public void testResolveAgainstBase() { + assertEquals("http://base.org/path", RDFXMLUtils.resolveAgainstBase("path", "http://base.org/")); + } + + /** + * Tests recognition of RDF/XML syntax attributes. + */ + @Test + public void testIsSyntaxAttribute() { + assertTrue(RDFXMLUtils.isSyntaxAttribute(RDF.type.getNamespace(), "about", "rdf:about")); + assertTrue(RDFXMLUtils.isSyntaxAttribute(null, "lang", "xml:lang")); + assertFalse(RDFXMLUtils.isSyntaxAttribute("http://example.org/", "type", "ex:type")); + } + + /** + * Tests detection of RDF container types (Bag, Seq, Alt). + */ + @Test + public void testIsContainer() { + assertTrue(RDFXMLUtils.isContainer("Bag", RDF.type.getNamespace())); + assertFalse(RDFXMLUtils.isContainer("notAContainer", "http://example.org/")); + } + + /** + * Tests creation of an RDF collection using rdf:first, rdf:rest, and rdf:nil. + */ + @Test + public void testCreateRdfCollection() { + Model model = new CoreseModel(); + Resource r1 = factory.createIRI("http://example.org/A"); + Resource r2 = factory.createIRI("http://example.org/B"); + Resource head = RDFXMLUtils.createRdfCollection(List.of(r1, r2), model, factory); + + assertNotNull(head); + assertTrue(model.size() > 0); + assertTrue(model.contains(null, RDF.first.getIRI(), r1)); + assertTrue(model.contains(null, RDF.rest.getIRI(), RDF.nil.getIRI())); + } +} \ No newline at end of file diff --git a/src/test/java/fr/inria/corese/core/next/impl/io/parser/trig/ANTLRTrigParserTest.java b/src/test/java/fr/inria/corese/core/next/impl/io/parser/trig/ANTLRTrigParserTest.java new file mode 100644 index 000000000..8a9cc7bec --- /dev/null +++ b/src/test/java/fr/inria/corese/core/next/impl/io/parser/trig/ANTLRTrigParserTest.java @@ -0,0 +1,183 @@ +package fr.inria.corese.core.next.impl.io.parser.trig; + +import fr.inria.corese.core.next.api.Literal; +import fr.inria.corese.core.next.api.Model; +import fr.inria.corese.core.next.api.Value; +import fr.inria.corese.core.next.api.ValueFactory; +import fr.inria.corese.core.next.api.io.parser.RDFParser; +import fr.inria.corese.core.next.impl.temp.CoreseAdaptedValueFactory; +import fr.inria.corese.core.next.impl.temp.CoreseModel; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.StringReader; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * Unit tests for the ANTLRTrigParser class. + * These tests verify the parser's ability to correctly parse Trig + * and interact with the Model and ValueFactory, including error handling + * and unescaping of IRIs and literals, and named graphs. + */ +class ANTLRTrigParserTest { + + private static final Logger logger = LoggerFactory.getLogger(ANTLRTrigParserTest.class); + + /** + * helper method to parse trig data into corese model + * + * @param trigData a string of rdf data in trig format + * @param baseURI the base uri + * @return Corese rdf model + * @throws Exception + */ + + private Model parseFromString(String trigData, String baseURI) throws Exception { + Model model = new CoreseModel(); + ValueFactory factory = new CoreseAdaptedValueFactory(); + RDFParser parser = new ANTLRTrigParser(model, factory); + parser.parse(new StringReader(trigData), baseURI); + return model; + } + + /** + * Helper method to print the model. + * @param model + */ + private void printModel(Model model) { + model.stream().forEach(stmt -> { + Value obj = stmt.getObject(); + String subjectString = stmt.getSubject().stringValue(); + String predicateString = stmt.getPredicate().stringValue(); + + if (obj instanceof Literal literal) { + String label = String.valueOf(literal.getLabel()); + String languageTag = literal.getLanguage().orElse(null); + + if (languageTag != null) { + logger.debug("({}, {}, \"{}\"@{})", + subjectString, + predicateString, + label, + languageTag); + } else { + logger.debug("({}, {}, \"{}\")", + subjectString, + predicateString, + label); + } + } else { + logger.debug("({}, {}, {})", + subjectString, + predicateString, + obj.stringValue()); + } + }); + } + + @Test + void testNamedGraphParsing() throws Exception { + String trig = """ + @prefix ex: + ex:Graph1 { + ex:Alice ex:knows ex:Bob . + }""".trim(); + + Model model = parseFromString(trig, null); + printModel(model); + assertEquals(1, model.size()); + + assertEquals(1, model.getNamespaces().size()); + + assertEquals(1, model.contexts().size()); + } + + @Test + void testDocumentThatContainsOneGraphExample1() throws Exception { + String trig = """ + # This document encodes one graph. + @prefix ex: . + @prefix : . + + :G1 { :Monica a ex:Person ; + ex:name "Monica Murphy" ; + ex:homepage ; + ex:email ; + ex:hasSkill ex:Management , + ex:Programming . } + """.trim(); + + Model model = parseFromString(trig, null); + printModel(model); + + assertEquals(6, model.size()); + + assertEquals(2, model.getNamespaces().size()); + + assertEquals(1, model.contexts().size()); + } + + @Test + void testDocumentThatContainsTwoGraphExample() throws Exception { + String trig = """ + # This document contains a same data as the + # previous example. + + @prefix rdf: . + @prefix dc: . + @prefix foaf: . + + # default graph - no {} used. + dc:publisher "Bob" . + dc:publisher "Alice" . + + # GRAPH keyword to highlight a named graph + # Abbreviation of triples using ; + GRAPH + { + [] foaf:name "Bob" ; + foaf:mbox ; + foaf:knows _:b . + } + + GRAPH + { + _:b foaf:name "Alice" ; + foaf:mbox + } + """.trim(); + + Model model = parseFromString(trig, null); + printModel(model); + + assertEquals(7, model.size()); + + assertEquals(3, model.getNamespaces().size()); + + assertEquals(3, model.contexts().size()); + } + + @Test + void testNestedBlankNodesWithSharedIdentifiers() throws Exception { + String trig = """ + @prefix ex: . + + GRAPH ex:graph1 { + ex:Alice ex:knows [ + ex:name "Bob" ; + ex:knows [ + ex:name "Charlie" + ] + ] ; + ex:email "alice@example.org" . + } + """.trim(); + Model model = parseFromString(trig, null); + printModel(model); + + assertEquals(5, model.size()); + + } +} \ No newline at end of file diff --git a/src/test/java/fr/inria/corese/core/next/impl/io/parser/trig/TriGListenerImplTest.java b/src/test/java/fr/inria/corese/core/next/impl/io/parser/trig/TriGListenerImplTest.java new file mode 100644 index 000000000..869f5ff5f --- /dev/null +++ b/src/test/java/fr/inria/corese/core/next/impl/io/parser/trig/TriGListenerImplTest.java @@ -0,0 +1,102 @@ +package fr.inria.corese.core.next.impl.io.parser.trig; + +import fr.inria.corese.core.next.api.Model; +import fr.inria.corese.core.next.api.ValueFactory; +import fr.inria.corese.core.next.impl.parser.antlr.TriGLexer; +import fr.inria.corese.core.next.impl.parser.antlr.TriGParser; +import fr.inria.corese.core.next.impl.temp.CoreseAdaptedValueFactory; +import fr.inria.corese.core.next.impl.temp.CoreseModel; +import org.antlr.v4.runtime.CharStream; +import org.antlr.v4.runtime.CharStreams; +import org.antlr.v4.runtime.CommonTokenStream; +import org.antlr.v4.runtime.tree.ParseTree; +import org.antlr.v4.runtime.tree.ParseTreeWalker; +import org.junit.jupiter.api.Test; + +import java.io.StringReader; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Unit tests for the TriGListenerImpl class. + * These tests verify that the listener correctly processes ANTLR parse tree contexts + * to extract and unescape RDF terms (IRIs, Blank Nodes, Literals) and add them to the model. + */ +class TriGListenerImplTest { + private Model parseTrig(String trigData) throws Exception { + ValueFactory factory = new CoreseAdaptedValueFactory(); + + CharStream input = CharStreams.fromReader(new StringReader(trigData)); + TriGLexer lexer = new TriGLexer(input); + CommonTokenStream tokens = new CommonTokenStream(lexer); + TriGParser parser = new TriGParser(tokens); + ParseTree tree = parser.trigDoc(); + + Model model = new CoreseModel(); + TriGListerner listener = new TriGListerner(model, factory, null); + ParseTreeWalker.DEFAULT.walk(listener, tree); + + return model; + } + + @Test + void testSimpleNamedGraph() throws Exception { + String trig = """ + @prefix ex: . + + GRAPH ex:graph { + ex:subject ex:predicate "Hello" . + } + """; + + Model model = parseTrig(trig); + assertEquals(1, model.size()); + assertEquals(1, model.contexts().size()); + } + + @Test + void testBlankNodeWithProperties() throws Exception { + String trig = """ + @prefix ex: . + GRAPH ex:graph { + ex:Bob ex:knows [ ex:name "Charlie" ] . + } + """; + + Model model = parseTrig(trig); + assertEquals(2, model.size()); + } + + @Test + void testMultipleGraphsAndBase() throws Exception { + String trig = """ + @base . + @prefix dc: . + @prefix ex: . + + dc:creator "Bob" . + + GRAPH ex:other { + dc:creator "Alice" . + } + """; + + Model model = parseTrig(trig); + assertEquals(2, model.contexts().size()); + assertEquals(2, model.size()); + } + + @Test + void testTypedLiteralsAndLang() throws Exception { + String trig = """ + @prefix ex: . + @prefix xsd: . + + ex:subject ex:age "30"^^xsd:integer ; + ex:name "Jean"@fr . + """; + + Model model = parseTrig(trig); + assertEquals(2, model.size()); + } +} diff --git a/src/test/java/fr/inria/corese/core/next/impl/io/parser/turtle/ANTLRTurtleParserTest.java b/src/test/java/fr/inria/corese/core/next/impl/io/parser/turtle/ANTLRTurtleParserTest.java index 13a245645..bdf67298b 100644 --- a/src/test/java/fr/inria/corese/core/next/impl/io/parser/turtle/ANTLRTurtleParserTest.java +++ b/src/test/java/fr/inria/corese/core/next/impl/io/parser/turtle/ANTLRTurtleParserTest.java @@ -11,6 +11,12 @@ import static org.junit.jupiter.api.Assertions.assertEquals; +/** + * Unit tests for the ANTLRTurtle class. + * These tests verify the parser's ability to correctly parse Turtle + * and interact with the Model and ValueFactory, including error handling + * and unescaping of IRIs and literals, and named graphs. + */ public class ANTLRTurtleParserTest { private Model parseFromString(String turtleData, String baseURI) throws Exception { Model model = new CoreseModel(); diff --git a/src/test/java/fr/inria/corese/core/next/impl/temp/CoreseIRITest.java b/src/test/java/fr/inria/corese/core/next/impl/temp/CoreseIRITest.java index 9519cd4f0..77bfc928c 100644 --- a/src/test/java/fr/inria/corese/core/next/impl/temp/CoreseIRITest.java +++ b/src/test/java/fr/inria/corese/core/next/impl/temp/CoreseIRITest.java @@ -30,6 +30,21 @@ public void constructorStringTest() { assertEquals("test", coreseIRI.getLocalName()); } + @Test + public void constructorStringTest_otherURIS() { + CoreseIRI coreseIRI_noSlash = new CoreseIRI("http://www.monicamurphy.org"); + assertEquals("http://www.monicamurphy.org", coreseIRI_noSlash.stringValue()); + assertEquals("http://www.monicamurphy.org", coreseIRI_noSlash.getCoreseNode().getLabel()); + assertEquals("http://www.monicamurphy.org", coreseIRI_noSlash.getNamespace()); + assertEquals("", coreseIRI_noSlash.getLocalName()); + + CoreseIRI coreseIRI_email = new CoreseIRI("mailto:monica@monicamurphy.org"); + assertEquals("mailto:monica@monicamurphy.org", coreseIRI_email.stringValue()); + assertEquals("mailto:monica@monicamurphy.org", coreseIRI_email.getCoreseNode().getLabel()); + assertEquals("mailto:monica@monicamurphy.org", coreseIRI_email.getNamespace()); + assertEquals("", coreseIRI_email.getLocalName()); + } + @Test public void constructorIriTest() { CoreseIRI coreseIRI = new CoreseIRI("http://example.org/test");