diff --git a/AGENTS.md b/AGENTS.md index 7110c7b1f22..84faa50ee6e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,12 +1,70 @@ -# AGENTS.md +# You are a very strong reasoner and planner. Use these critical instructions to structure your plans, thoughts, and responses. -Welcome, AI Agent! Your persistence, curiosity, and craftsmanship make a difference. Take your time, work methodically, validate thoroughly, and iterate. This repository is large and tests can take time — that’s expected and supported. +Before taking any action (either tool calls *or* responses to the user), you must proactively, methodically, and independently plan and reason about: -You need to read the entire AGENTS.md file and follow all instructions exactly. Keep this fresh in your context as you work. +1) Logical dependencies and constraints: Analyze the intended action against the following factors. Resolve conflicts in order of importance: + + 1.1) Policy-based rules, mandatory prerequisites, and constraints. + + 1.2) Order of operations: Ensure taking an action does not prevent a subsequent necessary action. + + 1.2.1) The user may request actions in a random order, but you may need to reorder operations to maximize successful completion of the task. + + 1.3) Other prerequisites (information and/or actions needed). + + 1.4) Explicit user constraints or preferences. + +2) Risk assessment: What are the consequences of taking the action? Will the new state cause any future issues? + + 2.1) For exploratory tasks (like searches), missing *optional* parameters is a LOW risk. + **Prefer calling the tool with the available information over asking the user, unless** your `Rule 1` (Logical Dependencies) reasoning determines that optional information is required for a later step in your plan. + +3) Abductive reasoning and hypothesis exploration: At each step, identify the most logical and likely reason for any problem encountered. + + 3.1) Look beyond immediate or obvious causes. The most likely reason may not be the simplest and may require deeper inference. + + 3.2) Hypotheses may require additional research. Each hypothesis may take multiple steps to test. + + 3.3) Prioritize hypotheses based on likelihood, but do not discard less likely ones prematurely. A low-probability event may still be the root cause. + +4) Outcome evaluation and adaptability: Does the previous observation require any changes to your plan? + + 4.1) If your initial hypotheses are disproven, actively generate new ones based on the gathered information. + +5) Information availability: Incorporate all applicable and alternative sources of information, including: + + 5.1) Using available tools and their capabilities + 5.2) All policies, rules, checklists, and constraints + 5.3) Previous observations and conversation history + 5.4) Information only available by asking the user + +6) Precision and Grounding: Ensure your reasoning is extremely precise and relevant to each exact ongoing situation. + + 6.1) Verify your claims by quoting the exact applicable information (including policies) when referring to them. + +7) Completeness: Ensure that all requirements, constraints, options, and preferences are exhaustively incorporated into your plan. + + 7.1) Resolve conflicts using the order of importance in #1. + + 7.2) Avoid premature conclusions: There may be multiple relevant options for a given situation. + + 7.2.1) To check for whether an option is relevant, reason about all information sources from #5. + + 7.2.2) You may need to consult the user to even know whether something is applicable. Do not assume it is not applicable without checking. + + 7.3) Review applicable sources of information from #5 to confirm which are relevant to the current state. + +8) Persistence and patience: Do not give up unless all the reasoning above is exhausted. + + 8.1) Don't be dissuaded by time taken or user frustration. + + 8.2) This persistence must be intelligent: On *transient* errors (e.g. please try again), you *must* retry **unless an explicit retry limit (e.g., max x tries) has been reached**. If such a limit is hit, you *must* stop. On *other* errors, you must change your strategy or arguments, not repeat the same failed call. + +9) Inhibit your response: only take an action after all the above reasoning is completed. Once you've taken an action, you cannot take it back. --- -## Read‑Me‑Now: Proportional Test‑First Rule (Default) +## Read‑Me‑Now: Proportional Test‑First Rule **Default:** Use **test‑first (TDD)** for any change that alters externally observable behavior. @@ -31,21 +89,17 @@ It is illegal to `-q` when running tests! ## Four Routines: Choose Your Path -**Routine A — Full TDD (Default)** +**Routine A — Full TDD** **Routine B — Change without new tests (Proportional, gated)** **Routine C — Spike/Investigate (No production changes)** **Routine D — ExecPlans: Complex features or significant refactors** ### Decision quickstart -1. **Is ExecPlans required (complex feature, significant refactor or requested by the user)?** +1. **Is ExecPlans required (complex feature, significant refactor, etc. or explicitly requested by the user)?** → **Yes:** **Routine D (ExecPlans)**. Use an ExecPlan (as described in .agent/PLANS.md) from design to implementation. → **No:** continue. -2**Is new externally observable behavior required?** -→ **Yes:** **Routine A (Full TDD)**. Add the smallest failing test first. -→ **No:** continue. - 3**Does a failing test already exist in this repo that pinpoints the issue?** → **Yes:** **Routine B (Bugfix using existing failing test).** → **No:** continue. @@ -54,11 +108,13 @@ It is illegal to `-q` when running tests! → **Yes:** **Routine B (Refactor/micro‑perf/documentation/build).** → **No or unsure:** continue. -5**Is this purely an investigation/design spike with no production code changes?** -→ **Yes:** **Routine C (Spike/Investigate).** -→ **No or unsure:** **Routine A.** +4. **Is new externally observable behavior required?** + → **Yes:** **Routine A (Full TDD)**. Add the smallest failing test first. + → **No:** continue. -**When in doubt, choose Routine A (Full TDD).** Ambiguity is risk; tests are insurance. +5. **Is this purely an investigation/design spike with no production code changes?** + → **Yes:** **Routine C (Spike/Investigate).** + → **No or unsure:** **Routine A.** --- @@ -66,6 +122,10 @@ It is illegal to `-q` when running tests! When writing complex features or significant refactors, use an ExecPlan (as described in PLANS.md) from design to implementation. +## ExecPlans + +When writing complex features or significant refactors, use an ExecPlan (as described in PLANS.md) from design to implementation. + ## PIOSEE Decision Model (Adopted) Use this as a compact, repeatable loop for anything from a one‑line bug fix to a multi‑quarter program. @@ -257,12 +317,12 @@ Plan `-am` is helpful for **compiles**, hazardous for **tests**. * ✅ Use `-am` **only** for compile/verify with tests skipped (e.g. `-Pquick`): - * `mvn -o -Dmaven.repo.local=.m2_repo -pl -am -Pquick install` + * `mvn -o -Dmaven.repo.local=.m2_repo -pl -am -Pquick clean install` * ❌ Do **not** use `-am` with `verify` when tests are enabled. **Two-step pattern (fast + safe)** 1. **Compile deps fast (skip tests):** - `mvn -o -Dmaven.repo.local=.m2_repo -pl -am -Pquick install` + `mvn -o -Dmaven.repo.local=.m2_repo -pl -am -Pquick clean install` 2. **Run tests:** `mvn -o -Dmaven.repo.local=.m2_repo -pl verify | tail -500` @@ -276,9 +336,10 @@ It is illegal to `-q` when running tests! The Maven reactor resolves inter-module dependencies from the configured local Maven repository (here: `.m2_repo`). Running `install` publishes your changed modules there so downstream modules and tests pick up the correct versions. -* Always run `mvn -o -Dmaven.repo.local=.m2_repo -Pquick clean install | tail -200` before you start working. This command typically takes up to 30 seconds. Never use a shorter timeout than 30,000 ms. -* Always run `mvn -o -Dmaven.repo.local=.m2_repo -Pquick clean install | tail -200` before any `verify` or test runs. -* If offline resolution fails due to a missing dependency or plugin, rerun the exact `install` command once without `-o`, then return offline. +* Always run `mvn -T 1C -o -Dmaven.repo.local=.m2_repo -Pquick clean install | tail -200` before you start working. This command typically takes up to 30 seconds. Never use a shorter timeout than 60,000 ms. +* Always run `mvn -T 1C -o -Dmaven.repo.local=.m2_repo -Pquick clean install | tail -200` before any `verify` or test runs. +* If offline resolution fails due to a missing dependency or plugin, run the command without `-o`: `mvn -Dmaven.repo.local=.m2_repo -Pquick clean install | tail -200`, then return offline. +* If it fails for any other reason, run the command without `-T 1C`: `mvn -o -Dmaven.repo.local=.m2_repo -Pquick clean install | tail -200`. * Skipping this step can lead to stale or missing artifacts during tests, producing confusing compilation or linkage errors. * Always use a workspace-local Maven repository: append `-Dmaven.repo.local=.m2_repo` to all Maven commands (install, verify, formatter, etc.). * Always try to run these commands first to see if they run without needing any approvals from the user w.r.t. the sandboxing. @@ -287,8 +348,8 @@ Why this is mandatory - Tests must not use `-am`. Without `-am`, Maven will not build upstream modules when you run tests; it will resolve cross‑module dependencies from the configured local repository (here: `.m2_repo`). - Therefore, tests only see whatever versions were last published to the configured local repo (`.m2_repo`). If you change code in one module and then run tests in another, those tests will not see your changes unless the updated module has been installed to `.m2_repo` first. -- The reliable way to ensure all tests always use the latest code across the entire multi‑module build is to install all modules to the configured local repo (`.m2_repo`) before running any tests: run `mvn -o -Dmaven.repo.local=.m2_repo -Pquick install` at the repository root. -- In tight loops you may also install a specific module and its deps (`-pl -am -Pquick install`) to iterate quickly, but before executing tests anywhere that depend on your changes, run a root‑level `mvn -o -Dmaven.repo.local=.m2_repo -Pquick install` so the latest jars are available to the reactor from `.m2_repo`. +- The reliable way to ensure all tests always use the latest code across the entire multi‑module build is to install all modules to the configured local repo (`.m2_repo`) before running any tests: run `mvn -T 1C -o -Dmaven.repo.local=.m2_repo -Pquick clean install` at the repository root. +- In tight loops you may also install a specific module and its deps (`-pl -am -Pquick clean install`) to iterate quickly, but before executing tests anywhere that depend on your changes, run a root‑level `mvn -T 1C -o -Dmaven.repo.local=.m2_repo -Pquick clean install` so the latest jars are available to the reactor from `.m2_repo`. --- ## Quick Start (First 10 Minutes) @@ -297,7 +358,7 @@ Why this is mandatory * Inspect root `pom.xml` and module tree (see “Maven Module Overview”). * Search fast with ripgrep: `rg -n ""` 2. **Build sanity (fast, skip tests)** - * `mvn -o -Dmaven.repo.local=.m2_repo -Pquick install | tail -200` + * `mvn -T 1C -o -Dmaven.repo.local=.m2_repo -Pquick clean install | tail -200` 3. **Format (Java, imports, XML)** * `mvn -o -Dmaven.repo.local=.m2_repo -q -T 2C formatter:format impsort:sort xml-format:xml-format` 4. **Targeted tests (tight loops)** @@ -313,7 +374,7 @@ It is illegal to `-q` when running tests! --- -## Routine A — Full TDD (Default) +## Routine A — Full TDD > Use for **all behavior‑changing work** and whenever Routine B gates do not all pass. @@ -407,7 +468,7 @@ When writing complex features or significant refactors, use an ExecPlan (as desc * **Plan:** small, verifiable steps; keep one `in_progress`, or follow PLANS.md (ExecPlans) * **Change:** minimal, surgical edits; keep style/structure consistent. * **Format:** `mvn -o -Dmaven.repo.local=.m2_repo -q -T 2C formatter:format impsort:sort xml-format:xml-format` -* **Compile (fast):** `mvn -o -Dmaven.repo.local=.m2_repo -pl -am -Pquick install | tail -500` +* **Compile (fast):** `mvn -o -Dmaven.repo.local=.m2_repo -pl -am -Pquick clean install | tail -500` * **Test:** start smallest (class/method → module). For integration, run module `verify`. * **Triage:** read reports; fix root cause; expand scope only when needed. * **Iterate:** keep momentum; escalate only when blocked or irreversible. @@ -514,7 +575,7 @@ Do **not** modify existing headers’ years. ## Pre‑Commit Checklist * **Format:** `mvn -o -Dmaven.repo.local=.m2_repo -q -T 2C formatter:format impsort:sort xml-format:xml-format` -* **Compile (fast path):** `mvn -o -Dmaven.repo.local=.m2_repo -Pquick install | tail -200` +* **Compile (fast path):** `mvn -T 1C -o -Dmaven.repo.local=.m2_repo -Pquick clean install | tail -200` * **Tests (targeted):** `mvn -o -Dmaven.repo.local=.m2_repo -pl verify | tail -500` (broaden as needed) * **Reports:** zero new failures in Surefire/Failsafe, or explain precisely. * **Evidence:** Routine A — failing pre‑fix + passing post‑fix. @@ -629,7 +690,7 @@ Do **not** modify existing headers’ years. ## Build * **Build without tests (fast path):** - `mvn -o -Dmaven.repo.local=.m2_repo -Pquick install` + `mvn -T 1C -o -Dmaven.repo.local=.m2_repo -Pquick clean install` * **Verify with tests:** Targeted module(s): `mvn -o -Dmaven.repo.local=.m2_repo -pl verify` Entire repo: `mvn -o -Dmaven.repo.local=.m2_repo verify` (use judiciously) @@ -751,7 +812,6 @@ rdf4j: root project │ ├── lmdb: Sail implementation that stores data to disk using LMDB. │ ├── lucene-api: StackableSail API offering full-text search on literals, based on Apache Lucene. │ ├── lucene: StackableSail implementation offering full-text search on literals, based on Apache Lucene. - │ ├── solr: StackableSail implementation offering full-text search on literals, based on Solr. │ ├── elasticsearch: StackableSail implementation offering full-text search on literals, based on Elastic Search. │ ├── elasticsearch-store: Store for utilizing Elasticsearch as a triplestore. │ └── extensible-store: Store that can be extended with a simple user-made backend. @@ -791,7 +851,6 @@ rdf4j: root project ├── model: RDF4J: Model compliance tests ├── sparql: Tests for the SPARQL query language implementation ├── lucene: Compliance Tests for LuceneSail. - ├── solr: Tests for Solr Sail. ├── elasticsearch: Tests for Elasticsearch. └── geosparql: Tests for the GeoSPARQL query language implementation ├── examples: Examples and HowTos for use of RDF4J in Java @@ -803,6 +862,7 @@ rdf4j: root project * Don’t commit or push unless explicitly asked. * Don’t add new dependencies without explicit approval. +* Never revert unrelated working tree changes ### Version Control Conventions diff --git a/PLANS.md b/PLANS.md index 7d8044a9f71..b491c7abe2a 100644 --- a/PLANS.md +++ b/PLANS.md @@ -1,152 +1,153 @@ # Codex Execution Plans (ExecPlans): - + This document describes the requirements for an execution plan ("ExecPlan"), a design document that a coding agent can follow to deliver a working feature or system change. Treat the reader as a complete beginner to this repository: they have only the current working tree and the single ExecPlan file you provide. There is no memory of prior plans and no external context. - + ## How to use ExecPlans and PLANS.md - + When authoring an executable specification (ExecPlan), follow PLANS.md _to the letter_. If it is not in your context, refresh your memory by reading the entire PLANS.md file. Be thorough in reading (and re-reading) source material to produce an accurate specification. When creating a spec, start from the skeleton and flesh it out as you do your research. - + When implementing an executable specification (ExecPlan), do not prompt the user for "next steps"; simply proceed to the next milestone. Keep all sections up to date, add or split entries in the list at every stopping point to affirmatively state the progress made and next steps. Resolve ambiguities autonomously, and commit frequently. - + When discussing an executable specification (ExecPlan), record decisions in a log in the spec for posterity; it should be unambiguously clear why any change to the specification was made. ExecPlans are living documents, and it should always be possible to restart from _only_ the ExecPlan and no other work. - + When researching a design with challenging requirements or significant unknowns, use milestones to implement proof of concepts, "toy implementations", etc., that allow validating whether the user's proposal is feasible. Read the source code of libraries by finding or acquiring them, research deeply, and include prototypes to guide a fuller implementation. - + ## Requirements - + NON-NEGOTIABLE REQUIREMENTS: - + * Every ExecPlan must be fully self-contained. Self-contained means that in its current form it contains all knowledge and instructions needed for a novice to succeed. * Every ExecPlan is a living document. Contributors are required to revise it as progress is made, as discoveries occur, and as design decisions are finalized. Each revision must remain fully self-contained. * Every ExecPlan must enable a complete novice to implement the feature end-to-end without prior knowledge of this repo. * Every ExecPlan must produce a demonstrably working behavior, not merely code changes to "meet a definition". * Every ExecPlan must define every term of art in plain language or do not use it. - + Purpose and intent come first. Begin by explaining, in a few sentences, why the work matters from a user's perspective: what someone can do after this change that they could not do before, and how to see it working. Then guide the reader through the exact steps to achieve that outcome, including what to edit, what to run, and what they should observe. - + The agent executing your plan can list files, read files, search, run the project, and run tests. It does not know any prior context and cannot infer what you meant from earlier milestones. Repeat any assumption you rely on. Do not point to external blogs or docs; if knowledge is required, embed it in the plan itself in your own words. If an ExecPlan builds upon a prior ExecPlan and that file is checked in, incorporate it by reference. If it is not, you must include all relevant context from that plan. - + ## Formatting - + Format and envelope are simple and strict. Each ExecPlan must be one single fenced code block labeled as `md` that begins and ends with triple backticks. Do not nest additional triple-backtick code fences inside; when you need to show commands, transcripts, diffs, or code, present them as indented blocks within that single fence. Use indentation for clarity rather than code fences inside an ExecPlan to avoid prematurely closing the ExecPlan's code fence. Use two newlines after every heading, use # and ## and so on, and correct syntax for ordered and unordered lists. - + When writing an ExecPlan to a Markdown (.md) file where the content of the file *is only* the single ExecPlan, you should omit the triple backticks. - + Write in plain prose. Prefer sentences over lists. Avoid checklists, tables, and long enumerations unless brevity would obscure meaning. Checklists are permitted only in the `Progress` section, where they are mandatory. Narrative sections must remain prose-first. - + ## Guidelines - + Self-containment and plain language are paramount. If you introduce a phrase that is not ordinary English ("daemon", "middleware", "RPC gateway", "filter graph"), define it immediately and remind the reader how it manifests in this repository (for example, by naming the files or commands where it appears). Do not say "as defined previously" or "according to the architecture doc." Include the needed explanation here, even if you repeat yourself. - + Avoid common failure modes. Do not rely on undefined jargon. Do not describe "the letter of a feature" so narrowly that the resulting code compiles but does nothing meaningful. Do not outsource key decisions to the reader. When ambiguity exists, resolve it in the plan itself and explain why you chose that path. Err on the side of over-explaining user-visible effects and under-specifying incidental implementation details. - + Anchor the plan with observable outcomes. State what the user can do after implementation, the commands to run, and the outputs they should see. Acceptance should be phrased as behavior a human can verify ("after starting the server, navigating to [http://localhost:8080/health](http://localhost:8080/health) returns HTTP 200 with body OK") rather than internal attributes ("added a HealthCheck struct"). If a change is internal, explain how its impact can still be demonstrated (for example, by running tests that fail before and pass after, and by showing a scenario that uses the new behavior). - + Specify repository context explicitly. Name files with full repository-relative paths, name functions and modules precisely, and describe where new files should be created. If touching multiple areas, include a short orientation paragraph that explains how those parts fit together so a novice can navigate confidently. When running commands, show the working directory and exact command line. When outcomes depend on environment, state the assumptions and provide alternatives when reasonable. - + Be idempotent and safe. Write the steps so they can be run multiple times without causing damage or drift. If a step can fail halfway, include how to retry or adapt. If a migration or destructive operation is necessary, spell out backups or safe fallbacks. Prefer additive, testable changes that can be validated as you go. - + Validation is not optional. Include instructions to run tests, to start the system if applicable, and to observe it doing something useful. Describe comprehensive testing for any new features or capabilities. Include expected outputs and error messages so a novice can tell success from failure. Where possible, show how to prove that the change is effective beyond compilation (for example, through a small end-to-end scenario, a CLI invocation, or an HTTP request/response transcript). State the exact test commands appropriate to the project’s toolchain and how to interpret their results. - + Capture evidence. When your steps produce terminal output, short diffs, or logs, include them inside the single fenced block as indented examples. Keep them concise and focused on what proves success. If you need to include a patch, prefer file-scoped diffs or small excerpts that a reader can recreate by following your instructions rather than pasting large blobs. - + ## Milestones - + Milestones are narrative, not bureaucracy. If you break the work into milestones, introduce each with a brief paragraph that describes the scope, what will exist at the end of the milestone that did not exist before, the commands to run, and the acceptance you expect to observe. Keep it readable as a story: goal, work, result, proof. Progress and milestones are distinct: milestones tell the story, progress tracks granular work. Both must exist. Never abbreviate a milestone merely for the sake of brevity, do not leave out details that could be crucial to a future implementation. - + Each milestone must be independently verifiable and incrementally implement the overall goal of the execution plan. - + ## Living plans and design decisions - + * ExecPlans are living documents. As you make key design decisions, update the plan to record both the decision and the thinking behind it. Record all decisions in the `Decision Log` section. * ExecPlans must contain and maintain a `Progress` section, a `Surprises & Discoveries` section, a `Decision Log`, and an `Outcomes & Retrospective` section. These are not optional. * When you discover optimizer behavior, performance tradeoffs, unexpected bugs, or inverse/unapply semantics that shaped your approach, capture those observations in the `Surprises & Discoveries` section with short evidence snippets (test output is ideal). * If you change course mid-implementation, document why in the `Decision Log` and reflect the implications in `Progress`. Plans are guides for the next contributor as much as checklists for you. * At completion of a major task or the full plan, write an `Outcomes & Retrospective` entry summarizing what was achieved, what remains, and lessons learned. - + # Prototyping milestones and parallel implementations - + It is acceptable—-and often encouraged—-to include explicit prototyping milestones when they de-risk a larger change. Examples: adding a low-level operator to a dependency to validate feasibility, or exploring two composition orders while measuring optimizer effects. Keep prototypes additive and testable. Clearly label the scope as “prototyping”; describe how to run and observe results; and state the criteria for promoting or discarding the prototype. - + Prefer additive code changes followed by subtractions that keep tests passing. Parallel implementations (e.g., keeping an adapter alongside an older path during migration) are fine when they reduce risk or enable tests to continue passing during a large migration. Describe how to validate both paths and how to retire one safely with tests. When working with multiple new libraries or feature areas, consider creating spikes that evaluate the feasibility of these features _independently_ of one another, proving that the external library performs as expected and implements the features we need in isolation. - + ## Skeleton of a Good ExecPlan - + ```md # - + This ExecPlan is a living document. The sections `Progress`, `Surprises & Discoveries`, `Decision Log`, and `Outcomes & Retrospective` must be kept up to date as work proceeds. - + If PLANS.md file is checked into the repo, reference the path to that file here from the repository root and note that this document must be maintained in accordance with PLANS.md. - + ## Purpose / Big Picture - + Explain in a few sentences what someone gains after this change and how they can see it working. State the user-visible behavior you will enable. - + ## Progress - + Use a list with checkboxes to summarize granular steps. Every stopping point must be documented here, even if it requires splitting a partially completed task into two (“done” vs. “remaining”). This section must always reflect the actual current state of the work. - + - [x] (2025-10-01 13:00Z) Example completed step. - [ ] Example incomplete step. - [ ] Example partially completed step (completed: X; remaining: Y). - + Use timestamps to measure rates of progress. - + ## Surprises & Discoveries - + Document unexpected behaviors, bugs, optimizations, or insights discovered during implementation. Provide concise evidence. - + - Observation: … Evidence: … - + ## Decision Log - + Record every decision made while working on the plan in the format: - + - Decision: … Rationale: … Date/Author: … - + ## Outcomes & Retrospective - + Summarize outcomes, gaps, and lessons learned at major milestones or at completion. Compare the result against the original purpose. - + ## Context and Orientation - + Describe the current state relevant to this task as if the reader knows nothing. Name the key files and modules by full path. Define any non-obvious term you will use. Do not refer to prior plans. - + ## Plan of Work - + Describe, in prose, the sequence of edits and additions. For each edit, name the file and location (function, module) and what to insert or change. Keep it concrete and minimal. - + ## Concrete Steps - + State the exact commands to run and where to run them (working directory). When a command generates output, show a short expected transcript so the reader can compare. This section must be updated as work proceeds. - + ## Validation and Acceptance - + Describe how to start or exercise the system and what to observe. Phrase acceptance as behavior, with specific inputs and outputs. If tests are involved, say "run and expect passed; the new test fails before the change and passes after>". - + ## Idempotence and Recovery - + If steps can be repeated safely, say so. If a step is risky, provide a safe retry or rollback path. Keep the environment clean after completion. - + ## Artifacts and Notes - + Include the most important transcripts, diffs, or snippets as indented examples. Keep them concise and focused on what proves success. - + ## Interfaces and Dependencies - + Be prescriptive. Name the libraries, modules, and services to use and why. Specify the types, traits/interfaces, and function signatures that must exist at the end of the milestone. Prefer stable names and paths such as `crate::module::function` or `package.submodule.Interface`. E.g.: - + In crates/foo/planner.rs, define: - + pub trait Planner { fn plan(&self, observed: &Observed) -> Vec; } ``` - + If you follow the guidance above, a single, stateless agent -- or a human novice -- can read your ExecPlan from top to bottom and produce a working, observable result. That is the bar: SELF-CONTAINED, SELF-SUFFICIENT, NOVICE-GUIDING, OUTCOME-FOCUSED. - + When you revise a plan, you must ensure your changes are comprehensively reflected across all sections, including the living document sections, and you must write a note at the bottom of the plan describing the change and the reason why. ExecPlans must describe not just the what but the why for almost everything. + diff --git a/assembly-descriptors/pom.xml b/assembly-descriptors/pom.xml index 87c90124f1d..7a2fcc2a5f1 100644 --- a/assembly-descriptors/pom.xml +++ b/assembly-descriptors/pom.xml @@ -4,7 +4,7 @@ org.eclipse.rdf4j rdf4j - 5.2.2-SNAPSHOT + 5.2.3-SNAPSHOT rdf4j-assembly-descriptors RDF4J: Assembly Descriptors diff --git a/assembly/pom.xml b/assembly/pom.xml index 62b0034bc9d..4e882ad224f 100644 --- a/assembly/pom.xml +++ b/assembly/pom.xml @@ -4,7 +4,7 @@ org.eclipse.rdf4j rdf4j - 5.2.2-SNAPSHOT + 5.2.3-SNAPSHOT rdf4j-assembly pom diff --git a/bom/pom.xml b/bom/pom.xml index 0e288e50977..98124d1cd90 100644 --- a/bom/pom.xml +++ b/bom/pom.xml @@ -4,7 +4,7 @@ org.eclipse.rdf4j rdf4j - 5.2.2-SNAPSHOT + 5.2.3-SNAPSHOT rdf4j-bom pom diff --git a/compliance/elasticsearch/pom.xml b/compliance/elasticsearch/pom.xml index f5147a5d25b..b29d720713e 100644 --- a/compliance/elasticsearch/pom.xml +++ b/compliance/elasticsearch/pom.xml @@ -4,7 +4,7 @@ org.eclipse.rdf4j rdf4j-compliance - 5.2.2-SNAPSHOT + 5.2.3-SNAPSHOT rdf4j-elasticsearch-compliance RDF4J: Elasticsearch Sail Tests diff --git a/compliance/geosparql/pom.xml b/compliance/geosparql/pom.xml index d30b98e09bf..e8308f36fe0 100644 --- a/compliance/geosparql/pom.xml +++ b/compliance/geosparql/pom.xml @@ -4,7 +4,7 @@ org.eclipse.rdf4j rdf4j-compliance - 5.2.2-SNAPSHOT + 5.2.3-SNAPSHOT rdf4j-geosparql-compliance RDF4J: GeoSPARQL compliance tests diff --git a/compliance/lucene/pom.xml b/compliance/lucene/pom.xml index d16359d7f72..e779ad654a5 100644 --- a/compliance/lucene/pom.xml +++ b/compliance/lucene/pom.xml @@ -4,7 +4,7 @@ org.eclipse.rdf4j rdf4j-compliance - 5.2.2-SNAPSHOT + 5.2.3-SNAPSHOT rdf4j-lucene-compliance RDF4J: Lucene Sail Tests diff --git a/compliance/model/pom.xml b/compliance/model/pom.xml index 5159cdc5b1f..e2dc7118215 100644 --- a/compliance/model/pom.xml +++ b/compliance/model/pom.xml @@ -3,7 +3,7 @@ rdf4j-compliance org.eclipse.rdf4j - 5.2.2-SNAPSHOT + 5.2.3-SNAPSHOT 4.0.0 rdf4j-model-compliance diff --git a/compliance/pom.xml b/compliance/pom.xml index 1e20902e108..8cfcda6df00 100644 --- a/compliance/pom.xml +++ b/compliance/pom.xml @@ -4,7 +4,7 @@ org.eclipse.rdf4j rdf4j - 5.2.2-SNAPSHOT + 5.2.3-SNAPSHOT rdf4j-compliance pom diff --git a/compliance/repository/pom.xml b/compliance/repository/pom.xml index aca2b82d283..ca7b5fe9aac 100644 --- a/compliance/repository/pom.xml +++ b/compliance/repository/pom.xml @@ -4,7 +4,7 @@ org.eclipse.rdf4j rdf4j-compliance - 5.2.2-SNAPSHOT + 5.2.3-SNAPSHOT rdf4j-repository-compliance war diff --git a/compliance/rio/pom.xml b/compliance/rio/pom.xml index 45d71f37af1..87b4c4c03c6 100644 --- a/compliance/rio/pom.xml +++ b/compliance/rio/pom.xml @@ -4,7 +4,7 @@ org.eclipse.rdf4j rdf4j-compliance - 5.2.2-SNAPSHOT + 5.2.3-SNAPSHOT rdf4j-rio-compliance RDF4J: Rio compliance tests diff --git a/compliance/solr/pom.xml b/compliance/solr/pom.xml index c313e20cc98..9e493225d57 100644 --- a/compliance/solr/pom.xml +++ b/compliance/solr/pom.xml @@ -4,7 +4,7 @@ org.eclipse.rdf4j rdf4j-compliance - 5.2.2-SNAPSHOT + 5.2.3-SNAPSHOT rdf4j-solr-compliance RDF4J: Solr Sail Tests diff --git a/compliance/sparql/pom.xml b/compliance/sparql/pom.xml index d8a1afa40a1..1d42a185059 100644 --- a/compliance/sparql/pom.xml +++ b/compliance/sparql/pom.xml @@ -4,7 +4,7 @@ org.eclipse.rdf4j rdf4j-compliance - 5.2.2-SNAPSHOT + 5.2.3-SNAPSHOT rdf4j-sparql-compliance war diff --git a/core/client/pom.xml b/core/client/pom.xml index 5b794050112..02a25fdbd56 100644 --- a/core/client/pom.xml +++ b/core/client/pom.xml @@ -4,7 +4,7 @@ org.eclipse.rdf4j rdf4j-core - 5.2.2-SNAPSHOT + 5.2.3-SNAPSHOT rdf4j-client RDF4J: Client Libraries diff --git a/core/collection-factory/api/pom.xml b/core/collection-factory/api/pom.xml index 298eb2cbdcc..ed74ef74fd2 100644 --- a/core/collection-factory/api/pom.xml +++ b/core/collection-factory/api/pom.xml @@ -4,7 +4,7 @@ org.eclipse.rdf4j rdf4j-collection-factory - 5.2.2-SNAPSHOT + 5.2.3-SNAPSHOT rdf4j-collection-factory-api RDF4J: Collection Factory - API diff --git a/core/collection-factory/mapdb/pom.xml b/core/collection-factory/mapdb/pom.xml index c2afe64326b..33cfbd2e346 100644 --- a/core/collection-factory/mapdb/pom.xml +++ b/core/collection-factory/mapdb/pom.xml @@ -4,7 +4,7 @@ org.eclipse.rdf4j rdf4j-collection-factory - 5.2.2-SNAPSHOT + 5.2.3-SNAPSHOT rdf4j-collection-factory-mapdb RDF4J: Collection Factory - Map DB backed diff --git a/core/collection-factory/mapdb3/pom.xml b/core/collection-factory/mapdb3/pom.xml index 7cad9a497d2..b0119d2c882 100644 --- a/core/collection-factory/mapdb3/pom.xml +++ b/core/collection-factory/mapdb3/pom.xml @@ -4,7 +4,7 @@ org.eclipse.rdf4j rdf4j-collection-factory - 5.2.2-SNAPSHOT + 5.2.3-SNAPSHOT rdf4j-collection-factory-mapdb3 RDF4J: Collection Factory - Map DB v3 backed diff --git a/core/collection-factory/pom.xml b/core/collection-factory/pom.xml index fa41d02f9f7..358f516b7c8 100644 --- a/core/collection-factory/pom.xml +++ b/core/collection-factory/pom.xml @@ -4,7 +4,7 @@ org.eclipse.rdf4j rdf4j-core - 5.2.2-SNAPSHOT + 5.2.3-SNAPSHOT rdf4j-collection-factory pom diff --git a/core/common/annotation/pom.xml b/core/common/annotation/pom.xml index 744d7371323..182cf30859e 100644 --- a/core/common/annotation/pom.xml +++ b/core/common/annotation/pom.xml @@ -4,7 +4,7 @@ org.eclipse.rdf4j rdf4j-common - 5.2.2-SNAPSHOT + 5.2.3-SNAPSHOT rdf4j-common-annotation RDF4J: common annotation diff --git a/core/common/exception/pom.xml b/core/common/exception/pom.xml index f2075622303..3a2c3f5a5e0 100644 --- a/core/common/exception/pom.xml +++ b/core/common/exception/pom.xml @@ -4,7 +4,7 @@ org.eclipse.rdf4j rdf4j-common - 5.2.2-SNAPSHOT + 5.2.3-SNAPSHOT rdf4j-common-exception RDF4J: common exception diff --git a/core/common/io/pom.xml b/core/common/io/pom.xml index 1eb338119df..9976b29554b 100644 --- a/core/common/io/pom.xml +++ b/core/common/io/pom.xml @@ -4,7 +4,7 @@ org.eclipse.rdf4j rdf4j-common - 5.2.2-SNAPSHOT + 5.2.3-SNAPSHOT rdf4j-common-io RDF4J: common IO diff --git a/core/common/iterator/pom.xml b/core/common/iterator/pom.xml index a1ac21facf0..e1ba32933f2 100644 --- a/core/common/iterator/pom.xml +++ b/core/common/iterator/pom.xml @@ -4,7 +4,7 @@ org.eclipse.rdf4j rdf4j-common - 5.2.2-SNAPSHOT + 5.2.3-SNAPSHOT rdf4j-common-iterator RDF4J: common iterators diff --git a/core/common/order/pom.xml b/core/common/order/pom.xml index b737fd0ba06..3ad0ccc1f96 100644 --- a/core/common/order/pom.xml +++ b/core/common/order/pom.xml @@ -4,7 +4,7 @@ org.eclipse.rdf4j rdf4j-common - 5.2.2-SNAPSHOT + 5.2.3-SNAPSHOT rdf4j-common-order RDF4J: common order diff --git a/core/common/pom.xml b/core/common/pom.xml index 4250cb6d5b3..3f15667d895 100644 --- a/core/common/pom.xml +++ b/core/common/pom.xml @@ -4,7 +4,7 @@ org.eclipse.rdf4j rdf4j-core - 5.2.2-SNAPSHOT + 5.2.3-SNAPSHOT rdf4j-common pom diff --git a/core/common/text/pom.xml b/core/common/text/pom.xml index 59fc26baa1c..387efbef405 100644 --- a/core/common/text/pom.xml +++ b/core/common/text/pom.xml @@ -4,7 +4,7 @@ org.eclipse.rdf4j rdf4j-common - 5.2.2-SNAPSHOT + 5.2.3-SNAPSHOT rdf4j-common-text RDF4J: common text diff --git a/core/common/transaction/pom.xml b/core/common/transaction/pom.xml index fe0a7d518ec..c18a4f86826 100644 --- a/core/common/transaction/pom.xml +++ b/core/common/transaction/pom.xml @@ -4,7 +4,7 @@ org.eclipse.rdf4j rdf4j-common - 5.2.2-SNAPSHOT + 5.2.3-SNAPSHOT rdf4j-common-transaction RDF4J: common transaction diff --git a/core/common/xml/pom.xml b/core/common/xml/pom.xml index 2fd7a45473f..15911a031f8 100644 --- a/core/common/xml/pom.xml +++ b/core/common/xml/pom.xml @@ -4,7 +4,7 @@ org.eclipse.rdf4j rdf4j-common - 5.2.2-SNAPSHOT + 5.2.3-SNAPSHOT rdf4j-common-xml RDF4J: common XML diff --git a/core/http/client/pom.xml b/core/http/client/pom.xml index 1710bdb9336..8decf0f5112 100644 --- a/core/http/client/pom.xml +++ b/core/http/client/pom.xml @@ -4,7 +4,7 @@ org.eclipse.rdf4j rdf4j-http - 5.2.2-SNAPSHOT + 5.2.3-SNAPSHOT rdf4j-http-client RDF4J: HTTP client diff --git a/core/http/pom.xml b/core/http/pom.xml index 6e926cdad7e..0aeddd0f97b 100644 --- a/core/http/pom.xml +++ b/core/http/pom.xml @@ -4,7 +4,7 @@ org.eclipse.rdf4j rdf4j-core - 5.2.2-SNAPSHOT + 5.2.3-SNAPSHOT rdf4j-http pom diff --git a/core/http/protocol/pom.xml b/core/http/protocol/pom.xml index 091a2e91abb..e4bc9fc71e5 100644 --- a/core/http/protocol/pom.xml +++ b/core/http/protocol/pom.xml @@ -4,7 +4,7 @@ org.eclipse.rdf4j rdf4j-http - 5.2.2-SNAPSHOT + 5.2.3-SNAPSHOT rdf4j-http-protocol RDF4J: HTTP protocol diff --git a/core/model-api/pom.xml b/core/model-api/pom.xml index 2abd6355351..286ddeeed21 100644 --- a/core/model-api/pom.xml +++ b/core/model-api/pom.xml @@ -4,7 +4,7 @@ org.eclipse.rdf4j rdf4j-core - 5.2.2-SNAPSHOT + 5.2.3-SNAPSHOT rdf4j-model-api RDF4J: Model API diff --git a/core/model-vocabulary/pom.xml b/core/model-vocabulary/pom.xml index faf7f792111..86496d38298 100644 --- a/core/model-vocabulary/pom.xml +++ b/core/model-vocabulary/pom.xml @@ -4,7 +4,7 @@ org.eclipse.rdf4j rdf4j-core - 5.2.2-SNAPSHOT + 5.2.3-SNAPSHOT rdf4j-model-vocabulary RDF4J: RDF Vocabularies diff --git a/core/model/pom.xml b/core/model/pom.xml index 99d423b95f1..3153260cc67 100644 --- a/core/model/pom.xml +++ b/core/model/pom.xml @@ -4,7 +4,7 @@ org.eclipse.rdf4j rdf4j-core - 5.2.2-SNAPSHOT + 5.2.3-SNAPSHOT rdf4j-model RDF4J: Model diff --git a/core/pom.xml b/core/pom.xml index f68b18fef97..0df5a764c22 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -4,7 +4,7 @@ org.eclipse.rdf4j rdf4j - 5.2.2-SNAPSHOT + 5.2.3-SNAPSHOT rdf4j-core pom diff --git a/core/query/pom.xml b/core/query/pom.xml index a2203fe1a4d..a770a9a9dac 100644 --- a/core/query/pom.xml +++ b/core/query/pom.xml @@ -4,7 +4,7 @@ org.eclipse.rdf4j rdf4j-core - 5.2.2-SNAPSHOT + 5.2.3-SNAPSHOT rdf4j-query RDF4J: Query diff --git a/core/queryalgebra/evaluation/pom.xml b/core/queryalgebra/evaluation/pom.xml index eddc4b7149b..54982e477e7 100644 --- a/core/queryalgebra/evaluation/pom.xml +++ b/core/queryalgebra/evaluation/pom.xml @@ -4,7 +4,7 @@ org.eclipse.rdf4j rdf4j-queryalgebra - 5.2.2-SNAPSHOT + 5.2.3-SNAPSHOT rdf4j-queryalgebra-evaluation RDF4J: Query algebra - evaluation diff --git a/core/queryalgebra/geosparql/pom.xml b/core/queryalgebra/geosparql/pom.xml index 5b94d6d89ce..e0ad3a69e74 100644 --- a/core/queryalgebra/geosparql/pom.xml +++ b/core/queryalgebra/geosparql/pom.xml @@ -4,7 +4,7 @@ org.eclipse.rdf4j rdf4j-queryalgebra - 5.2.2-SNAPSHOT + 5.2.3-SNAPSHOT rdf4j-queryalgebra-geosparql RDF4J: Query algebra - GeoSPARQL diff --git a/core/queryalgebra/model/pom.xml b/core/queryalgebra/model/pom.xml index 4f837c9bd5d..44899c60df4 100644 --- a/core/queryalgebra/model/pom.xml +++ b/core/queryalgebra/model/pom.xml @@ -4,7 +4,7 @@ org.eclipse.rdf4j rdf4j-queryalgebra - 5.2.2-SNAPSHOT + 5.2.3-SNAPSHOT rdf4j-queryalgebra-model RDF4J: Query algebra - model diff --git a/core/queryalgebra/pom.xml b/core/queryalgebra/pom.xml index 8bc6a07f93a..aad87d4f170 100644 --- a/core/queryalgebra/pom.xml +++ b/core/queryalgebra/pom.xml @@ -4,7 +4,7 @@ org.eclipse.rdf4j rdf4j-core - 5.2.2-SNAPSHOT + 5.2.3-SNAPSHOT rdf4j-queryalgebra pom diff --git a/core/queryparser/api/pom.xml b/core/queryparser/api/pom.xml index b4273160b3b..81b9f655bbd 100644 --- a/core/queryparser/api/pom.xml +++ b/core/queryparser/api/pom.xml @@ -4,7 +4,7 @@ org.eclipse.rdf4j rdf4j-queryparser - 5.2.2-SNAPSHOT + 5.2.3-SNAPSHOT rdf4j-queryparser-api RDF4J: Query parser - API diff --git a/core/queryparser/pom.xml b/core/queryparser/pom.xml index 8679dad3547..7ba8e6cf2ba 100644 --- a/core/queryparser/pom.xml +++ b/core/queryparser/pom.xml @@ -4,7 +4,7 @@ org.eclipse.rdf4j rdf4j-core - 5.2.2-SNAPSHOT + 5.2.3-SNAPSHOT rdf4j-queryparser pom diff --git a/core/queryparser/sparql/pom.xml b/core/queryparser/sparql/pom.xml index d7df87b99f0..7eb8f3f7307 100644 --- a/core/queryparser/sparql/pom.xml +++ b/core/queryparser/sparql/pom.xml @@ -4,7 +4,7 @@ org.eclipse.rdf4j rdf4j-queryparser - 5.2.2-SNAPSHOT + 5.2.3-SNAPSHOT rdf4j-queryparser-sparql RDF4J: Query parser - SPARQL diff --git a/core/queryrender/pom.xml b/core/queryrender/pom.xml index 7ade20df19d..3b5c797d421 100644 --- a/core/queryrender/pom.xml +++ b/core/queryrender/pom.xml @@ -4,7 +4,7 @@ org.eclipse.rdf4j rdf4j-core - 5.2.2-SNAPSHOT + 5.2.3-SNAPSHOT rdf4j-queryrender RDF4J: Query Rendering diff --git a/core/queryresultio/api/pom.xml b/core/queryresultio/api/pom.xml index 3c747ce2659..1c8526474b9 100644 --- a/core/queryresultio/api/pom.xml +++ b/core/queryresultio/api/pom.xml @@ -4,7 +4,7 @@ org.eclipse.rdf4j rdf4j-queryresultio - 5.2.2-SNAPSHOT + 5.2.3-SNAPSHOT rdf4j-queryresultio-api RDF4J: Query result IO - API diff --git a/core/queryresultio/binary/pom.xml b/core/queryresultio/binary/pom.xml index 479023268e5..dc8c3a308d8 100644 --- a/core/queryresultio/binary/pom.xml +++ b/core/queryresultio/binary/pom.xml @@ -4,7 +4,7 @@ org.eclipse.rdf4j rdf4j-queryresultio - 5.2.2-SNAPSHOT + 5.2.3-SNAPSHOT rdf4j-queryresultio-binary RDF4J: Query result IO - binary diff --git a/core/queryresultio/ods/pom.xml b/core/queryresultio/ods/pom.xml index 02786a3ebeb..75d77de85b6 100644 --- a/core/queryresultio/ods/pom.xml +++ b/core/queryresultio/ods/pom.xml @@ -4,7 +4,7 @@ org.eclipse.rdf4j rdf4j-queryresultio - 5.2.2-SNAPSHOT + 5.2.3-SNAPSHOT rdf4j-queryresultio-sparqlods RDF4J: Query result IO - ODS diff --git a/core/queryresultio/pom.xml b/core/queryresultio/pom.xml index 6711be4a312..a8c6b9101c9 100644 --- a/core/queryresultio/pom.xml +++ b/core/queryresultio/pom.xml @@ -4,7 +4,7 @@ org.eclipse.rdf4j rdf4j-core - 5.2.2-SNAPSHOT + 5.2.3-SNAPSHOT rdf4j-queryresultio pom diff --git a/core/queryresultio/sparqljson/pom.xml b/core/queryresultio/sparqljson/pom.xml index f50a6bfc339..600bdf81ddb 100644 --- a/core/queryresultio/sparqljson/pom.xml +++ b/core/queryresultio/sparqljson/pom.xml @@ -4,7 +4,7 @@ org.eclipse.rdf4j rdf4j-queryresultio - 5.2.2-SNAPSHOT + 5.2.3-SNAPSHOT rdf4j-queryresultio-sparqljson RDF4J: Query result IO - SPARQL/JSON diff --git a/core/queryresultio/sparqlxml/pom.xml b/core/queryresultio/sparqlxml/pom.xml index 21a32534429..6a1d3f371aa 100644 --- a/core/queryresultio/sparqlxml/pom.xml +++ b/core/queryresultio/sparqlxml/pom.xml @@ -4,7 +4,7 @@ org.eclipse.rdf4j rdf4j-queryresultio - 5.2.2-SNAPSHOT + 5.2.3-SNAPSHOT rdf4j-queryresultio-sparqlxml RDF4J: Query result IO - SPARQL/XML diff --git a/core/queryresultio/text/pom.xml b/core/queryresultio/text/pom.xml index 343987e1b88..d135eb98079 100644 --- a/core/queryresultio/text/pom.xml +++ b/core/queryresultio/text/pom.xml @@ -4,7 +4,7 @@ org.eclipse.rdf4j rdf4j-queryresultio - 5.2.2-SNAPSHOT + 5.2.3-SNAPSHOT rdf4j-queryresultio-text RDF4J: Query result IO - plain text booleans diff --git a/core/queryresultio/xlsx/pom.xml b/core/queryresultio/xlsx/pom.xml index bdd1a36e050..8a7d397bedd 100644 --- a/core/queryresultio/xlsx/pom.xml +++ b/core/queryresultio/xlsx/pom.xml @@ -4,7 +4,7 @@ org.eclipse.rdf4j rdf4j-queryresultio - 5.2.2-SNAPSHOT + 5.2.3-SNAPSHOT rdf4j-queryresultio-sparqlxlsx RDF4J: Query result IO - XSLX diff --git a/core/repository/api/pom.xml b/core/repository/api/pom.xml index 6b894952220..379858e325b 100644 --- a/core/repository/api/pom.xml +++ b/core/repository/api/pom.xml @@ -4,7 +4,7 @@ org.eclipse.rdf4j rdf4j-repository - 5.2.2-SNAPSHOT + 5.2.3-SNAPSHOT rdf4j-repository-api RDF4J: Repository - API diff --git a/core/repository/contextaware/pom.xml b/core/repository/contextaware/pom.xml index 8249947619a..3485ba645d9 100644 --- a/core/repository/contextaware/pom.xml +++ b/core/repository/contextaware/pom.xml @@ -4,7 +4,7 @@ org.eclipse.rdf4j rdf4j-repository - 5.2.2-SNAPSHOT + 5.2.3-SNAPSHOT rdf4j-repository-contextaware RDF4J: Repository - context aware (wrapper) diff --git a/core/repository/dataset/pom.xml b/core/repository/dataset/pom.xml index f601b256f95..df3f93addf6 100644 --- a/core/repository/dataset/pom.xml +++ b/core/repository/dataset/pom.xml @@ -4,7 +4,7 @@ org.eclipse.rdf4j rdf4j-repository - 5.2.2-SNAPSHOT + 5.2.3-SNAPSHOT rdf4j-repository-dataset RDF4J: DatasetRepository (wrapper) diff --git a/core/repository/event/pom.xml b/core/repository/event/pom.xml index 3b17e8ac582..b827ca57cd3 100644 --- a/core/repository/event/pom.xml +++ b/core/repository/event/pom.xml @@ -4,7 +4,7 @@ org.eclipse.rdf4j rdf4j-repository - 5.2.2-SNAPSHOT + 5.2.3-SNAPSHOT rdf4j-repository-event RDF4J: Repository - event (wrapper) diff --git a/core/repository/http/pom.xml b/core/repository/http/pom.xml index 8b59e400777..a8ee2176904 100644 --- a/core/repository/http/pom.xml +++ b/core/repository/http/pom.xml @@ -4,7 +4,7 @@ org.eclipse.rdf4j rdf4j-repository - 5.2.2-SNAPSHOT + 5.2.3-SNAPSHOT rdf4j-repository-http RDF4J: HTTPRepository diff --git a/core/repository/manager/pom.xml b/core/repository/manager/pom.xml index faaab9fa76f..146758ccac2 100644 --- a/core/repository/manager/pom.xml +++ b/core/repository/manager/pom.xml @@ -4,7 +4,7 @@ org.eclipse.rdf4j rdf4j-repository - 5.2.2-SNAPSHOT + 5.2.3-SNAPSHOT rdf4j-repository-manager RDF4J: Repository manager diff --git a/core/repository/pom.xml b/core/repository/pom.xml index f7e4cb76a31..df6438e587b 100644 --- a/core/repository/pom.xml +++ b/core/repository/pom.xml @@ -4,7 +4,7 @@ org.eclipse.rdf4j rdf4j-core - 5.2.2-SNAPSHOT + 5.2.3-SNAPSHOT rdf4j-repository pom diff --git a/core/repository/sail/pom.xml b/core/repository/sail/pom.xml index ebde006341b..06b549e2fae 100644 --- a/core/repository/sail/pom.xml +++ b/core/repository/sail/pom.xml @@ -4,7 +4,7 @@ org.eclipse.rdf4j rdf4j-repository - 5.2.2-SNAPSHOT + 5.2.3-SNAPSHOT rdf4j-repository-sail RDF4J: SailRepository diff --git a/core/repository/sparql/pom.xml b/core/repository/sparql/pom.xml index a8a1a0b7ed0..c6d6f49ccb1 100644 --- a/core/repository/sparql/pom.xml +++ b/core/repository/sparql/pom.xml @@ -4,7 +4,7 @@ org.eclipse.rdf4j rdf4j-repository - 5.2.2-SNAPSHOT + 5.2.3-SNAPSHOT rdf4j-repository-sparql RDF4J: SPARQL Repository diff --git a/core/repository/sparql/src/main/java/org/eclipse/rdf4j/repository/sparql/federation/RepositoryFederatedService.java b/core/repository/sparql/src/main/java/org/eclipse/rdf4j/repository/sparql/federation/RepositoryFederatedService.java index 936733bfa58..9c8e7a47e7e 100644 --- a/core/repository/sparql/src/main/java/org/eclipse/rdf4j/repository/sparql/federation/RepositoryFederatedService.java +++ b/core/repository/sparql/src/main/java/org/eclipse/rdf4j/repository/sparql/federation/RepositoryFederatedService.java @@ -16,6 +16,9 @@ import java.util.LinkedList; import java.util.List; import java.util.Set; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; import org.eclipse.rdf4j.common.iteration.CloseableIteration; import org.eclipse.rdf4j.common.iteration.EmptyIteration; @@ -62,6 +65,10 @@ private class BatchingServiceIteration extends JoinExecutorBase { private final Service service; + private final ExecutorService threadExecutor = Executors.newSingleThreadExecutor(); + + private final Future querySubmissionTask; + /** * @param inputBindings * @throws QueryEvaluationException @@ -71,13 +78,26 @@ public BatchingServiceIteration(CloseableIteration inputBindings, super(inputBindings, null, EmptyBindingSet.getInstance()); this.blockSize = blockSize; this.service = service; - run(); + + // Set up a consumer task to send HTTP requests in parallel. This must be done in a + // separate thread, because submitting HTTP requests may block if the HTTP pool is full. + // In that case, we would enter a deadlock, with the main thread waiting for both the + // pool to yield, and the consumer of the bindings to read from the queue. + // See: https://github.com/eclipse-rdf4j/rdf4j/discussions/5120 + // Test case: https://github.com/tkuhn/rdf4j-timeout-test + try { + querySubmissionTask = threadExecutor.submit(this::run); + } catch (Exception e) { + throw new QueryEvaluationException("Failed to start a thread for batched federated query submission", + e); + } } @Override protected void handleBindings() throws Exception { + // Note: any exceptions here will be intercepted by the caller and tossed asynchronously + // via the rightQueue. while (!isClosed() && leftIter.hasNext()) { - ArrayList blockBindings = new ArrayList<>(blockSize); for (int i = 0; i < blockSize; i++) { if (!leftIter.hasNext()) { @@ -87,9 +107,19 @@ protected void handleBindings() throws Exception { } CloseableIteration materializedIter = new CollectionIteration<>( blockBindings); + // evaluateInternal is BLOCKING if the HTTP pool is exhausted addResult(evaluateInternal(service, materializedIter, service.getBaseURI())); } } + + @Override + public void handleClose() throws QueryEvaluationException { + super.handleClose(); + if (querySubmissionTask != null) { + querySubmissionTask.cancel(true); + } + threadExecutor.shutdownNow(); + } } /** @@ -631,4 +661,4 @@ private static void closeQuietly(RepositoryConnection conn) { logger.debug("Details: ", t); } } -} +} \ No newline at end of file diff --git a/core/rio/api/pom.xml b/core/rio/api/pom.xml index 491bbe5a102..45a6bd2ff2f 100644 --- a/core/rio/api/pom.xml +++ b/core/rio/api/pom.xml @@ -4,7 +4,7 @@ org.eclipse.rdf4j rdf4j-rio - 5.2.2-SNAPSHOT + 5.2.3-SNAPSHOT rdf4j-rio-api RDF4J: Rio - API diff --git a/core/rio/binary/pom.xml b/core/rio/binary/pom.xml index 50c6668605c..77c701c7713 100644 --- a/core/rio/binary/pom.xml +++ b/core/rio/binary/pom.xml @@ -4,7 +4,7 @@ org.eclipse.rdf4j rdf4j-rio - 5.2.2-SNAPSHOT + 5.2.3-SNAPSHOT rdf4j-rio-binary RDF4J: Rio - Binary diff --git a/core/rio/datatypes/pom.xml b/core/rio/datatypes/pom.xml index 6e874bb380c..e7941d0d8ac 100644 --- a/core/rio/datatypes/pom.xml +++ b/core/rio/datatypes/pom.xml @@ -4,7 +4,7 @@ org.eclipse.rdf4j rdf4j-rio - 5.2.2-SNAPSHOT + 5.2.3-SNAPSHOT rdf4j-rio-datatypes RDF4J: Rio - Datatypes diff --git a/core/rio/hdt/pom.xml b/core/rio/hdt/pom.xml index 57c357c25e1..82b317ffcb3 100644 --- a/core/rio/hdt/pom.xml +++ b/core/rio/hdt/pom.xml @@ -4,7 +4,7 @@ org.eclipse.rdf4j rdf4j-rio - 5.2.2-SNAPSHOT + 5.2.3-SNAPSHOT rdf4j-rio-hdt jar diff --git a/core/rio/jsonld-legacy/pom.xml b/core/rio/jsonld-legacy/pom.xml index 28129bb391a..5bc96f79d3d 100644 --- a/core/rio/jsonld-legacy/pom.xml +++ b/core/rio/jsonld-legacy/pom.xml @@ -4,7 +4,7 @@ org.eclipse.rdf4j rdf4j-rio - 5.2.2-SNAPSHOT + 5.2.3-SNAPSHOT rdf4j-rio-jsonld-legacy RDF4J: Rio - JSON-LD 1.0 (legacy) diff --git a/core/rio/jsonld/pom.xml b/core/rio/jsonld/pom.xml index cb5238e4272..0fd593ff16a 100644 --- a/core/rio/jsonld/pom.xml +++ b/core/rio/jsonld/pom.xml @@ -4,7 +4,7 @@ org.eclipse.rdf4j rdf4j-rio - 5.2.2-SNAPSHOT + 5.2.3-SNAPSHOT rdf4j-rio-jsonld RDF4J: Rio - JSON-LD diff --git a/core/rio/languages/pom.xml b/core/rio/languages/pom.xml index 39cf210bfdd..76a1855f2a6 100644 --- a/core/rio/languages/pom.xml +++ b/core/rio/languages/pom.xml @@ -4,7 +4,7 @@ org.eclipse.rdf4j rdf4j-rio - 5.2.2-SNAPSHOT + 5.2.3-SNAPSHOT rdf4j-rio-languages RDF4J: Rio - Languages diff --git a/core/rio/n3/pom.xml b/core/rio/n3/pom.xml index 9a424c7c020..dea069c488a 100644 --- a/core/rio/n3/pom.xml +++ b/core/rio/n3/pom.xml @@ -4,7 +4,7 @@ org.eclipse.rdf4j rdf4j-rio - 5.2.2-SNAPSHOT + 5.2.3-SNAPSHOT rdf4j-rio-n3 RDF4J: Rio - N3 (writer-only) diff --git a/core/rio/nquads/pom.xml b/core/rio/nquads/pom.xml index 8bda4bef787..b0ebeaa9db7 100644 --- a/core/rio/nquads/pom.xml +++ b/core/rio/nquads/pom.xml @@ -4,7 +4,7 @@ org.eclipse.rdf4j rdf4j-rio - 5.2.2-SNAPSHOT + 5.2.3-SNAPSHOT rdf4j-rio-nquads RDF4J: Rio - N-Quads diff --git a/core/rio/ntriples/pom.xml b/core/rio/ntriples/pom.xml index c18d4d755da..8d4882f6b42 100644 --- a/core/rio/ntriples/pom.xml +++ b/core/rio/ntriples/pom.xml @@ -4,7 +4,7 @@ org.eclipse.rdf4j rdf4j-rio - 5.2.2-SNAPSHOT + 5.2.3-SNAPSHOT rdf4j-rio-ntriples RDF4J: Rio - N-Triples diff --git a/core/rio/pom.xml b/core/rio/pom.xml index 4a383451978..c44889c3ed7 100644 --- a/core/rio/pom.xml +++ b/core/rio/pom.xml @@ -4,7 +4,7 @@ org.eclipse.rdf4j rdf4j-core - 5.2.2-SNAPSHOT + 5.2.3-SNAPSHOT rdf4j-rio pom diff --git a/core/rio/rdfjson/pom.xml b/core/rio/rdfjson/pom.xml index 2bd430f70d1..1f2250d3a46 100644 --- a/core/rio/rdfjson/pom.xml +++ b/core/rio/rdfjson/pom.xml @@ -4,7 +4,7 @@ org.eclipse.rdf4j rdf4j-rio - 5.2.2-SNAPSHOT + 5.2.3-SNAPSHOT rdf4j-rio-rdfjson RDF4J: Rio - RDF/JSON diff --git a/core/rio/rdfxml/pom.xml b/core/rio/rdfxml/pom.xml index d51829efb98..84807e46e25 100644 --- a/core/rio/rdfxml/pom.xml +++ b/core/rio/rdfxml/pom.xml @@ -4,7 +4,7 @@ org.eclipse.rdf4j rdf4j-rio - 5.2.2-SNAPSHOT + 5.2.3-SNAPSHOT rdf4j-rio-rdfxml RDF4J: Rio - RDF/XML diff --git a/core/rio/trig/pom.xml b/core/rio/trig/pom.xml index f641ddcca8d..fe9e6b57ab5 100644 --- a/core/rio/trig/pom.xml +++ b/core/rio/trig/pom.xml @@ -4,7 +4,7 @@ org.eclipse.rdf4j rdf4j-rio - 5.2.2-SNAPSHOT + 5.2.3-SNAPSHOT rdf4j-rio-trig RDF4J: Rio - TriG diff --git a/core/rio/trix/pom.xml b/core/rio/trix/pom.xml index b46fcabe425..d8f3fa4c566 100644 --- a/core/rio/trix/pom.xml +++ b/core/rio/trix/pom.xml @@ -4,7 +4,7 @@ org.eclipse.rdf4j rdf4j-rio - 5.2.2-SNAPSHOT + 5.2.3-SNAPSHOT rdf4j-rio-trix RDF4J: Rio - TriX diff --git a/core/rio/turtle/pom.xml b/core/rio/turtle/pom.xml index 5213825b842..5bb289d609b 100644 --- a/core/rio/turtle/pom.xml +++ b/core/rio/turtle/pom.xml @@ -4,7 +4,7 @@ org.eclipse.rdf4j rdf4j-rio - 5.2.2-SNAPSHOT + 5.2.3-SNAPSHOT rdf4j-rio-turtle RDF4J: Rio - Turtle diff --git a/core/sail/api/pom.xml b/core/sail/api/pom.xml index 46dc2b464b7..2119e5e58a9 100644 --- a/core/sail/api/pom.xml +++ b/core/sail/api/pom.xml @@ -4,7 +4,7 @@ org.eclipse.rdf4j rdf4j-sail - 5.2.2-SNAPSHOT + 5.2.3-SNAPSHOT rdf4j-sail-api RDF4J: Sail API diff --git a/core/sail/base/pom.xml b/core/sail/base/pom.xml index d640ff5f4ee..8e12949c141 100644 --- a/core/sail/base/pom.xml +++ b/core/sail/base/pom.xml @@ -4,7 +4,7 @@ org.eclipse.rdf4j rdf4j-sail - 5.2.2-SNAPSHOT + 5.2.3-SNAPSHOT rdf4j-sail-base RDF4J: Sail base implementations diff --git a/core/sail/elasticsearch-store/pom.xml b/core/sail/elasticsearch-store/pom.xml index 84491092c6f..30fd88e4e5b 100644 --- a/core/sail/elasticsearch-store/pom.xml +++ b/core/sail/elasticsearch-store/pom.xml @@ -4,7 +4,7 @@ org.eclipse.rdf4j rdf4j-sail - 5.2.2-SNAPSHOT + 5.2.3-SNAPSHOT rdf4j-sail-elasticsearch-store RDF4J: Elasticsearch Store diff --git a/core/sail/elasticsearch/pom.xml b/core/sail/elasticsearch/pom.xml index 6f68dce5b73..3245432d0b7 100644 --- a/core/sail/elasticsearch/pom.xml +++ b/core/sail/elasticsearch/pom.xml @@ -4,7 +4,7 @@ org.eclipse.rdf4j rdf4j-sail - 5.2.2-SNAPSHOT + 5.2.3-SNAPSHOT rdf4j-sail-elasticsearch RDF4J: Elastic Search Sail Index diff --git a/core/sail/extensible-store/pom.xml b/core/sail/extensible-store/pom.xml index 80fb49f0141..d6c6549c71d 100644 --- a/core/sail/extensible-store/pom.xml +++ b/core/sail/extensible-store/pom.xml @@ -4,7 +4,7 @@ org.eclipse.rdf4j rdf4j-sail - 5.2.2-SNAPSHOT + 5.2.3-SNAPSHOT rdf4j-sail-extensible-store RDF4J: Extensible Store diff --git a/core/sail/inferencer/pom.xml b/core/sail/inferencer/pom.xml index 5997d3b2552..09d53c4ce4b 100644 --- a/core/sail/inferencer/pom.xml +++ b/core/sail/inferencer/pom.xml @@ -4,7 +4,7 @@ org.eclipse.rdf4j rdf4j-sail - 5.2.2-SNAPSHOT + 5.2.3-SNAPSHOT rdf4j-sail-inferencer RDF4J: Inferencer Sails diff --git a/core/sail/lmdb/pom.xml b/core/sail/lmdb/pom.xml index 82ec4209967..2398419f830 100644 --- a/core/sail/lmdb/pom.xml +++ b/core/sail/lmdb/pom.xml @@ -4,7 +4,7 @@ org.eclipse.rdf4j rdf4j-sail - 5.2.2-SNAPSHOT + 5.2.3-SNAPSHOT rdf4j-sail-lmdb RDF4J: LmdbStore diff --git a/core/sail/lucene-api/pom.xml b/core/sail/lucene-api/pom.xml index 97d99470fca..0a300eccd18 100644 --- a/core/sail/lucene-api/pom.xml +++ b/core/sail/lucene-api/pom.xml @@ -4,7 +4,7 @@ org.eclipse.rdf4j rdf4j-sail - 5.2.2-SNAPSHOT + 5.2.3-SNAPSHOT rdf4j-sail-lucene-api RDF4J: Lucene Sail API diff --git a/core/sail/lucene/pom.xml b/core/sail/lucene/pom.xml index e0d313f8f76..603aa97060c 100644 --- a/core/sail/lucene/pom.xml +++ b/core/sail/lucene/pom.xml @@ -4,7 +4,7 @@ org.eclipse.rdf4j rdf4j-sail - 5.2.2-SNAPSHOT + 5.2.3-SNAPSHOT rdf4j-sail-lucene RDF4J: Lucene Sail Index diff --git a/core/sail/memory/pom.xml b/core/sail/memory/pom.xml index 2326f4ddf72..8cf2989032b 100644 --- a/core/sail/memory/pom.xml +++ b/core/sail/memory/pom.xml @@ -4,7 +4,7 @@ org.eclipse.rdf4j rdf4j-sail - 5.2.2-SNAPSHOT + 5.2.3-SNAPSHOT rdf4j-sail-memory RDF4J: MemoryStore diff --git a/core/sail/model/pom.xml b/core/sail/model/pom.xml index 354c921b209..637aaf587d9 100644 --- a/core/sail/model/pom.xml +++ b/core/sail/model/pom.xml @@ -4,7 +4,7 @@ org.eclipse.rdf4j rdf4j-sail - 5.2.2-SNAPSHOT + 5.2.3-SNAPSHOT rdf4j-sail-model RDF4J: Sail Model diff --git a/core/sail/nativerdf/pom.xml b/core/sail/nativerdf/pom.xml index c3357ea3aa2..ff7481ed6a0 100644 --- a/core/sail/nativerdf/pom.xml +++ b/core/sail/nativerdf/pom.xml @@ -4,7 +4,7 @@ org.eclipse.rdf4j rdf4j-sail - 5.2.2-SNAPSHOT + 5.2.3-SNAPSHOT rdf4j-sail-nativerdf RDF4J: NativeStore diff --git a/core/sail/pom.xml b/core/sail/pom.xml index e96be017f80..d66127fdc7f 100644 --- a/core/sail/pom.xml +++ b/core/sail/pom.xml @@ -4,7 +4,7 @@ org.eclipse.rdf4j rdf4j-core - 5.2.2-SNAPSHOT + 5.2.3-SNAPSHOT rdf4j-sail pom diff --git a/core/sail/shacl/pom.xml b/core/sail/shacl/pom.xml index 4ccc459b118..02eb8d5dbab 100644 --- a/core/sail/shacl/pom.xml +++ b/core/sail/shacl/pom.xml @@ -4,7 +4,7 @@ org.eclipse.rdf4j rdf4j-sail - 5.2.2-SNAPSHOT + 5.2.3-SNAPSHOT rdf4j-shacl RDF4J: SHACL diff --git a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ShaclSailValidationReportHelper.java b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ShaclSailValidationReportHelper.java index a1c1410f644..9100683f8b2 100644 --- a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ShaclSailValidationReportHelper.java +++ b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ShaclSailValidationReportHelper.java @@ -22,6 +22,7 @@ import org.eclipse.rdf4j.rio.Rio; import org.eclipse.rdf4j.rio.WriterConfig; import org.eclipse.rdf4j.rio.helpers.BasicWriterSettings; +import org.eclipse.rdf4j.sail.shacl.results.ValidationReport; /** * @author Florian Kleedorfer @@ -54,6 +55,16 @@ public static Optional getValidationReportAsString(Throwable t) { return Optional.of(reportAsString); } + public static Optional getValidationReportAsString(ValidationReport t) { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + printValidationReport(t, baos); + String reportAsString = baos.toString(); + if (reportAsString == null || reportAsString.isBlank()) { + return Optional.empty(); + } + return Optional.of(reportAsString); + } + /** * Finds a validation report using {@link #getValidationReport(Throwable)} and pretty-prints it to the specified * output stream. @@ -68,6 +79,12 @@ public static void printValidationReport(Throwable t, OutputStream out) { } } + public static void printValidationReport(ValidationReport t, OutputStream out) { + Model model = t.asModel(); + Rio.write(model, out, RDFFormat.TURTLE, WRITER_CONFIG); + + } + /** * Looks for a {@link ValidationException} starting with the specified throwable and working back through the cause * references, and returns the validation report as a {@link Model} if one is found. diff --git a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/PropertyShape.java b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/PropertyShape.java index db324f3c020..9e53b665ff9 100644 --- a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/PropertyShape.java +++ b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/PropertyShape.java @@ -10,6 +10,7 @@ *******************************************************************************/ package org.eclipse.rdf4j.sail.shacl.ast; +import java.util.IdentityHashMap; import java.util.List; import java.util.Objects; import java.util.Set; @@ -344,14 +345,8 @@ public SourceConstraintComponent getConstraintComponent() { } @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - if (!super.equals(o)) { + public boolean equals(ConstraintComponent o, IdentityHashMap kvIdentityHashMap) { + if (!(o instanceof PropertyShape)) { return false; } @@ -369,12 +364,18 @@ public boolean equals(Object o) { if (!Objects.equals(group, that.group)) { return false; } - return Objects.equals(path, that.path); + + if (!Objects.equals(path, that.path)) { + return false; + } + + return super.equals(o, kvIdentityHashMap); + } @Override - public int hashCode() { - int result = super.hashCode(); + public int hashCode(IdentityHashMap cycleDetection) { + int result = super.hashCode(cycleDetection); result = 31 * result + (name != null ? name.hashCode() : 0); result = 31 * result + (description != null ? description.hashCode() : 0); result = 31 * result + (defaultValue != null ? defaultValue.hashCode() : 0); diff --git a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/ShaclShapeParsingException.java b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/ShaclShapeParsingException.java index 2b867ef5a63..09d9270b827 100644 --- a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/ShaclShapeParsingException.java +++ b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/ShaclShapeParsingException.java @@ -32,6 +32,10 @@ public ShaclShapeParsingException(String msg, Throwable t, Resource id) { this.id = id; } + public ShaclShapeParsingException(String msg, Throwable t) { + super(msg, t); + } + public ShaclShapeParsingException(Throwable t, Resource id) { super(t.getMessage() + " - Caused by shape with id: " + resourceToString(id), t); this.id = id; diff --git a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/Shape.java b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/Shape.java index 4c3d7db0bb4..45702900b82 100644 --- a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/Shape.java +++ b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/Shape.java @@ -14,6 +14,7 @@ import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; +import java.util.IdentityHashMap; import java.util.List; import java.util.Objects; import java.util.Set; @@ -21,6 +22,7 @@ import java.util.stream.Stream; import org.eclipse.rdf4j.common.annotation.InternalUseOnly; +import org.eclipse.rdf4j.common.exception.RDF4JException; import org.eclipse.rdf4j.model.IRI; import org.eclipse.rdf4j.model.Literal; import org.eclipse.rdf4j.model.Model; @@ -43,6 +45,7 @@ import org.eclipse.rdf4j.sail.shacl.SourceConstraintComponent; import org.eclipse.rdf4j.sail.shacl.ValidationSettings; import org.eclipse.rdf4j.sail.shacl.ast.StatementMatcher.Variable; +import org.eclipse.rdf4j.sail.shacl.ast.constraintcomponents.AbstractConstraintComponent; import org.eclipse.rdf4j.sail.shacl.ast.constraintcomponents.AndConstraintComponent; import org.eclipse.rdf4j.sail.shacl.ast.constraintcomponents.ClassConstraintComponent; import org.eclipse.rdf4j.sail.shacl.ast.constraintcomponents.ClosedConstraintComponent; @@ -201,7 +204,7 @@ public void toModel(Resource subject, IRI predicate, Model model, Set modelBuilder.subject(getId()); if (deactivated) { - modelBuilder.add(SHACL.DEACTIVATED, deactivated); + modelBuilder.add(SHACL.DEACTIVATED, true); } for (Literal literal : message) { @@ -513,6 +516,13 @@ public final List getMessage() { return message; } + /** + * @return any explicit sh:message values defined on this shape, without falling back to constraint defaults. + */ + public final List getExplicitMessages() { + return message; + } + @Override public List getDefaultMessage() { return constraintComponents @@ -523,7 +533,7 @@ public List getDefaultMessage() { } @Override - public boolean equals(Object o) { + public final boolean equals(Object o) { if (this == o) { return true; } @@ -531,8 +541,24 @@ public boolean equals(Object o) { return false; } + return equals((Shape) o, new IdentityHashMap<>()); + } + + @Override + public boolean equals(ConstraintComponent o, IdentityHashMap comparisonGuard) { + Object alreadyComparing = comparisonGuard.get(this); + if (alreadyComparing == o) { + return true; + } + + if (!(o instanceof Shape)) { + return false; + } + Shape shape = (Shape) o; + comparisonGuard.put(this, shape); + if (produceValidationReports != shape.produceValidationReports) { return false; } @@ -548,20 +574,87 @@ public boolean equals(Object o) { if (severity != shape.severity) { return false; } - if (!Objects.equals(constraintComponents, shape.constraintComponents)) { + + if (constraintComponents.size() != shape.constraintComponents.size()) { return false; } + + boolean[] matchedRightSide = new boolean[shape.constraintComponents.size()]; + + for (int i = 0; i < constraintComponents.size(); i++) { + ConstraintComponent left = constraintComponents.get(i); + + if (left == null) { + continue; + } + + int matchIndex = -1; + + if (!matchedRightSide[i]) { + ConstraintComponent right = shape.constraintComponents.get(i); + if (left.equals(right, comparisonGuard)) { + matchIndex = i; + } + } + + if (matchIndex == -1) { + for (int j = 0; j < shape.constraintComponents.size(); j++) { + if (matchedRightSide[j]) { + continue; + } + ConstraintComponent candidate = shape.constraintComponents.get(j); + if (left.equals(candidate, comparisonGuard)) { + matchIndex = j; + break; + } + } + } + + if (matchIndex == -1) { + return false; + } + + matchedRightSide[matchIndex] = true; + } + return true; } @Override - public int hashCode() { + public final int hashCode() { + return hashCode(new IdentityHashMap<>()); + } + + @Override + public int hashCode(IdentityHashMap cycleDetection) { + if (cycleDetection.put(this, Boolean.TRUE) != null) { + throw new ShaclShapeParsingException("Recursive shape definition detected while computing hashCode", + getId()); + } + int result = (produceValidationReports ? 1 : 0); result = 31 * result + (target != null ? target.hashCode() : 0); result = 31 * result + (deactivated ? 1 : 0); result = 31 * result + (message != null ? message.hashCode() : 0); result = 31 * result + (severity != null ? severity.hashCode() : 0); - result = 31 * result + (constraintComponents != null ? constraintComponents.hashCode() : 0); + + long temp = 0; + + for (ConstraintComponent constraintComponent : constraintComponents) { + int componentHash; + if (constraintComponent instanceof Shape) { + componentHash = constraintComponent.hashCode(cycleDetection); + } else if (constraintComponent instanceof AbstractConstraintComponent) { + componentHash = constraintComponent.hashCode(cycleDetection); + } else { + componentHash = constraintComponent != null ? constraintComponent.hashCode() : 0; + } + temp += componentHash; + } + + result = 31 * result + Long.hashCode(temp); + + cycleDetection.remove(this); return result; } @@ -569,26 +662,42 @@ public static class Factory { public static List getShapes(ShapeSource shapeSource, ParseSettings parseSettings) { - List parsed = parse(shapeSource, parseSettings); - - return getShapes(parsed); + try { + List parsed = parse(shapeSource, parseSettings); + return getShapes(parsed); + } catch (RDF4JException e) { + logger.error(e.getMessage(), e); + throw e; + } catch (Throwable e) { + logger.error("Unexpected error while parsing shapes", e); + throw new ShaclShapeParsingException("Unexpected error while parsing shapes", e); + } } public static List getShapes(List parsed) { - return parsed.stream() - .flatMap(contextWithShapes -> { - List split = split(contextWithShapes.getShape()); - calculateTargetChain(split); - calculateIfProducesValidationResult(split); - return split.stream().map(s -> { - return new ContextWithShape(contextWithShapes.getDataGraph(), - contextWithShapes.getShapeGraph(), s); - }); - }) - .filter(ContextWithShape::hasShape) - .distinct() - .collect(Collectors.toList()); + try { + return parsed.stream() + .flatMap(contextWithShapes -> { + List split = split(contextWithShapes.getShape()); + calculateTargetChain(split); + calculateIfProducesValidationResult(split); + return split.stream().map(s -> { + return new ContextWithShape(contextWithShapes.getDataGraph(), + contextWithShapes.getShapeGraph(), s); + }); + }) + .filter(ContextWithShape::hasShape) + .distinct() + .peek(a -> a.getShape().verifyNotRecursive()) + .collect(Collectors.toList()); + } catch (RDF4JException e) { + logger.error(e.getMessage(), e); + throw e; + } catch (Throwable e) { + logger.error("Unexpected error while parsing shapes", e); + throw new ShaclShapeParsingException("Unexpected error while parsing shapes", e); + } } private static void calculateIfProducesValidationResult(List split) { @@ -750,6 +859,11 @@ public static List getShapesInContext(ShapeSource shapeSource, } + private void verifyNotRecursive() { + // calling hashCode will throw an exception if recursion is detected + int i = hashCode(new IdentityHashMap<>()); + } + @Override public String toString() { Model statements = toModel(new DynamicModel(new LinkedHashModelFactory())); diff --git a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/constraintcomponents/AbstractConstraintComponent.java b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/constraintcomponents/AbstractConstraintComponent.java index fedfe846162..b6de74b1f30 100644 --- a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/constraintcomponents/AbstractConstraintComponent.java +++ b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/constraintcomponents/AbstractConstraintComponent.java @@ -11,6 +11,8 @@ package org.eclipse.rdf4j.sail.shacl.ast.constraintcomponents; +import java.util.IdentityHashMap; + import org.eclipse.rdf4j.model.IRI; import org.eclipse.rdf4j.model.Literal; import org.eclipse.rdf4j.model.Resource; @@ -178,4 +180,21 @@ static PlanNode getAllTargetsIncludingThoseAddedByPath(ConnectionsGroup connecti return allTargets; } + @Override + public final int hashCode() { + return hashCode(new IdentityHashMap<>()); + } + + @Override + public final boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + return equals(((AbstractConstraintComponent) o), new IdentityHashMap<>()); + } + } diff --git a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/constraintcomponents/AbstractPairwiseConstraintComponent.java b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/constraintcomponents/AbstractPairwiseConstraintComponent.java index daba61bd6c0..eb43ec44531 100644 --- a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/constraintcomponents/AbstractPairwiseConstraintComponent.java +++ b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/constraintcomponents/AbstractPairwiseConstraintComponent.java @@ -11,6 +11,7 @@ package org.eclipse.rdf4j.sail.shacl.ast.constraintcomponents; +import java.util.IdentityHashMap; import java.util.Optional; import java.util.Set; @@ -268,7 +269,7 @@ public boolean requiresEvaluation(ConnectionsGroup connectionsGroup, Scope scope } @Override - public boolean equals(Object o) { + public boolean equals(ConstraintComponent o, IdentityHashMap kvIdentityHashMap) { if (this == o) { return true; } @@ -282,7 +283,7 @@ public boolean equals(Object o) { } @Override - public int hashCode() { + public int hashCode(IdentityHashMap identityHashMap) { return predicate.hashCode() + "AbstractPairwiseConstraintComponent".hashCode(); } diff --git a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/constraintcomponents/AndConstraintComponent.java b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/constraintcomponents/AndConstraintComponent.java index bfc71a8e135..3326c1cc1d3 100644 --- a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/constraintcomponents/AndConstraintComponent.java +++ b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/constraintcomponents/AndConstraintComponent.java @@ -11,6 +11,7 @@ package org.eclipse.rdf4j.sail.shacl.ast.constraintcomponents; +import java.util.IdentityHashMap; import java.util.List; import java.util.Set; import java.util.stream.Collectors; @@ -176,21 +177,31 @@ public List getDefaultMessage() { } @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { + public boolean equals(ConstraintComponent o, IdentityHashMap guard) { + if (!(o instanceof AndConstraintComponent)) { return false; } AndConstraintComponent that = (AndConstraintComponent) o; - return and.equals(that.and); + if (and.size() != that.and.size()) { + return false; + } + + for (int i = 0; i < and.size(); i++) { + if (!and.get(i).equals(that.and.get(i), guard)) { + return false; + } + } + return true; } @Override - public int hashCode() { - return and.hashCode() + "AndConstraintComponent".hashCode(); + public int hashCode(IdentityHashMap guard) { + int result = "AndConstraintComponent".hashCode(); + for (Shape shape : and) { + result = 31 * result + shape.hashCode(guard); + } + return result; } } diff --git a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/constraintcomponents/ClassConstraintComponent.java b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/constraintcomponents/ClassConstraintComponent.java index 935c1cf3727..a2aee5506cb 100644 --- a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/constraintcomponents/ClassConstraintComponent.java +++ b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/constraintcomponents/ClassConstraintComponent.java @@ -11,6 +11,7 @@ package org.eclipse.rdf4j.sail.shacl.ast.constraintcomponents; +import java.util.IdentityHashMap; import java.util.List; import java.util.Set; @@ -23,6 +24,7 @@ import org.eclipse.rdf4j.model.vocabulary.SHACL; import org.eclipse.rdf4j.sail.shacl.SourceConstraintComponent; import org.eclipse.rdf4j.sail.shacl.ValidationSettings; +import org.eclipse.rdf4j.sail.shacl.ast.Shape; import org.eclipse.rdf4j.sail.shacl.ast.SparqlFragment; import org.eclipse.rdf4j.sail.shacl.ast.StatementMatcher; import org.eclipse.rdf4j.sail.shacl.ast.StatementMatcher.Variable; @@ -403,7 +405,7 @@ public List getDefaultMessage() { } @Override - public boolean equals(Object o) { + public boolean equals(ConstraintComponent o, IdentityHashMap kvIdentityHashMap) { if (this == o) { return true; } @@ -417,7 +419,7 @@ public boolean equals(Object o) { } @Override - public int hashCode() { + public int hashCode(IdentityHashMap identityHashMap) { return clazz.hashCode() + "ClassConstraintComponent".hashCode(); } } diff --git a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/constraintcomponents/ClosedConstraintComponent.java b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/constraintcomponents/ClosedConstraintComponent.java index a12cc650c84..025e4fe021f 100644 --- a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/constraintcomponents/ClosedConstraintComponent.java +++ b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/constraintcomponents/ClosedConstraintComponent.java @@ -15,6 +15,7 @@ import java.util.Collections; import java.util.HashSet; +import java.util.IdentityHashMap; import java.util.List; import java.util.Objects; import java.util.Set; @@ -556,7 +557,7 @@ public List getDefaultMessage() { } @Override - public boolean equals(Object o) { + public boolean equals(ConstraintComponent o, IdentityHashMap kvIdentityHashMap) { if (this == o) { return true; } @@ -573,7 +574,7 @@ public boolean equals(Object o) { } @Override - public int hashCode() { + public int hashCode(IdentityHashMap identityHashMap) { int result = paths != null ? paths.hashCode() : 0; result = 31 * result + (ignoredProperties != null ? ignoredProperties.hashCode() : 0); return result; diff --git a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/constraintcomponents/ConstraintComponent.java b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/constraintcomponents/ConstraintComponent.java index e051b20057c..384d91f165b 100644 --- a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/constraintcomponents/ConstraintComponent.java +++ b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/constraintcomponents/ConstraintComponent.java @@ -11,6 +11,7 @@ package org.eclipse.rdf4j.sail.shacl.ast.constraintcomponents; +import java.util.IdentityHashMap; import java.util.List; import org.eclipse.rdf4j.model.Literal; @@ -19,6 +20,7 @@ import org.eclipse.rdf4j.sail.shacl.SourceConstraintComponent; import org.eclipse.rdf4j.sail.shacl.ValidationSettings; import org.eclipse.rdf4j.sail.shacl.ast.Exportable; +import org.eclipse.rdf4j.sail.shacl.ast.Shape; import org.eclipse.rdf4j.sail.shacl.ast.SparqlFragment; import org.eclipse.rdf4j.sail.shacl.ast.StatementMatcher; import org.eclipse.rdf4j.sail.shacl.ast.StatementMatcher.Variable; @@ -74,6 +76,10 @@ SparqlFragment buildSparqlValidNodes_rsx_targetShape(Variable subject, List getDefaultMessage(); + boolean equals(ConstraintComponent o, IdentityHashMap kvIdentityHashMap); + + int hashCode(IdentityHashMap identityHashMap); + enum Scope { none, nodeShape, diff --git a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/constraintcomponents/DashHasValueInConstraintComponent.java b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/constraintcomponents/DashHasValueInConstraintComponent.java index 2839d3bcd71..2324f71d007 100644 --- a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/constraintcomponents/DashHasValueInConstraintComponent.java +++ b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/constraintcomponents/DashHasValueInConstraintComponent.java @@ -13,6 +13,7 @@ import java.util.ArrayList; import java.util.Collections; +import java.util.IdentityHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Set; @@ -27,6 +28,7 @@ import org.eclipse.rdf4j.sail.shacl.SourceConstraintComponent; import org.eclipse.rdf4j.sail.shacl.ValidationSettings; import org.eclipse.rdf4j.sail.shacl.ast.ShaclAstLists; +import org.eclipse.rdf4j.sail.shacl.ast.Shape; import org.eclipse.rdf4j.sail.shacl.ast.SparqlFragment; import org.eclipse.rdf4j.sail.shacl.ast.StatementMatcher; import org.eclipse.rdf4j.sail.shacl.ast.StatementMatcher.Variable; @@ -224,7 +226,7 @@ public List getDefaultMessage() { } @Override - public boolean equals(Object o) { + public boolean equals(ConstraintComponent o, IdentityHashMap kvIdentityHashMap) { if (this == o) { return true; } @@ -238,7 +240,7 @@ public boolean equals(Object o) { } @Override - public int hashCode() { + public int hashCode(IdentityHashMap identityHashMap) { return hasValueIn.hashCode() + "DashHasValueInConstraintComponent".hashCode(); } } diff --git a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/constraintcomponents/DatatypeConstraintComponent.java b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/constraintcomponents/DatatypeConstraintComponent.java index b2571243577..62c8447320a 100644 --- a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/constraintcomponents/DatatypeConstraintComponent.java +++ b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/constraintcomponents/DatatypeConstraintComponent.java @@ -11,6 +11,7 @@ package org.eclipse.rdf4j.sail.shacl.ast.constraintcomponents; +import java.util.IdentityHashMap; import java.util.List; import java.util.Set; import java.util.function.Function; @@ -24,6 +25,7 @@ import org.eclipse.rdf4j.model.vocabulary.RSX; import org.eclipse.rdf4j.model.vocabulary.SHACL; import org.eclipse.rdf4j.sail.shacl.SourceConstraintComponent; +import org.eclipse.rdf4j.sail.shacl.ast.Shape; import org.eclipse.rdf4j.sail.shacl.ast.StatementMatcher.Variable; import org.eclipse.rdf4j.sail.shacl.ast.planNodes.DatatypeFilter; import org.eclipse.rdf4j.sail.shacl.ast.planNodes.FilterPlanNode; @@ -94,7 +96,7 @@ public List getDefaultMessage() { } @Override - public boolean equals(Object o) { + public boolean equals(ConstraintComponent o, IdentityHashMap kvIdentityHashMap) { if (this == o) { return true; } @@ -108,7 +110,7 @@ public boolean equals(Object o) { } @Override - public int hashCode() { + public int hashCode(IdentityHashMap identityHashMap) { return datatype.hashCode() + "DatatypeConstraintComponent".hashCode(); } } diff --git a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/constraintcomponents/DisjointConstraintComponent.java b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/constraintcomponents/DisjointConstraintComponent.java index ba9d6b2e03c..cf21ee80ed5 100644 --- a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/constraintcomponents/DisjointConstraintComponent.java +++ b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/constraintcomponents/DisjointConstraintComponent.java @@ -11,6 +11,7 @@ package org.eclipse.rdf4j.sail.shacl.ast.constraintcomponents; +import java.util.IdentityHashMap; import java.util.List; import org.eclipse.rdf4j.model.IRI; @@ -62,7 +63,7 @@ public List getDefaultMessage() { } @Override - public boolean equals(Object o) { + public boolean equals(ConstraintComponent o, IdentityHashMap kvIdentityHashMap) { if (this == o) { return true; } @@ -76,7 +77,7 @@ public boolean equals(Object o) { } @Override - public int hashCode() { + public int hashCode(IdentityHashMap identityHashMap) { return predicate.hashCode() + "DisjointConstraintComponent".hashCode(); } } diff --git a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/constraintcomponents/EqualsConstraintComponent.java b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/constraintcomponents/EqualsConstraintComponent.java index bf7ffedb77e..43cb2b0e5f6 100644 --- a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/constraintcomponents/EqualsConstraintComponent.java +++ b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/constraintcomponents/EqualsConstraintComponent.java @@ -11,6 +11,7 @@ package org.eclipse.rdf4j.sail.shacl.ast.constraintcomponents; +import java.util.IdentityHashMap; import java.util.List; import org.eclipse.rdf4j.model.IRI; @@ -62,7 +63,7 @@ public List getDefaultMessage() { } @Override - public boolean equals(Object o) { + public boolean equals(ConstraintComponent o, IdentityHashMap kvIdentityHashMap) { if (this == o) { return true; } @@ -76,7 +77,7 @@ public boolean equals(Object o) { } @Override - public int hashCode() { + public int hashCode(IdentityHashMap identityHashMap) { return predicate.hashCode() + "EqualsConstraintComponent".hashCode(); } } diff --git a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/constraintcomponents/HasValueConstraintComponent.java b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/constraintcomponents/HasValueConstraintComponent.java index a4f3d66b11c..11cfed73fcf 100644 --- a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/constraintcomponents/HasValueConstraintComponent.java +++ b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/constraintcomponents/HasValueConstraintComponent.java @@ -13,6 +13,7 @@ import java.util.Collections; import java.util.HashSet; +import java.util.IdentityHashMap; import java.util.List; import java.util.Set; @@ -24,6 +25,7 @@ import org.eclipse.rdf4j.model.vocabulary.SHACL; import org.eclipse.rdf4j.sail.shacl.SourceConstraintComponent; import org.eclipse.rdf4j.sail.shacl.ValidationSettings; +import org.eclipse.rdf4j.sail.shacl.ast.Shape; import org.eclipse.rdf4j.sail.shacl.ast.SparqlFragment; import org.eclipse.rdf4j.sail.shacl.ast.StatementMatcher; import org.eclipse.rdf4j.sail.shacl.ast.StatementMatcher.Variable; @@ -235,7 +237,7 @@ public List getDefaultMessage() { } @Override - public boolean equals(Object o) { + public boolean equals(ConstraintComponent o, IdentityHashMap kvIdentityHashMap) { if (this == o) { return true; } @@ -249,7 +251,7 @@ public boolean equals(Object o) { } @Override - public int hashCode() { + public int hashCode(IdentityHashMap identityHashMap) { return hasValue.hashCode() + "HasValueConstraintComponent".hashCode(); } } diff --git a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/constraintcomponents/InConstraintComponent.java b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/constraintcomponents/InConstraintComponent.java index df2b549f8ad..0932904aaa5 100644 --- a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/constraintcomponents/InConstraintComponent.java +++ b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/constraintcomponents/InConstraintComponent.java @@ -12,6 +12,7 @@ package org.eclipse.rdf4j.sail.shacl.ast.constraintcomponents; import java.util.Collections; +import java.util.IdentityHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Set; @@ -25,6 +26,7 @@ import org.eclipse.rdf4j.model.vocabulary.SHACL; import org.eclipse.rdf4j.sail.shacl.SourceConstraintComponent; import org.eclipse.rdf4j.sail.shacl.ast.ShaclAstLists; +import org.eclipse.rdf4j.sail.shacl.ast.Shape; import org.eclipse.rdf4j.sail.shacl.ast.StatementMatcher.Variable; import org.eclipse.rdf4j.sail.shacl.ast.planNodes.FilterPlanNode; import org.eclipse.rdf4j.sail.shacl.ast.planNodes.PlanNode; @@ -109,7 +111,7 @@ public List getDefaultMessage() { } @Override - public boolean equals(Object o) { + public boolean equals(ConstraintComponent o, IdentityHashMap kvIdentityHashMap) { if (this == o) { return true; } @@ -123,7 +125,7 @@ public boolean equals(Object o) { } @Override - public int hashCode() { + public int hashCode(IdentityHashMap identityHashMap) { return in.hashCode() + "InConstraintComponent".hashCode(); } } diff --git a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/constraintcomponents/LanguageInConstraintComponent.java b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/constraintcomponents/LanguageInConstraintComponent.java index a6542ae19aa..ba0e94365ef 100644 --- a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/constraintcomponents/LanguageInConstraintComponent.java +++ b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/constraintcomponents/LanguageInConstraintComponent.java @@ -14,6 +14,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; +import java.util.IdentityHashMap; import java.util.List; import java.util.Set; import java.util.function.Function; @@ -28,6 +29,7 @@ import org.eclipse.rdf4j.model.vocabulary.SHACL; import org.eclipse.rdf4j.sail.shacl.SourceConstraintComponent; import org.eclipse.rdf4j.sail.shacl.ast.ShaclAstLists; +import org.eclipse.rdf4j.sail.shacl.ast.Shape; import org.eclipse.rdf4j.sail.shacl.ast.StatementMatcher.Variable; import org.eclipse.rdf4j.sail.shacl.ast.planNodes.FilterPlanNode; import org.eclipse.rdf4j.sail.shacl.ast.planNodes.LanguageInFilter; @@ -123,7 +125,7 @@ public List getDefaultMessage() { } @Override - public boolean equals(Object o) { + public boolean equals(ConstraintComponent o, IdentityHashMap kvIdentityHashMap) { if (this == o) { return true; } @@ -137,7 +139,7 @@ public boolean equals(Object o) { } @Override - public int hashCode() { + public int hashCode(IdentityHashMap identityHashMap) { return languageIn.hashCode() + "LanguageInConstraintComponent".hashCode(); } } diff --git a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/constraintcomponents/LessThanConstraintComponent.java b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/constraintcomponents/LessThanConstraintComponent.java index 9dc72f88ced..81bf4afb87f 100644 --- a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/constraintcomponents/LessThanConstraintComponent.java +++ b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/constraintcomponents/LessThanConstraintComponent.java @@ -11,6 +11,7 @@ package org.eclipse.rdf4j.sail.shacl.ast.constraintcomponents; +import java.util.IdentityHashMap; import java.util.List; import org.eclipse.rdf4j.model.IRI; @@ -62,7 +63,7 @@ public List getDefaultMessage() { } @Override - public boolean equals(Object o) { + public boolean equals(ConstraintComponent o, IdentityHashMap kvIdentityHashMap) { if (this == o) { return true; } @@ -76,7 +77,7 @@ public boolean equals(Object o) { } @Override - public int hashCode() { + public int hashCode(IdentityHashMap identityHashMap) { return predicate.hashCode() + "LessThanConstraintComponent".hashCode(); } } diff --git a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/constraintcomponents/LessThanOrEqualsConstraintComponent.java b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/constraintcomponents/LessThanOrEqualsConstraintComponent.java index 8b5e39cfecc..7281218907e 100644 --- a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/constraintcomponents/LessThanOrEqualsConstraintComponent.java +++ b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/constraintcomponents/LessThanOrEqualsConstraintComponent.java @@ -11,6 +11,7 @@ package org.eclipse.rdf4j.sail.shacl.ast.constraintcomponents; +import java.util.IdentityHashMap; import java.util.List; import org.eclipse.rdf4j.model.IRI; @@ -62,7 +63,7 @@ public List getDefaultMessage() { } @Override - public boolean equals(Object o) { + public boolean equals(ConstraintComponent o, IdentityHashMap kvIdentityHashMap) { if (this == o) { return true; } @@ -76,7 +77,7 @@ public boolean equals(Object o) { } @Override - public int hashCode() { + public int hashCode(IdentityHashMap identityHashMap) { return predicate.hashCode() + "LessThanOrEqualsConstraintComponent".hashCode(); } } diff --git a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/constraintcomponents/MaxCountConstraintComponent.java b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/constraintcomponents/MaxCountConstraintComponent.java index f38c04ac44a..3e287815a6f 100644 --- a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/constraintcomponents/MaxCountConstraintComponent.java +++ b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/constraintcomponents/MaxCountConstraintComponent.java @@ -16,6 +16,7 @@ import java.math.BigInteger; import java.util.ArrayList; import java.util.HashSet; +import java.util.IdentityHashMap; import java.util.List; import java.util.Optional; import java.util.Set; @@ -27,6 +28,7 @@ import org.eclipse.rdf4j.model.vocabulary.SHACL; import org.eclipse.rdf4j.sail.shacl.SourceConstraintComponent; import org.eclipse.rdf4j.sail.shacl.ValidationSettings; +import org.eclipse.rdf4j.sail.shacl.ast.Shape; import org.eclipse.rdf4j.sail.shacl.ast.SparqlFragment; import org.eclipse.rdf4j.sail.shacl.ast.StatementMatcher; import org.eclipse.rdf4j.sail.shacl.ast.ValidationApproach; @@ -248,7 +250,7 @@ public List getDefaultMessage() { } @Override - public boolean equals(Object o) { + public boolean equals(ConstraintComponent o, IdentityHashMap kvIdentityHashMap) { if (this == o) { return true; } @@ -262,7 +264,7 @@ public boolean equals(Object o) { } @Override - public int hashCode() { + public int hashCode(IdentityHashMap identityHashMap) { return Long.hashCode(maxCount) + "MaxCountConstraintComponent".hashCode(); } } diff --git a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/constraintcomponents/MaxExclusiveConstraintComponent.java b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/constraintcomponents/MaxExclusiveConstraintComponent.java index 8f1ef0cd175..bc772caad8b 100644 --- a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/constraintcomponents/MaxExclusiveConstraintComponent.java +++ b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/constraintcomponents/MaxExclusiveConstraintComponent.java @@ -11,6 +11,7 @@ package org.eclipse.rdf4j.sail.shacl.ast.constraintcomponents; +import java.util.IdentityHashMap; import java.util.List; import java.util.Set; import java.util.function.Function; @@ -23,6 +24,7 @@ import org.eclipse.rdf4j.model.vocabulary.SHACL; import org.eclipse.rdf4j.query.algebra.Compare; import org.eclipse.rdf4j.sail.shacl.SourceConstraintComponent; +import org.eclipse.rdf4j.sail.shacl.ast.Shape; import org.eclipse.rdf4j.sail.shacl.ast.StatementMatcher.Variable; import org.eclipse.rdf4j.sail.shacl.ast.planNodes.FilterPlanNode; import org.eclipse.rdf4j.sail.shacl.ast.planNodes.LiteralComparatorFilter; @@ -73,7 +75,7 @@ public List getDefaultMessage() { } @Override - public boolean equals(Object o) { + public boolean equals(ConstraintComponent o, IdentityHashMap kvIdentityHashMap) { if (this == o) { return true; } @@ -87,7 +89,7 @@ public boolean equals(Object o) { } @Override - public int hashCode() { + public int hashCode(IdentityHashMap identityHashMap) { return maxExclusive.hashCode() + "MaxExclusiveConstraintComponent".hashCode(); } } diff --git a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/constraintcomponents/MaxInclusiveConstraintComponent.java b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/constraintcomponents/MaxInclusiveConstraintComponent.java index 2713f231748..44c39f7efeb 100644 --- a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/constraintcomponents/MaxInclusiveConstraintComponent.java +++ b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/constraintcomponents/MaxInclusiveConstraintComponent.java @@ -11,6 +11,7 @@ package org.eclipse.rdf4j.sail.shacl.ast.constraintcomponents; +import java.util.IdentityHashMap; import java.util.List; import java.util.Set; import java.util.function.Function; @@ -23,6 +24,7 @@ import org.eclipse.rdf4j.model.vocabulary.SHACL; import org.eclipse.rdf4j.query.algebra.Compare; import org.eclipse.rdf4j.sail.shacl.SourceConstraintComponent; +import org.eclipse.rdf4j.sail.shacl.ast.Shape; import org.eclipse.rdf4j.sail.shacl.ast.StatementMatcher.Variable; import org.eclipse.rdf4j.sail.shacl.ast.planNodes.FilterPlanNode; import org.eclipse.rdf4j.sail.shacl.ast.planNodes.LiteralComparatorFilter; @@ -73,7 +75,7 @@ public List getDefaultMessage() { } @Override - public boolean equals(Object o) { + public boolean equals(ConstraintComponent o, IdentityHashMap kvIdentityHashMap) { if (this == o) { return true; } @@ -87,7 +89,7 @@ public boolean equals(Object o) { } @Override - public int hashCode() { + public int hashCode(IdentityHashMap identityHashMap) { return maxInclusive.hashCode() + "MaxInclusiveConstraintComponent".hashCode(); } } diff --git a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/constraintcomponents/MaxLengthConstraintComponent.java b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/constraintcomponents/MaxLengthConstraintComponent.java index 63f3cd979d6..3d34becfa01 100644 --- a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/constraintcomponents/MaxLengthConstraintComponent.java +++ b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/constraintcomponents/MaxLengthConstraintComponent.java @@ -14,6 +14,7 @@ import static org.eclipse.rdf4j.model.util.Values.literal; import java.math.BigInteger; +import java.util.IdentityHashMap; import java.util.List; import java.util.Set; import java.util.function.Function; @@ -25,6 +26,7 @@ import org.eclipse.rdf4j.model.Value; import org.eclipse.rdf4j.model.vocabulary.SHACL; import org.eclipse.rdf4j.sail.shacl.SourceConstraintComponent; +import org.eclipse.rdf4j.sail.shacl.ast.Shape; import org.eclipse.rdf4j.sail.shacl.ast.StatementMatcher.Variable; import org.eclipse.rdf4j.sail.shacl.ast.planNodes.FilterPlanNode; import org.eclipse.rdf4j.sail.shacl.ast.planNodes.MaxLengthFilter; @@ -76,7 +78,7 @@ public List getDefaultMessage() { } @Override - public boolean equals(Object o) { + public boolean equals(ConstraintComponent o, IdentityHashMap kvIdentityHashMap) { if (this == o) { return true; } @@ -90,7 +92,7 @@ public boolean equals(Object o) { } @Override - public int hashCode() { + public int hashCode(IdentityHashMap identityHashMap) { return Long.hashCode(maxLength) + "MaxLengthConstraintComponent".hashCode(); } } diff --git a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/constraintcomponents/MinCountConstraintComponent.java b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/constraintcomponents/MinCountConstraintComponent.java index 7f4f4af9ae6..c8c1e6e7dd6 100644 --- a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/constraintcomponents/MinCountConstraintComponent.java +++ b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/constraintcomponents/MinCountConstraintComponent.java @@ -16,6 +16,7 @@ import java.math.BigInteger; import java.util.ArrayList; import java.util.HashSet; +import java.util.IdentityHashMap; import java.util.List; import java.util.Set; @@ -26,6 +27,7 @@ import org.eclipse.rdf4j.model.vocabulary.SHACL; import org.eclipse.rdf4j.sail.shacl.SourceConstraintComponent; import org.eclipse.rdf4j.sail.shacl.ValidationSettings; +import org.eclipse.rdf4j.sail.shacl.ast.Shape; import org.eclipse.rdf4j.sail.shacl.ast.StatementMatcher; import org.eclipse.rdf4j.sail.shacl.ast.ValidationApproach; import org.eclipse.rdf4j.sail.shacl.ast.ValidationQuery; @@ -223,7 +225,7 @@ public List getDefaultMessage() { } @Override - public boolean equals(Object o) { + public boolean equals(ConstraintComponent o, IdentityHashMap kvIdentityHashMap) { if (this == o) { return true; } @@ -237,7 +239,7 @@ public boolean equals(Object o) { } @Override - public int hashCode() { + public int hashCode(IdentityHashMap identityHashMap) { return Long.hashCode(minCount) + "MinCountConstraintComponent".hashCode(); } } diff --git a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/constraintcomponents/MinExclusiveConstraintComponent.java b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/constraintcomponents/MinExclusiveConstraintComponent.java index b8448811711..a9e994862fe 100644 --- a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/constraintcomponents/MinExclusiveConstraintComponent.java +++ b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/constraintcomponents/MinExclusiveConstraintComponent.java @@ -11,6 +11,7 @@ package org.eclipse.rdf4j.sail.shacl.ast.constraintcomponents; +import java.util.IdentityHashMap; import java.util.List; import java.util.Set; import java.util.function.Function; @@ -23,6 +24,7 @@ import org.eclipse.rdf4j.model.vocabulary.SHACL; import org.eclipse.rdf4j.query.algebra.Compare; import org.eclipse.rdf4j.sail.shacl.SourceConstraintComponent; +import org.eclipse.rdf4j.sail.shacl.ast.Shape; import org.eclipse.rdf4j.sail.shacl.ast.StatementMatcher.Variable; import org.eclipse.rdf4j.sail.shacl.ast.planNodes.FilterPlanNode; import org.eclipse.rdf4j.sail.shacl.ast.planNodes.LiteralComparatorFilter; @@ -73,7 +75,7 @@ public List getDefaultMessage() { } @Override - public boolean equals(Object o) { + public boolean equals(ConstraintComponent o, IdentityHashMap kvIdentityHashMap) { if (this == o) { return true; } @@ -87,7 +89,7 @@ public boolean equals(Object o) { } @Override - public int hashCode() { + public int hashCode(IdentityHashMap identityHashMap) { return minExclusive.hashCode() + "MinExclusiveConstraintComponent".hashCode(); } } diff --git a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/constraintcomponents/MinInclusiveConstraintComponent.java b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/constraintcomponents/MinInclusiveConstraintComponent.java index d9654cc37b8..8777e0b855b 100644 --- a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/constraintcomponents/MinInclusiveConstraintComponent.java +++ b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/constraintcomponents/MinInclusiveConstraintComponent.java @@ -11,6 +11,7 @@ package org.eclipse.rdf4j.sail.shacl.ast.constraintcomponents; +import java.util.IdentityHashMap; import java.util.List; import java.util.Set; import java.util.function.Function; @@ -23,6 +24,7 @@ import org.eclipse.rdf4j.model.vocabulary.SHACL; import org.eclipse.rdf4j.query.algebra.Compare; import org.eclipse.rdf4j.sail.shacl.SourceConstraintComponent; +import org.eclipse.rdf4j.sail.shacl.ast.Shape; import org.eclipse.rdf4j.sail.shacl.ast.StatementMatcher.Variable; import org.eclipse.rdf4j.sail.shacl.ast.planNodes.FilterPlanNode; import org.eclipse.rdf4j.sail.shacl.ast.planNodes.LiteralComparatorFilter; @@ -73,7 +75,7 @@ public List getDefaultMessage() { } @Override - public boolean equals(Object o) { + public boolean equals(ConstraintComponent o, IdentityHashMap kvIdentityHashMap) { if (this == o) { return true; } @@ -87,7 +89,7 @@ public boolean equals(Object o) { } @Override - public int hashCode() { + public int hashCode(IdentityHashMap identityHashMap) { return minInclusive.hashCode() + "MinInclusiveConstraintComponent".hashCode(); } } diff --git a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/constraintcomponents/MinLengthConstraintComponent.java b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/constraintcomponents/MinLengthConstraintComponent.java index 792cb8aaf45..086c939bc83 100644 --- a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/constraintcomponents/MinLengthConstraintComponent.java +++ b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/constraintcomponents/MinLengthConstraintComponent.java @@ -14,6 +14,7 @@ import static org.eclipse.rdf4j.model.util.Values.literal; import java.math.BigInteger; +import java.util.IdentityHashMap; import java.util.List; import java.util.Set; import java.util.function.Function; @@ -25,6 +26,7 @@ import org.eclipse.rdf4j.model.Value; import org.eclipse.rdf4j.model.vocabulary.SHACL; import org.eclipse.rdf4j.sail.shacl.SourceConstraintComponent; +import org.eclipse.rdf4j.sail.shacl.ast.Shape; import org.eclipse.rdf4j.sail.shacl.ast.StatementMatcher.Variable; import org.eclipse.rdf4j.sail.shacl.ast.planNodes.FilterPlanNode; import org.eclipse.rdf4j.sail.shacl.ast.planNodes.MinLengthFilter; @@ -76,7 +78,7 @@ public List getDefaultMessage() { } @Override - public boolean equals(Object o) { + public boolean equals(ConstraintComponent o, IdentityHashMap kvIdentityHashMap) { if (this == o) { return true; } @@ -90,7 +92,7 @@ public boolean equals(Object o) { } @Override - public int hashCode() { + public int hashCode(IdentityHashMap identityHashMap) { return Long.hashCode(minLength) + "MinLengthConstraintComponent".hashCode(); } } diff --git a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/constraintcomponents/NodeKindConstraintComponent.java b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/constraintcomponents/NodeKindConstraintComponent.java index e165851bb5b..0836dcd0a7e 100644 --- a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/constraintcomponents/NodeKindConstraintComponent.java +++ b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/constraintcomponents/NodeKindConstraintComponent.java @@ -11,6 +11,7 @@ package org.eclipse.rdf4j.sail.shacl.ast.constraintcomponents; +import java.util.IdentityHashMap; import java.util.List; import java.util.Set; import java.util.function.Function; @@ -22,6 +23,7 @@ import org.eclipse.rdf4j.model.Value; import org.eclipse.rdf4j.model.vocabulary.SHACL; import org.eclipse.rdf4j.sail.shacl.SourceConstraintComponent; +import org.eclipse.rdf4j.sail.shacl.ast.Shape; import org.eclipse.rdf4j.sail.shacl.ast.StatementMatcher.Variable; import org.eclipse.rdf4j.sail.shacl.ast.planNodes.FilterPlanNode; import org.eclipse.rdf4j.sail.shacl.ast.planNodes.NodeKindFilter; @@ -113,7 +115,7 @@ public List getDefaultMessage() { } @Override - public boolean equals(Object o) { + public boolean equals(ConstraintComponent o, IdentityHashMap kvIdentityHashMap) { if (this == o) { return true; } @@ -127,7 +129,7 @@ public boolean equals(Object o) { } @Override - public int hashCode() { + public int hashCode(IdentityHashMap identityHashMap) { return nodeKind.hashCode() + "NodeKindConstraintComponent".hashCode(); } } diff --git a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/constraintcomponents/NotConstraintComponent.java b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/constraintcomponents/NotConstraintComponent.java index 40f793308ac..42a9ded6b42 100644 --- a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/constraintcomponents/NotConstraintComponent.java +++ b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/constraintcomponents/NotConstraintComponent.java @@ -11,6 +11,7 @@ package org.eclipse.rdf4j.sail.shacl.ast.constraintcomponents; +import java.util.IdentityHashMap; import java.util.List; import java.util.Set; @@ -212,21 +213,15 @@ public List getDefaultMessage() { } @Override - public boolean equals(Object o) { - if (this == o) { - return true; + public boolean equals(ConstraintComponent that, IdentityHashMap guard) { + if (that instanceof NotConstraintComponent) { + return not.equals(((NotConstraintComponent) that).not, guard); } - if (o == null || getClass() != o.getClass()) { - return false; - } - - NotConstraintComponent that = (NotConstraintComponent) o; - - return not.equals(that.not); + return false; } @Override - public int hashCode() { - return not.hashCode() + "NotConstraintComponent".hashCode(); + public int hashCode(IdentityHashMap guard) { + return not.hashCode(guard) + "NotConstraintComponent".hashCode(); } } diff --git a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/constraintcomponents/OrConstraintComponent.java b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/constraintcomponents/OrConstraintComponent.java index 393e7cbd9a4..d132b217b5d 100644 --- a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/constraintcomponents/OrConstraintComponent.java +++ b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/constraintcomponents/OrConstraintComponent.java @@ -12,6 +12,7 @@ package org.eclipse.rdf4j.sail.shacl.ast.constraintcomponents; import java.util.Collections; +import java.util.IdentityHashMap; import java.util.List; import java.util.Set; import java.util.stream.Collectors; @@ -213,21 +214,29 @@ public List getDefaultMessage() { } @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { + public boolean equals(ConstraintComponent o, IdentityHashMap guard) { + if (!(o instanceof OrConstraintComponent)) { return false; } - OrConstraintComponent that = (OrConstraintComponent) o; - return or.equals(that.or); + if (or.size() != that.or.size()) { + return false; + } + for (int i = 0; i < or.size(); i++) { + if (!or.get(i).equals(that.or.get(i), guard)) { + return false; + } + } + return true; } @Override - public int hashCode() { - return or.hashCode() + "OrConstraintComponent".hashCode(); + public int hashCode(IdentityHashMap guard) { + int result = "OrConstraintComponent".hashCode(); + for (Shape shape : or) { + result = 31 * result + shape.hashCode(guard); + } + return result; } } diff --git a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/constraintcomponents/PatternConstraintComponent.java b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/constraintcomponents/PatternConstraintComponent.java index 8536421133f..a6989d55a70 100644 --- a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/constraintcomponents/PatternConstraintComponent.java +++ b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/constraintcomponents/PatternConstraintComponent.java @@ -13,6 +13,7 @@ import static org.eclipse.rdf4j.model.util.Values.literal; +import java.util.IdentityHashMap; import java.util.List; import java.util.Objects; import java.util.Set; @@ -26,6 +27,7 @@ import org.eclipse.rdf4j.model.Value; import org.eclipse.rdf4j.model.vocabulary.SHACL; import org.eclipse.rdf4j.sail.shacl.SourceConstraintComponent; +import org.eclipse.rdf4j.sail.shacl.ast.Shape; import org.eclipse.rdf4j.sail.shacl.ast.StatementMatcher.Variable; import org.eclipse.rdf4j.sail.shacl.ast.planNodes.FilterPlanNode; import org.eclipse.rdf4j.sail.shacl.ast.planNodes.PatternFilter; @@ -148,7 +150,7 @@ public List getDefaultMessage() { } @Override - public boolean equals(Object o) { + public boolean equals(ConstraintComponent o, IdentityHashMap kvIdentityHashMap) { if (this == o) { return true; } @@ -165,7 +167,7 @@ public boolean equals(Object o) { } @Override - public int hashCode() { + public int hashCode(IdentityHashMap identityHashMap) { int result = pattern.hashCode(); result = 31 * result + (flags != null ? flags.hashCode() : 0); return result + "PatternConstraintComponent".hashCode(); diff --git a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/constraintcomponents/QualifiedMaxCountConstraintComponent.java b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/constraintcomponents/QualifiedMaxCountConstraintComponent.java index 63b660bf6f5..a3b50d55b8f 100644 --- a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/constraintcomponents/QualifiedMaxCountConstraintComponent.java +++ b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/constraintcomponents/QualifiedMaxCountConstraintComponent.java @@ -13,6 +13,7 @@ import static org.eclipse.rdf4j.model.util.Values.literal; import java.math.BigInteger; +import java.util.IdentityHashMap; import java.util.List; import java.util.Objects; import java.util.Set; @@ -288,11 +289,8 @@ public List getDefaultMessage() { } @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { + public boolean equals(ConstraintComponent o, IdentityHashMap guard) { + if (!(o instanceof QualifiedMaxCountConstraintComponent)) { return false; } @@ -301,15 +299,15 @@ public boolean equals(Object o) { if (qualifiedValueShapesDisjoint != that.qualifiedValueShapesDisjoint) { return false; } - if (!qualifiedValueShape.equals(that.qualifiedValueShape)) { + if (!qualifiedValueShape.equals(that.qualifiedValueShape, guard)) { return false; } return Objects.equals(qualifiedMaxCount, that.qualifiedMaxCount); } @Override - public int hashCode() { - int result = qualifiedValueShape.hashCode(); + public int hashCode(IdentityHashMap guard) { + int result = qualifiedValueShape.hashCode(guard); result = 31 * result + (qualifiedValueShapesDisjoint ? 1 : 0); result = 31 * result + (qualifiedMaxCount != null ? qualifiedMaxCount.hashCode() : 0); return result + "QualifiedMaxCountConstraintComponent".hashCode(); diff --git a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/constraintcomponents/QualifiedMinCountConstraintComponent.java b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/constraintcomponents/QualifiedMinCountConstraintComponent.java index a3ad8f3a70e..b68b5ccbe0e 100644 --- a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/constraintcomponents/QualifiedMinCountConstraintComponent.java +++ b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/constraintcomponents/QualifiedMinCountConstraintComponent.java @@ -14,6 +14,7 @@ import static org.eclipse.rdf4j.model.util.Values.literal; import java.math.BigInteger; +import java.util.IdentityHashMap; import java.util.List; import java.util.Objects; import java.util.Set; @@ -287,11 +288,8 @@ public List getDefaultMessage() { } @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { + public boolean equals(ConstraintComponent o, IdentityHashMap guard) { + if (!(o instanceof QualifiedMinCountConstraintComponent)) { return false; } @@ -300,15 +298,15 @@ public boolean equals(Object o) { if (qualifiedValueShapesDisjoint != that.qualifiedValueShapesDisjoint) { return false; } - if (!qualifiedValueShape.equals(that.qualifiedValueShape)) { + if (!qualifiedValueShape.equals(that.qualifiedValueShape, guard)) { return false; } return Objects.equals(qualifiedMinCount, that.qualifiedMinCount); } @Override - public int hashCode() { - int result = qualifiedValueShape.hashCode(); + public int hashCode(IdentityHashMap guard) { + int result = qualifiedValueShape.hashCode(guard); result = 31 * result + (qualifiedValueShapesDisjoint ? 1 : 0); result = 31 * result + (qualifiedMinCount != null ? qualifiedMinCount.hashCode() : 0); return result + "QualifiedMinCountConstraintComponent".hashCode(); diff --git a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/constraintcomponents/SparqlConstraintComponent.java b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/constraintcomponents/SparqlConstraintComponent.java index 77af99c9376..37bf477514e 100644 --- a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/constraintcomponents/SparqlConstraintComponent.java +++ b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/constraintcomponents/SparqlConstraintComponent.java @@ -12,6 +12,7 @@ package org.eclipse.rdf4j.sail.shacl.ast.constraintcomponents; import java.util.ArrayList; +import java.util.IdentityHashMap; import java.util.List; import java.util.Objects; import java.util.Set; @@ -54,6 +55,7 @@ public class SparqlConstraintComponent extends AbstractConstraintComponent imple private Boolean deactivated; private final Set namespaces; private final Model prefixes; + private final boolean hasMessageTemplateVariables; public SparqlConstraintComponent(Resource id, ShapeSource shapeSource, Shape shape) { super(id); @@ -96,6 +98,8 @@ public SparqlConstraintComponent(Resource id, ShapeSource shapeSource, Shape sha }); } + hasMessageTemplateVariables = containsMessageTemplateVariables(message); + var shaclNamespaces = ShaclPrefixParser.extractNamespaces(id, shapeSource); prefixes = shaclNamespaces.getModel(); namespaces = shaclNamespaces.getNamespaces(); @@ -115,6 +119,21 @@ public SparqlConstraintComponent(Resource id, Shape shape, boolean produceValida this.deactivated = deactivated; this.prefixes = prefixes; this.namespaces = namespaces; + this.hasMessageTemplateVariables = containsMessageTemplateVariables(message); + } + + public boolean hasMessageTemplateVariables() { + return hasMessageTemplateVariables; + } + + private static boolean containsMessageTemplateVariables(List messages) { + for (Literal literal : messages) { + String label = literal.getLabel(); + if (label.contains("{?") || label.contains("{$")) { + return true; + } + } + return false; } @Override @@ -155,6 +174,10 @@ public PlanNode generateTransactionalValidationPlan(ConnectionsGroup connections Path path = getTargetChain().getPath().get(); String s = path.toSparqlPathString(); select = select.replace(" $PATH ", " " + s + " "); + if (select.contains("$PATH")) { + throw new IllegalStateException( + "Illegal use of $PATH in SPARQL constraint for property shape " + getId()); + } } PlanNode allTargets; @@ -254,7 +277,7 @@ public List getDefaultMessage() { } @Override - public boolean equals(Object o) { + public boolean equals(ConstraintComponent o, IdentityHashMap kvIdentityHashMap) { if (this == o) { return true; } @@ -280,7 +303,7 @@ public boolean equals(Object o) { } @Override - public int hashCode() { + public int hashCode(IdentityHashMap identityHashMap) { int result = (produceValidationReports ? 1 : 0); result = 31 * result + select.hashCode(); result = 31 * result + originalSelect.hashCode(); diff --git a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/constraintcomponents/UniqueLangConstraintComponent.java b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/constraintcomponents/UniqueLangConstraintComponent.java index 99858731510..1097cab12d2 100644 --- a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/constraintcomponents/UniqueLangConstraintComponent.java +++ b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/constraintcomponents/UniqueLangConstraintComponent.java @@ -11,6 +11,7 @@ package org.eclipse.rdf4j.sail.shacl.ast.constraintcomponents; +import java.util.IdentityHashMap; import java.util.List; import java.util.Optional; import java.util.Set; @@ -23,6 +24,7 @@ import org.eclipse.rdf4j.model.vocabulary.SHACL; import org.eclipse.rdf4j.sail.shacl.SourceConstraintComponent; import org.eclipse.rdf4j.sail.shacl.ValidationSettings; +import org.eclipse.rdf4j.sail.shacl.ast.Shape; import org.eclipse.rdf4j.sail.shacl.ast.StatementMatcher; import org.eclipse.rdf4j.sail.shacl.ast.ValidationApproach; import org.eclipse.rdf4j.sail.shacl.ast.ValidationQuery; @@ -225,12 +227,12 @@ public List getDefaultMessage() { } @Override - public int hashCode() { + public int hashCode(IdentityHashMap identityHashMap) { return "UniqueLangConstraintComponent".hashCode(); } @Override - public boolean equals(Object obj) { - return obj instanceof UniqueLangConstraintComponent; + public boolean equals(ConstraintComponent o, IdentityHashMap kvIdentityHashMap) { + return o instanceof UniqueLangConstraintComponent; } } diff --git a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/constraintcomponents/VoidConstraintComponent.java b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/constraintcomponents/VoidConstraintComponent.java index f658cf12b45..8d9c2a3aea4 100644 --- a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/constraintcomponents/VoidConstraintComponent.java +++ b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/constraintcomponents/VoidConstraintComponent.java @@ -11,6 +11,7 @@ package org.eclipse.rdf4j.sail.shacl.ast.constraintcomponents; +import java.util.IdentityHashMap; import java.util.List; import java.util.Set; @@ -20,6 +21,7 @@ import org.eclipse.rdf4j.model.Resource; import org.eclipse.rdf4j.sail.shacl.SourceConstraintComponent; import org.eclipse.rdf4j.sail.shacl.ValidationSettings; +import org.eclipse.rdf4j.sail.shacl.ast.Shape; import org.eclipse.rdf4j.sail.shacl.ast.StatementMatcher; import org.eclipse.rdf4j.sail.shacl.ast.ValidationApproach; import org.eclipse.rdf4j.sail.shacl.ast.planNodes.EmptyNode; @@ -81,7 +83,7 @@ public List getDefaultMessage() { } @Override - public boolean equals(Object o) { + public boolean equals(ConstraintComponent o, IdentityHashMap kvIdentityHashMap) { if (this == o) { return true; } @@ -89,11 +91,11 @@ public boolean equals(Object o) { return false; } - return super.equals(o); + return true; } @Override - public int hashCode() { - return super.hashCode(); + public int hashCode(IdentityHashMap identityHashMap) { + return this.getClass().hashCode(); } } diff --git a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/constraintcomponents/XoneConstraintComponent.java b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/constraintcomponents/XoneConstraintComponent.java index f7997c207b1..cb59eb471ea 100644 --- a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/constraintcomponents/XoneConstraintComponent.java +++ b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/constraintcomponents/XoneConstraintComponent.java @@ -11,6 +11,7 @@ package org.eclipse.rdf4j.sail.shacl.ast.constraintcomponents; +import java.util.IdentityHashMap; import java.util.List; import java.util.Set; import java.util.stream.Collectors; @@ -99,21 +100,30 @@ public List getDefaultMessage() { } @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { + public boolean equals(ConstraintComponent o, IdentityHashMap guard) { + if (!(o instanceof XoneConstraintComponent)) { return false; } XoneConstraintComponent that = (XoneConstraintComponent) o; - return xone.equals(that.xone); + if (xone.size() != that.xone.size()) { + return false; + } + for (int i = 0; i < xone.size(); i++) { + if (!xone.get(i).equals(that.xone.get(i), guard)) { + return false; + } + } + return true; } @Override - public int hashCode() { - return xone.hashCode() + "XoneConstraintComponent".hashCode(); + public int hashCode(IdentityHashMap guard) { + int result = "XoneConstraintComponent".hashCode(); + for (Shape shape : xone) { + result = 31 * result + shape.hashCode(guard); + } + return result; } } diff --git a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/planNodes/SparqlConstraintSelect.java b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/planNodes/SparqlConstraintSelect.java index 9b244b4bc10..f3485dae8ed 100644 --- a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/planNodes/SparqlConstraintSelect.java +++ b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/planNodes/SparqlConstraintSelect.java @@ -12,16 +12,23 @@ package org.eclipse.rdf4j.sail.shacl.ast.planNodes; import java.util.Arrays; +import java.util.List; import java.util.Objects; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; import org.eclipse.rdf4j.common.iteration.CloseableIteration; +import org.eclipse.rdf4j.model.Literal; import org.eclipse.rdf4j.model.Resource; import org.eclipse.rdf4j.model.Value; import org.eclipse.rdf4j.model.impl.BooleanLiteral; +import org.eclipse.rdf4j.model.util.Values; import org.eclipse.rdf4j.query.BindingSet; import org.eclipse.rdf4j.query.Dataset; import org.eclipse.rdf4j.query.MalformedQueryException; import org.eclipse.rdf4j.query.QueryLanguage; +import org.eclipse.rdf4j.query.impl.MapBindingSet; import org.eclipse.rdf4j.query.parser.ParsedQuery; import org.eclipse.rdf4j.query.parser.QueryParserFactory; import org.eclipse.rdf4j.query.parser.QueryParserRegistry; @@ -41,6 +48,7 @@ public class SparqlConstraintSelect implements PlanNode { private static final Logger logger = LoggerFactory.getLogger(SparqlConstraintSelect.class); + private static final Pattern MESSAGE_TEMPLATE_PATTERN = Pattern.compile("\\{[?$]([A-Za-z_][A-Za-z0-9_]*)\\}"); private final SailConnection connection; @@ -55,6 +63,9 @@ public class SparqlConstraintSelect implements PlanNode { private final Dataset dataset; private final ParsedQuery parsedQuery; private final boolean printed = false; + private final Value shapesGraphBinding; + private final Value currentShapeBinding; + private final Value pathForMessageBinding; private ValidationExecutionLogger validationExecutionLogger; public SparqlConstraintSelect(SailConnection connection, PlanNode targets, String query, @@ -72,6 +83,9 @@ public SparqlConstraintSelect(SailConnection connection, PlanNode targets, Strin this.variables = new String[] { "$this" }; this.scope = scope; this.dataset = PlanNodeHelper.asDefaultGraphDataset(dataGraph); + this.currentShapeBinding = shape != null ? shape.getId() : null; + this.shapesGraphBinding = determineShapesGraphBinding(shape); + this.pathForMessageBinding = determinePathForMessageBinding(constraintComponent, scope); QueryParserFactory queryParserFactory = QueryParserRegistry.getInstance() .get(QueryLanguage.SPARQL) @@ -86,6 +100,33 @@ public SparqlConstraintSelect(SailConnection connection, PlanNode targets, Strin } + private static Value determineShapesGraphBinding(Shape shape) { + if (shape == null) { + return null; + } + Resource[] contexts = shape.getContexts(); + if (contexts == null) { + return null; + } + for (Resource context : contexts) { + if (context != null && context.isIRI()) { + return context; + } + } + return null; + } + + private static Value determinePathForMessageBinding(SparqlConstraintComponent constraintComponent, + ConstraintComponent.Scope scope) { + if (constraintComponent == null || scope != ConstraintComponent.Scope.propertyShape) { + return null; + } + return constraintComponent.getTargetChain() + .getPath() + .map(p -> Values.literal(p.toSparqlPathString())) + .orElse(null); + } + @Override public CloseableIteration iterator() { return new LoggingCloseableIteration(this, validationExecutionLogger) { @@ -108,7 +149,14 @@ private void calculateNext() { if (results == null && targetIterator.hasNext()) { nextTarget = targetIterator.next(); - SingletonBindingSet bindings = new SingletonBindingSet("this", nextTarget.getActiveTarget()); + MapBindingSet bindings = new MapBindingSet(3); + bindings.setBinding("this", nextTarget.getActiveTarget()); + if (currentShapeBinding != null) { + bindings.setBinding("currentShape", currentShapeBinding); + } + if (shapesGraphBinding != null) { + bindings.setBinding("shapesGraph", shapesGraphBinding); + } results = connection.evaluate(parsedQuery.getTupleExpr(), dataset, bindings, true); } @@ -128,6 +176,32 @@ private void calculateNext() { Value currentValue = value1; Value path = bindingSet.getValue("path"); + List resultMessages = null; + + Value messageValue = bindingSet.getValue("message"); + if (messageValue != null) { + if (messageValue.isLiteral()) { + resultMessages = List.of((Literal) messageValue); + } else { + resultMessages = List.of(Values.literal(messageValue.stringValue())); + } + } else if (produceValidationReports) { + List templates = constraintComponent.getDefaultMessage(); + if (!templates.isEmpty()) { + if (constraintComponent.hasMessageTemplateVariables()) { + Value focusNode = nextTarget.getActiveTarget(); + resultMessages = templates.stream() + .map(t -> substituteMessageTemplate(t, bindingSet, focusNode, + shapesGraphBinding, currentShapeBinding, + pathForMessageBinding)) + .collect(Collectors.toList()); + } else { + resultMessages = List.copyOf(templates); + } + } + } + + final List finalResultMessages = resultMessages; if (scope == ConstraintComponent.Scope.nodeShape) { next = nextTarget.addValidationResult(t -> { @@ -137,7 +211,10 @@ private void calculateNext() { constraintComponent, shape.getSeverity(), ConstraintComponent.Scope.nodeShape, t.getContexts(), shape.getContexts()); - if (path != null) { + if (finalResultMessages != null) { + validationResult.setMessagesOverride(finalResultMessages); + } + if (path != null && path.isIRI()) { validationResult.setPathIri(path); } return validationResult; @@ -154,7 +231,10 @@ private void calculateNext() { constraintComponent, shape.getSeverity(), ConstraintComponent.Scope.propertyShape, t.getContexts(), shape.getContexts()); - if (path != null) { + if (finalResultMessages != null) { + validationResult.setMessagesOverride(finalResultMessages); + } + if (path != null && path.isIRI()) { validationResult.setPathIri(path); } return validationResult; @@ -225,6 +305,41 @@ public void getPlanAsGraphvizDot(StringBuilder stringBuilder) { } + private static Literal substituteMessageTemplate(Literal template, BindingSet bindingSet, Value focusNode, + Value shapesGraphBinding, Value currentShapeBinding, Value pathForMessageBinding) { + String label = template.getLabel(); + Matcher matcher = MESSAGE_TEMPLATE_PATTERN.matcher(label); + if (!matcher.find()) { + return template; + } + + matcher.reset(); + StringBuffer sb = new StringBuffer(); + while (matcher.find()) { + String varName = matcher.group(1); + Value value = bindingSet.getValue(varName); + if (value == null) { + if ("this".equals(varName)) { + value = focusNode; + } else if ("shapesGraph".equals(varName)) { + value = shapesGraphBinding; + } else if ("currentShape".equals(varName)) { + value = currentShapeBinding; + } else if ("PATH".equals(varName)) { + value = pathForMessageBinding; + } + } + String replacement = value != null ? value.stringValue() : matcher.group(0); + matcher.appendReplacement(sb, Matcher.quoteReplacement(replacement)); + } + matcher.appendTail(sb); + + if (template.getLanguage().isPresent()) { + return Values.literal(sb.toString(), template.getLanguage().get()); + } + return Values.literal(sb.toString(), template.getDatatype()); + } + @Override public String getId() { return System.identityHashCode(this) + ""; diff --git a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/results/ValidationReport.java b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/results/ValidationReport.java index 4dbf63d2442..412ff1b3f9f 100644 --- a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/results/ValidationReport.java +++ b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/results/ValidationReport.java @@ -25,6 +25,7 @@ import org.eclipse.rdf4j.model.impl.DynamicModelFactory; import org.eclipse.rdf4j.model.vocabulary.RDF; import org.eclipse.rdf4j.model.vocabulary.RDF4J; +import org.eclipse.rdf4j.model.vocabulary.RDFS; import org.eclipse.rdf4j.model.vocabulary.SHACL; /** @@ -74,7 +75,11 @@ public Model asModel(Model model) { } public Model asModel() { - return asModel(DYNAMIC_MODEL_FACTORY.createEmptyModel()); + Model model = asModel(DYNAMIC_MODEL_FACTORY.createEmptyModel()); + model.setNamespace(SHACL.PREFIX, SHACL.NAMESPACE); + model.setNamespace(RDF.PREFIX, RDF.NAMESPACE); + model.setNamespace(RDFS.PREFIX, RDFS.NAMESPACE); + return model; } public final Resource getId() { diff --git a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/results/ValidationResult.java b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/results/ValidationResult.java index 600a8cd6ee8..1a88950b801 100644 --- a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/results/ValidationResult.java +++ b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/results/ValidationResult.java @@ -66,6 +66,7 @@ public class ValidationResult { private Path rsxPairwisePath; private ValidationResult detail; private Value pathIri; + private List messagesOverride; public ValidationResult(Value focusNode, Value value, Shape shape, ConstraintComponent sourceConstraint, Severity severity, ConstraintComponent.Scope scope, @@ -119,6 +120,13 @@ public void setDetail(ValidationResult detail) { this.detail = detail; } + /** + * Allows per-result overriding of sh:resultMessage values (used by SPARQL-based constraints). + */ + public void setMessagesOverride(List messagesOverride) { + this.messagesOverride = messagesOverride; + } + /** * @return all ValidationResult(s) with more information as to what failed. Usually for nested Shapes in eg. sh:or. */ @@ -175,7 +183,21 @@ public Model asModel(Model model, Set rdfListDedupe) { model.add(getId(), SHACL.SOURCE_CONSTRAINT_COMPONENT, getSourceConstraintComponent().getIri()); model.add(getId(), SHACL.RESULT_SEVERITY, severity.getIri()); - for (Literal message : shape.getMessage()) { + List messagesToAdd; + if (messagesOverride != null) { + messagesToAdd = messagesOverride; + } else if (sourceConstraint instanceof SparqlConstraintComponent) { + messagesToAdd = sourceConstraint.getDefaultMessage(); + } else { + List explicitMessages = shape.getExplicitMessages(); + if (!explicitMessages.isEmpty()) { + messagesToAdd = explicitMessages; + } else { + messagesToAdd = sourceConstraint.getDefaultMessage(); + } + } + + for (Literal message : messagesToAdd) { model.add(getId(), SHACL.RESULT_MESSAGE, message); } diff --git a/core/sail/shacl/src/test/java/org/eclipse/rdf4j/sail/shacl/DeepRecursionValidationTest.java b/core/sail/shacl/src/test/java/org/eclipse/rdf4j/sail/shacl/DeepRecursionValidationTest.java new file mode 100644 index 00000000000..27ffcb5e946 --- /dev/null +++ b/core/sail/shacl/src/test/java/org/eclipse/rdf4j/sail/shacl/DeepRecursionValidationTest.java @@ -0,0 +1,137 @@ +/******************************************************************************* + * Copyright (c) 2025 Eclipse RDF4J contributors. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Distribution License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + *******************************************************************************/ +package org.eclipse.rdf4j.sail.shacl; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.net.URL; +import java.util.Objects; +import java.util.Set; + +import org.eclipse.rdf4j.common.transaction.IsolationLevels; +import org.eclipse.rdf4j.model.Resource; +import org.eclipse.rdf4j.model.util.Values; +import org.eclipse.rdf4j.model.vocabulary.RDF4J; +import org.eclipse.rdf4j.model.vocabulary.RDFS; +import org.eclipse.rdf4j.repository.RepositoryException; +import org.eclipse.rdf4j.repository.sail.SailRepository; +import org.eclipse.rdf4j.repository.sail.SailRepositoryConnection; +import org.eclipse.rdf4j.rio.RDFFormat; +import org.eclipse.rdf4j.sail.memory.MemoryStore; +import org.eclipse.rdf4j.sail.shacl.ast.ShaclShapeParsingException; +import org.junit.jupiter.api.Test; + +class DeepRecursionValidationTest { + + @Test + void recursiveShapesCanBeRemovedAfterFailedValidation() throws Exception { + ShaclSail shaclSail = new ShaclSail(new MemoryStore()); + shaclSail.setShapesGraphs(Set.of(RDF4J.SHACL_SHAPE_GRAPH)); + SailRepository shaclRepository = new SailRepository(shaclSail); + shaclRepository.init(); + + URL shapes = Objects.requireNonNull( + getClass().getClassLoader().getResource("recursion/deep/shacl.trig")); + + try (SailRepositoryConnection connection = shaclRepository.getConnection()) { + connection.begin(IsolationLevels.NONE, ShaclSail.TransactionSettings.ValidationApproach.Disabled); + connection.add(shapes, shapes.toString(), RDFFormat.TRIG, RDF4J.SHACL_SHAPE_GRAPH); + connection.commit(); + } + + try (SailRepositoryConnection connection = shaclRepository.getConnection()) { + connection.begin(); + connection.add(RDF4J.SHACL_SHAPE_GRAPH, RDF4J.SHACL_SHAPE_GRAPH, RDF4J.SHACL_SHAPE_GRAPH); + + assertThrows(ShaclShapeParsingException.class, connection::commit); + connection.rollback(); + } + + try (SailRepositoryConnection connection = shaclRepository.getConnection()) { + connection.begin(); + connection.clear(RDF4J.SHACL_SHAPE_GRAPH); + connection.commit(); + + assertFalse(connection.hasStatement(null, null, null, false, RDF4J.SHACL_SHAPE_GRAPH)); + } finally { + shaclRepository.shutDown(); + } + } + + @Test + void recursiveShapesCanBeRemovedAfterFailedValidation2() throws Exception { + ShaclSail shaclSail = new ShaclSail(new MemoryStore()); + shaclSail.setShapesGraphs(Set.of(RDF4J.NIL)); + + SailRepository shaclRepository = new SailRepository(shaclSail); + shaclRepository.init(); + + URL shapes = Objects.requireNonNull( + getClass().getClassLoader().getResource("recursion/deep/shacl.trig")); + + try (SailRepositoryConnection connection = shaclRepository.getConnection()) { + connection.begin(IsolationLevels.NONE, ShaclSail.TransactionSettings.ValidationApproach.Disabled); + connection.add(shapes, shapes.toString(), RDFFormat.TRIG); + connection.commit(); + } + + try (SailRepositoryConnection connection = shaclRepository.getConnection()) { + connection.begin(); + connection.add(Values.bnode(), RDFS.LABEL, Values.literal("This will fail")); + + assertThrows(ShaclShapeParsingException.class, connection::commit); + connection.rollback(); + } + + try (SailRepositoryConnection connection = shaclRepository.getConnection()) { + connection.begin(); + connection.clear(new Resource[1]); + connection.commit(); + + assertFalse(connection.hasStatement(null, null, null, false)); + } finally { + shaclRepository.shutDown(); + } + } + + @Test + void deepRecursionAcrossAllConstraintTypesValidates() throws Exception { + SailRepository shaclRepository = Utils.getInitializedShaclRepository( + "recursion/deep/shacl.trig"); + + ShaclSail shaclSail = (ShaclSail) shaclRepository.getSail(); + shaclSail.setShapesGraphs(Set.of(RDF4J.NIL)); + + URL data = Objects.requireNonNull( + getClass().getClassLoader().getResource("recursion/deep/data.trig")); + + try (SailRepositoryConnection connection = shaclRepository.getConnection()) { + connection.begin(); + connection.add(data, data.toString(), RDFFormat.TRIG); + + ShaclShapeParsingException exception = assertThrows(ShaclShapeParsingException.class, () -> { + try { + connection.commit(); + } catch (RepositoryException e) { + connection.rollback(); + throw e.getCause(); + } + }); + + assertNotNull(exception.getId()); + + } finally { + shaclRepository.shutDown(); + } + } +} diff --git a/core/sail/shacl/src/test/java/org/eclipse/rdf4j/sail/shacl/LogicalRecursionValidationTest.java b/core/sail/shacl/src/test/java/org/eclipse/rdf4j/sail/shacl/LogicalRecursionValidationTest.java new file mode 100644 index 00000000000..feaada5ffee --- /dev/null +++ b/core/sail/shacl/src/test/java/org/eclipse/rdf4j/sail/shacl/LogicalRecursionValidationTest.java @@ -0,0 +1,105 @@ +/******************************************************************************* + * Copyright (c) 2025 Eclipse RDF4J contributors. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Distribution License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + *******************************************************************************/ +package org.eclipse.rdf4j.sail.shacl; + +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.net.URL; +import java.util.Objects; +import java.util.Set; + +import org.eclipse.rdf4j.model.vocabulary.RDF4J; +import org.eclipse.rdf4j.repository.sail.SailRepository; +import org.eclipse.rdf4j.repository.sail.SailRepositoryConnection; +import org.eclipse.rdf4j.rio.RDFFormat; +import org.eclipse.rdf4j.sail.shacl.ast.ShaclShapeParsingException; +import org.junit.jupiter.api.Test; + +class LogicalRecursionValidationTest { + + @Test + void recursionInNotIsDetected() throws Exception { + SailRepository shaclRepository = Utils + .getInitializedShaclRepository("recursion/logical/not/shacl.trig"); + + ShaclSail shaclSail = (ShaclSail) shaclRepository.getSail(); + shaclSail.setShapesGraphs(Set.of(RDF4J.NIL)); + + URL data = Objects.requireNonNull( + getClass().getClassLoader().getResource("recursion/logical/not/data.trig")); + + try (SailRepositoryConnection connection = shaclRepository.getConnection()) { + connection.begin(); + connection.add(data, data.toString(), RDFFormat.TRIG); + + assertThrows(ShaclShapeParsingException.class, () -> { + try { + connection.commit(); + } catch (Exception e) { + connection.rollback(); + throw e; + } + }); + } + } + + @Test + void recursionInOrIsDetected() throws Exception { + SailRepository shaclRepository = Utils + .getInitializedShaclRepository("recursion/logical/or/shacl.trig"); + + ShaclSail shaclSail = (ShaclSail) shaclRepository.getSail(); + shaclSail.setShapesGraphs(Set.of(RDF4J.NIL)); + + URL data = Objects.requireNonNull( + getClass().getClassLoader().getResource("recursion/logical/or/data.trig")); + + try (SailRepositoryConnection connection = shaclRepository.getConnection()) { + connection.begin(); + connection.add(data, data.toString(), RDFFormat.TRIG); + + assertThrows(ShaclShapeParsingException.class, () -> { + try { + connection.commit(); + } catch (Exception e) { + connection.rollback(); + throw e; + } + }); + } + } + + @Test + void recursionInNodeIsDetected() throws Exception { + SailRepository shaclRepository = Utils + .getInitializedShaclRepository("recursion/node/selfNode/shacl.trig"); + + ShaclSail shaclSail = (ShaclSail) shaclRepository.getSail(); + shaclSail.setShapesGraphs(Set.of(RDF4J.NIL)); + + URL data = Objects.requireNonNull( + getClass().getClassLoader().getResource("recursion/node/selfNode/data.trig")); + + try (SailRepositoryConnection connection = shaclRepository.getConnection()) { + connection.begin(); + connection.add(data, data.toString(), RDFFormat.TRIG); + + assertThrows(ShaclShapeParsingException.class, () -> { + try { + connection.commit(); + } catch (Exception e) { + connection.rollback(); + throw e; + } + }); + } + } +} diff --git a/core/sail/shacl/src/test/java/org/eclipse/rdf4j/sail/shacl/PropertyRecursionValidationTest.java b/core/sail/shacl/src/test/java/org/eclipse/rdf4j/sail/shacl/PropertyRecursionValidationTest.java new file mode 100644 index 00000000000..da95ec879cc --- /dev/null +++ b/core/sail/shacl/src/test/java/org/eclipse/rdf4j/sail/shacl/PropertyRecursionValidationTest.java @@ -0,0 +1,57 @@ +/******************************************************************************* + * Copyright (c) 2025 Eclipse RDF4J contributors. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Distribution License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + *******************************************************************************/ +package org.eclipse.rdf4j.sail.shacl; + +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.net.URL; +import java.util.Objects; +import java.util.Set; + +import org.eclipse.rdf4j.model.vocabulary.RDF4J; +import org.eclipse.rdf4j.repository.sail.SailRepository; +import org.eclipse.rdf4j.repository.sail.SailRepositoryConnection; +import org.eclipse.rdf4j.rio.RDFFormat; +import org.eclipse.rdf4j.sail.shacl.ast.ShaclShapeParsingException; +import org.junit.jupiter.api.Test; + +class PropertyRecursionValidationTest { + + @Test + void nodeShapeRecursingThroughPropertyShapeWithOnlyNodeIsDetected() throws Exception { + SailRepository shaclRepository = Utils.getInitializedShaclRepository( + "recursion/property/selfNodeOnly/shacl.trig"); + + ShaclSail shaclSail = (ShaclSail) shaclRepository.getSail(); + shaclSail.setShapesGraphs(Set.of(RDF4J.NIL)); + + URL data = Objects.requireNonNull( + getClass().getClassLoader() + .getResource("recursion/property/selfNodeOnly/data.trig")); + + try (SailRepositoryConnection connection = shaclRepository.getConnection()) { + connection.begin(); + connection.add(data, data.toString(), RDFFormat.TRIG); + + assertThrows(ShaclShapeParsingException.class, () -> { + try { + connection.commit(); + } catch (Exception e) { + connection.rollback(); + throw e; + } + }); + } finally { + shaclRepository.shutDown(); + } + } + +} diff --git a/core/sail/shacl/src/test/java/org/eclipse/rdf4j/sail/shacl/ast/ShapeEqualsTest.java b/core/sail/shacl/src/test/java/org/eclipse/rdf4j/sail/shacl/ast/ShapeEqualsTest.java new file mode 100644 index 00000000000..752e146bb47 --- /dev/null +++ b/core/sail/shacl/src/test/java/org/eclipse/rdf4j/sail/shacl/ast/ShapeEqualsTest.java @@ -0,0 +1,62 @@ +/******************************************************************************* + * Copyright (c) 2025 Eclipse RDF4J contributors. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Distribution License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + *******************************************************************************/ +package org.eclipse.rdf4j.sail.shacl.ast; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.eclipse.rdf4j.model.vocabulary.XSD; +import org.eclipse.rdf4j.sail.shacl.ast.constraintcomponents.DatatypeConstraintComponent; +import org.junit.jupiter.api.Test; + +public class ShapeEqualsTest { + + @Test + public void shapesWithDuplicateConstraintComponentsAreNotEqualToShapesWithDifferentComponents() { + NodeShape shapeWithDuplicate = new NodeShape(); + + DatatypeConstraintComponent firstConstraint = new DatatypeConstraintComponent(XSD.STRING); + + shapeWithDuplicate.constraintComponents.add(firstConstraint); + shapeWithDuplicate.constraintComponents.add(firstConstraint); + + NodeShape shapeWithDifferentSecondConstraint = new NodeShape(); + + DatatypeConstraintComponent differentConstraint = new DatatypeConstraintComponent(XSD.INT); + + shapeWithDifferentSecondConstraint.constraintComponents.add(firstConstraint); + shapeWithDifferentSecondConstraint.constraintComponents.add(differentConstraint); + + assertTrue(shapeWithDuplicate.equals(shapeWithDuplicate)); + assertTrue(shapeWithDifferentSecondConstraint.equals(shapeWithDifferentSecondConstraint)); + + assertFalse(shapeWithDuplicate.equals(shapeWithDifferentSecondConstraint)); + assertFalse(shapeWithDifferentSecondConstraint.equals(shapeWithDuplicate)); + } + + @Test + public void shapesWithSameComponentsInDifferentOrderAreEqual() { + NodeShape first = new NodeShape(); + NodeShape second = new NodeShape(); + + DatatypeConstraintComponent firstConstraint = new DatatypeConstraintComponent(XSD.STRING); + DatatypeConstraintComponent secondConstraint = new DatatypeConstraintComponent(XSD.INT); + + first.constraintComponents.add(firstConstraint); + first.constraintComponents.add(secondConstraint); + + second.constraintComponents.add(secondConstraint); + second.constraintComponents.add(firstConstraint); + + assertTrue(first.equals(second)); + assertTrue(second.equals(first)); + } +} diff --git a/core/sail/shacl/src/test/java/org/eclipse/rdf4j/sail/shacl/results/ShaclValidatorSparqlMessagesTest.java b/core/sail/shacl/src/test/java/org/eclipse/rdf4j/sail/shacl/results/ShaclValidatorSparqlMessagesTest.java new file mode 100644 index 00000000000..0c96b634220 --- /dev/null +++ b/core/sail/shacl/src/test/java/org/eclipse/rdf4j/sail/shacl/results/ShaclValidatorSparqlMessagesTest.java @@ -0,0 +1,1046 @@ +/******************************************************************************* + * Copyright (c) 2025 Eclipse RDF4J contributors. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Distribution License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + ******************************************************************************/ + +package org.eclipse.rdf4j.sail.shacl.results; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.StringReader; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; + +import org.eclipse.rdf4j.model.IRI; +import org.eclipse.rdf4j.model.Literal; +import org.eclipse.rdf4j.model.Model; +import org.eclipse.rdf4j.model.Resource; +import org.eclipse.rdf4j.model.Value; +import org.eclipse.rdf4j.model.vocabulary.SHACL; +import org.eclipse.rdf4j.repository.sail.SailRepository; +import org.eclipse.rdf4j.repository.sail.SailRepositoryConnection; +import org.eclipse.rdf4j.rio.RDFFormat; +import org.eclipse.rdf4j.sail.SailException; +import org.eclipse.rdf4j.sail.memory.MemoryStore; +import org.eclipse.rdf4j.sail.shacl.ShaclSailValidationReportHelper; +import org.eclipse.rdf4j.sail.shacl.ShaclValidator; +import org.junit.jupiter.api.Test; + +public class ShaclValidatorSparqlMessagesTest { + + @Test + public void multipleSparqlConstraintsDifferentMessages() throws Exception { + + String shapesTtl = "@prefix ex: .\n" + + "@prefix sh: .\n" + + "@prefix xsd: .\n" + + "\n" + + "ex:PersonShape a sh:NodeShape ;\n" + + " sh:targetClass ex:Person ;\n" + + " sh:sparql [\n" + + " a sh:SPARQLConstraint ;\n" + + " sh:message \"Missing ex:name.\" ;\n" + + " sh:select \"\"\"\n" + + " PREFIX ex: \n" + + " SELECT $this WHERE {\n" + + " FILTER NOT EXISTS { $this ex:name ?name }\n" + + " }\n" + + " \"\"\" ;\n" + + " ] ;\n" + + " sh:sparql [\n" + + " a sh:SPARQLConstraint ;\n" + + " sh:message \"Invalid ex:age.\" ;\n" + + " sh:select \"\"\"\n" + + " PREFIX ex: \n" + + " PREFIX xsd: \n" + + " SELECT $this WHERE {\n" + + " $this ex:age ?age .\n" + + " FILTER ( datatype(?age) != xsd:integer )\n" + + " }\n" + + " \"\"\" ;\n" + + " ] .\n"; + + String dataTtl = "@prefix ex: .\n" + + "@prefix xsd: .\n" + + "\n" + + "ex:alice a ex:Person ;\n" + + " ex:age \"twenty\"^^xsd:string .\n"; + + SailRepository shapes = new SailRepository(new MemoryStore()); + try (SailRepositoryConnection connection = shapes.getConnection()) { + connection.add(new StringReader(shapesTtl), RDFFormat.TURTLE); + } + + SailRepository data = new SailRepository(new MemoryStore()); + try (SailRepositoryConnection connection = data.getConnection()) { + connection.add(new StringReader(dataTtl), RDFFormat.TURTLE); + } + + ValidationReport report = ShaclValidator.validate(data.getSail(), shapes.getSail()); + assertFalse(report.conforms()); + assertEquals(2, report.getValidationResult().size()); + + assertResultMessagesMatchConstraintMessages(report); + } + + @Test + public void multipleMessagesPerConstraintAreAllReported() throws Exception { + + String shapesTtl = "@prefix ex: .\n" + + "@prefix sh: .\n" + + "@prefix xsd: .\n" + + "\n" + + "ex:PersonShape a sh:NodeShape ;\n" + + " sh:targetClass ex:Person ;\n" + + " sh:sparql [\n" + + " a sh:SPARQLConstraint ;\n" + + " sh:message \"Name missing\"@en, \"Naam ontbreekt\"@nl ;\n" + + " sh:select \"\"\"\n" + + " PREFIX ex: \n" + + " SELECT $this WHERE {\n" + + " FILTER NOT EXISTS { $this ex:name ?name }\n" + + " }\n" + + " \"\"\" ;\n" + + " ] ;\n" + + " sh:sparql [\n" + + " a sh:SPARQLConstraint ;\n" + + " sh:message \"Age must be integer\" ;\n" + + " sh:select \"\"\"\n" + + " PREFIX ex: \n" + + " PREFIX xsd: \n" + + " SELECT $this WHERE {\n" + + " $this ex:age ?age .\n" + + " FILTER ( datatype(?age) != xsd:integer )\n" + + " }\n" + + " \"\"\" ;\n" + + " ] .\n"; + + String dataTtl = "@prefix ex: .\n" + + "@prefix xsd: .\n" + + "\n" + + "ex:bob a ex:Person ;\n" + + " ex:age \"old\"^^xsd:string .\n"; + + SailRepository shapes = new SailRepository(new MemoryStore()); + try (SailRepositoryConnection connection = shapes.getConnection()) { + connection.add(new StringReader(shapesTtl), RDFFormat.TURTLE); + } + + SailRepository data = new SailRepository(new MemoryStore()); + try (SailRepositoryConnection connection = data.getConnection()) { + connection.add(new StringReader(dataTtl), RDFFormat.TURTLE); + } + + ValidationReport report = ShaclValidator.validate(data.getSail(), shapes.getSail()); + assertFalse(report.conforms()); + + assertResultMessagesMatchConstraintMessages(report); + } + + @Test + public void propertyShapeWithMultipleSparqlConstraintsReportsAllMessages() throws Exception { + + String shapesTtl = "@prefix ex: .\n" + + "@prefix sh: .\n" + + "\n" + + "ex:PersonShape a sh:NodeShape ;\n" + + " sh:targetClass ex:Person ;\n" + + " sh:message \"test\" ;\n" + + " sh:property [\n" + + " sh:path ex:knows ;\n" + + " sh:sparql [\n" + + " a sh:SPARQLConstraint ;\n" + + " sh:message \"ex:knows values must be IRIs.\" ;\n" + + " sh:select \"\"\"\n" + + " PREFIX ex: \n" + + " SELECT $this ?value WHERE {\n" + + " $this ex:knows ?value .\n" + + " FILTER ( !isIRI(?value) )\n" + + " }\n" + + " \"\"\" ;\n" + + " ] ;\n" + + " sh:sparql [\n" + + " a sh:SPARQLConstraint ;\n" + + " sh:message \"ex:knows values must be ex:Person.\" ;\n" + + " sh:select \"\"\"\n" + + " PREFIX ex: \n" + + " SELECT $this ?value WHERE {\n" + + " $this ex:knows ?value .\n" + + " FILTER NOT EXISTS { ?value a ex:Person }\n" + + " }\n" + + " \"\"\" ;\n" + + " ]\n" + + " ] .\n"; + + String dataTtl = "@prefix ex: .\n" + + "\n" + + "ex:alice a ex:Person ;\n" + + " ex:knows \"Bob\" , ex:charlie .\n"; + + SailRepository shapes = new SailRepository(new MemoryStore()); + try (SailRepositoryConnection connection = shapes.getConnection()) { + connection.add(new StringReader(shapesTtl), RDFFormat.TURTLE); + } + + SailRepository data = new SailRepository(new MemoryStore()); + try (SailRepositoryConnection connection = data.getConnection()) { + connection.add(new StringReader(dataTtl), RDFFormat.TURTLE); + } + + ValidationReport report = ShaclValidator.validate(data.getSail(), shapes.getSail()); + System.out.println(report); + assertFalse(report.conforms()); + + assertResultMessagesMatchConstraintMessages(report); + } + + @Test + public void personShapePositiveAgeAndNoSelfManageMessagesPerViolation() throws Exception { + + String shapesTtl = "@prefix ex: .\n" + + "@prefix sh: .\n" + + "@prefix xsd: .\n" + + "\n" + + "ex:PersonShape a sh:NodeShape ;\n" + + " sh:targetClass ex:Person ;\n" + + " sh:message \"a\", \"b\" ;\n" + + " sh:sparql [\n" + + " a sh:SPARQLConstraint ;\n" + + " sh:message \"Age must be positive.\" ;\n" + + " sh:select \"\"\"\n" + + " PREFIX ex: \n" + + " SELECT $this WHERE {\n" + + " $this ex:age ?age .\n" + + " FILTER ( ?age <= 0 )\n" + + " }\n" + + " \"\"\" ;\n" + + " ] ;\n" + + " sh:sparql [\n" + + " a sh:SPARQLConstraint ;\n" + + " sh:message \"Person must not manage themselves.\" ;\n" + + " sh:select \"\"\"\n" + + " PREFIX ex: \n" + + " SELECT $this WHERE {\n" + + " $this ex:manage $this .\n" + + " }\n" + + " \"\"\" ;\n" + + " ] .\n"; + + String dataTtl = "@prefix ex: .\n" + + "@prefix xsd: .\n" + + "\n" + + "ex:alice a ex:Person ;\n" + + " ex:age \"-1\"^^xsd:integer ;\n" + + " ex:age \"-2\"^^xsd:integer ;\n" + + " ex:manage ex:alice .\n"; + + SailRepository shapes = new SailRepository(new MemoryStore()); + try (SailRepositoryConnection connection = shapes.getConnection()) { + connection.add(new StringReader(shapesTtl), RDFFormat.TURTLE); + } + + SailRepository data = new SailRepository(new MemoryStore()); + try (SailRepositoryConnection connection = data.getConnection()) { + connection.add(new StringReader(dataTtl), RDFFormat.TURTLE); + } + + ValidationReport report = ShaclValidator.validate(data.getSail(), shapes.getSail()); + ShaclSailValidationReportHelper.printValidationReport(report, System.out); + assertFalse(report.conforms()); + assertEquals(3, report.getValidationResult().size()); + + assertResultMessagesMatchConstraintMessages(report); + } + + @Test + public void messageBindingOverridesConstraintMessages() throws Exception { + + String shapesTtl = "@prefix ex: .\n" + + "@prefix sh: .\n" + + "\n" + + "ex:PersonShape a sh:NodeShape ;\n" + + " sh:targetClass ex:Person ;\n" + + " sh:sparql [\n" + + " a sh:SPARQLConstraint ;\n" + + " sh:message \"Fallback message\" ;\n" + + " sh:select \"\"\"\n" + + " PREFIX ex: \n" + + " SELECT $this ?message WHERE {\n" + + " FILTER NOT EXISTS { $this ex:name ?n }\n" + + " BIND(\"Bound message\" AS ?message)\n" + + " }\n" + + " \"\"\" ;\n" + + " ] .\n"; + + String dataTtl = "@prefix ex: .\n" + + "\n" + + "ex:alice a ex:Person .\n"; + + SailRepository shapes = new SailRepository(new MemoryStore()); + try (SailRepositoryConnection connection = shapes.getConnection()) { + connection.add(new StringReader(shapesTtl), RDFFormat.TURTLE); + } + + SailRepository data = new SailRepository(new MemoryStore()); + try (SailRepositoryConnection connection = data.getConnection()) { + connection.add(new StringReader(dataTtl), RDFFormat.TURTLE); + } + + ValidationReport report = ShaclValidator.validate(data.getSail(), shapes.getSail()); + assertFalse(report.conforms()); + assertEquals(1, report.getValidationResult().size()); + + assertAllResultsHaveMessages(report, Set.of("Bound message")); + } + + @Test + public void messageBindingUsedWhenNoConstraintMessages() throws Exception { + + String shapesTtl = "@prefix ex: .\n" + + "@prefix sh: .\n" + + "\n" + + "ex:PersonShape a sh:NodeShape ;\n" + + " sh:targetClass ex:Person ;\n" + + " sh:sparql [\n" + + " a sh:SPARQLConstraint ;\n" + + " sh:select \"\"\"\n" + + " PREFIX ex: \n" + + " SELECT $this ?message WHERE {\n" + + " FILTER NOT EXISTS { $this ex:name ?n }\n" + + " BIND(\"Only bound message\"@en AS ?message)\n" + + " }\n" + + " \"\"\" ;\n" + + " ] .\n"; + + String dataTtl = "@prefix ex: .\n" + + "\n" + + "ex:alice a ex:Person .\n"; + + SailRepository shapes = new SailRepository(new MemoryStore()); + try (SailRepositoryConnection connection = shapes.getConnection()) { + connection.add(new StringReader(shapesTtl), RDFFormat.TURTLE); + } + + SailRepository data = new SailRepository(new MemoryStore()); + try (SailRepositoryConnection connection = data.getConnection()) { + connection.add(new StringReader(dataTtl), RDFFormat.TURTLE); + } + + ValidationReport report = ShaclValidator.validate(data.getSail(), shapes.getSail()); + assertFalse(report.conforms()); + assertEquals(1, report.getValidationResult().size()); + + assertAllResultsHaveMessages(report, Set.of("Only bound message@en")); + } + + @Test + public void messageTemplatesSubstituteSelectVariables() throws Exception { + + String shapesTtl = "@prefix ex: .\n" + + "@prefix sh: .\n" + + "@prefix xsd: .\n" + + "\n" + + "ex:PersonShape a sh:NodeShape ;\n" + + " sh:targetClass ex:Person ;\n" + + " sh:sparql [\n" + + " a sh:SPARQLConstraint ;\n" + + " sh:message \"Invalid age {?value} for {$this}\" ;\n" + + " sh:select \"\"\"\n" + + " PREFIX ex: \n" + + " PREFIX xsd: \n" + + " SELECT $this ?value WHERE {\n" + + " $this ex:age ?value .\n" + + " FILTER ( datatype(?value) != xsd:integer )\n" + + " }\n" + + " \"\"\" ;\n" + + " ] .\n"; + + String dataTtl = "@prefix ex: .\n" + + "@prefix xsd: .\n" + + "\n" + + "ex:bob a ex:Person ;\n" + + " ex:age \"old\"^^xsd:string .\n"; + + SailRepository shapes = new SailRepository(new MemoryStore()); + try (SailRepositoryConnection connection = shapes.getConnection()) { + connection.add(new StringReader(shapesTtl), RDFFormat.TURTLE); + } + + SailRepository data = new SailRepository(new MemoryStore()); + try (SailRepositoryConnection connection = data.getConnection()) { + connection.add(new StringReader(dataTtl), RDFFormat.TURTLE); + } + + ValidationReport report = ShaclValidator.validate(data.getSail(), shapes.getSail()); + assertFalse(report.conforms()); + assertEquals(1, report.getValidationResult().size()); + + assertAllResultsHaveMessages(report, Set.of("Invalid age old for http://example.com/ns#bob")); + } + + @Test + public void noMessagesProducesNoResultMessage() throws Exception { + + String shapesTtl = "@prefix ex: .\n" + + "@prefix sh: .\n" + + "\n" + + "ex:PersonShape a sh:NodeShape ;\n" + + " sh:targetClass ex:Person ;\n" + + " sh:sparql [\n" + + " a sh:SPARQLConstraint ;\n" + + " sh:select \"\"\"\n" + + " PREFIX ex: \n" + + " SELECT $this WHERE {\n" + + " FILTER NOT EXISTS { $this ex:name ?n }\n" + + " }\n" + + " \"\"\" ;\n" + + " ] .\n"; + + String dataTtl = "@prefix ex: .\n" + + "\n" + + "ex:alice a ex:Person .\n"; + + SailRepository shapes = new SailRepository(new MemoryStore()); + try (SailRepositoryConnection connection = shapes.getConnection()) { + connection.add(new StringReader(shapesTtl), RDFFormat.TURTLE); + } + + SailRepository data = new SailRepository(new MemoryStore()); + try (SailRepositoryConnection connection = data.getConnection()) { + connection.add(new StringReader(dataTtl), RDFFormat.TURTLE); + } + + ValidationReport report = ShaclValidator.validate(data.getSail(), shapes.getSail()); + assertFalse(report.conforms()); + assertEquals(1, report.getValidationResult().size()); + + Model model = report.asModel(); + for (ValidationResult result : report.getValidationResult()) { + Set resultMessages = toMessageSet( + model.filter(result.getId(), SHACL.RESULT_MESSAGE, null).objects()); + assertTrue(resultMessages.isEmpty()); + } + } + + @Test + public void multipleTemplateMessagesAreAllSubstitutedAndReported() throws Exception { + + String shapesTtl = "@prefix ex: .\n" + + "@prefix sh: .\n" + + "@prefix xsd: .\n" + + "\n" + + "ex:PersonShape a sh:NodeShape ;\n" + + " sh:targetClass ex:Person ;\n" + + " sh:sparql [\n" + + " a sh:SPARQLConstraint ;\n" + + " sh:message \"Bad age {?value}\"@en , \"Leeftijd ongeldig {?value}\"@nl ;\n" + + " sh:select \"\"\"\n" + + " PREFIX ex: \n" + + " PREFIX xsd: \n" + + " SELECT $this ?value WHERE {\n" + + " $this ex:age ?value .\n" + + " FILTER ( datatype(?value) != xsd:integer )\n" + + " }\n" + + " \"\"\" ;\n" + + " ] .\n"; + + String dataTtl = "@prefix ex: .\n" + + "@prefix xsd: .\n" + + "\n" + + "ex:bob a ex:Person ;\n" + + " ex:age \"old\"^^xsd:string ;\n" + + " ex:age \"ancient\"^^xsd:string .\n"; + + SailRepository shapes = new SailRepository(new MemoryStore()); + try (SailRepositoryConnection connection = shapes.getConnection()) { + connection.add(new StringReader(shapesTtl), RDFFormat.TURTLE); + } + + SailRepository data = new SailRepository(new MemoryStore()); + try (SailRepositoryConnection connection = data.getConnection()) { + connection.add(new StringReader(dataTtl), RDFFormat.TURTLE); + } + + ValidationReport report = ShaclValidator.validate(data.getSail(), shapes.getSail()); + assertFalse(report.conforms()); + assertEquals(2, report.getValidationResult().size()); + + Model model = report.asModel(); + for (ValidationResult result : report.getValidationResult()) { + String valueLabel = model.filter(result.getId(), SHACL.VALUE, null) + .objects() + .stream() + .findFirst() + .map(Value::stringValue) + .orElseThrow(); + + Set expected = Set.of( + "Bad age " + valueLabel + "@en", + "Leeftijd ongeldig " + valueLabel + "@nl"); + + Set actual = toMessageSet(model.filter(result.getId(), SHACL.RESULT_MESSAGE, null).objects()); + assertEquals(expected, actual); + } + } + + @Test + public void unboundTemplateVariablesRemainUnchanged() throws Exception { + + String shapesTtl = "@prefix ex: .\n" + + "@prefix sh: .\n" + + "\n" + + "ex:PersonShape a sh:NodeShape ;\n" + + " sh:targetClass ex:Person ;\n" + + " sh:sparql [\n" + + " a sh:SPARQLConstraint ;\n" + + " sh:message \"Missing value {?missing} for {$this}\" ;\n" + + " sh:select \"\"\"\n" + + " PREFIX ex: \n" + + " SELECT $this WHERE {\n" + + " FILTER NOT EXISTS { $this ex:name ?n }\n" + + " }\n" + + " \"\"\" ;\n" + + " ] .\n"; + + String dataTtl = "@prefix ex: .\n" + + "\n" + + "ex:alice a ex:Person .\n"; + + SailRepository shapes = new SailRepository(new MemoryStore()); + try (SailRepositoryConnection connection = shapes.getConnection()) { + connection.add(new StringReader(shapesTtl), RDFFormat.TURTLE); + } + + SailRepository data = new SailRepository(new MemoryStore()); + try (SailRepositoryConnection connection = data.getConnection()) { + connection.add(new StringReader(dataTtl), RDFFormat.TURTLE); + } + + ValidationReport report = ShaclValidator.validate(data.getSail(), shapes.getSail()); + assertFalse(report.conforms()); + assertEquals(1, report.getValidationResult().size()); + + assertAllResultsHaveMessages(report, Set.of("Missing value {?missing} for http://example.com/ns#alice")); + } + + @Test + public void repeatedTemplatePlaceholdersAreAllReplaced() throws Exception { + + String shapesTtl = "@prefix ex: .\n" + + "@prefix sh: .\n" + + "@prefix xsd: .\n" + + "\n" + + "ex:PersonShape a sh:NodeShape ;\n" + + " sh:targetClass ex:Person ;\n" + + " sh:sparql [\n" + + " a sh:SPARQLConstraint ;\n" + + " sh:message \"Value {?value} repeats {?value}\" ;\n" + + " sh:select \"\"\"\n" + + " PREFIX ex: \n" + + " PREFIX xsd: \n" + + " SELECT $this ?value WHERE {\n" + + " $this ex:age ?value .\n" + + " FILTER ( datatype(?value) != xsd:integer )\n" + + " }\n" + + " \"\"\" ;\n" + + " ] .\n"; + + String dataTtl = "@prefix ex: .\n" + + "@prefix xsd: .\n" + + "\n" + + "ex:bob a ex:Person ;\n" + + " ex:age \"old\"^^xsd:string .\n"; + + SailRepository shapes = new SailRepository(new MemoryStore()); + try (SailRepositoryConnection connection = shapes.getConnection()) { + connection.add(new StringReader(shapesTtl), RDFFormat.TURTLE); + } + + SailRepository data = new SailRepository(new MemoryStore()); + try (SailRepositoryConnection connection = data.getConnection()) { + connection.add(new StringReader(dataTtl), RDFFormat.TURTLE); + } + + ValidationReport report = ShaclValidator.validate(data.getSail(), shapes.getSail()); + assertFalse(report.conforms()); + assertEquals(1, report.getValidationResult().size()); + + assertAllResultsHaveMessages(report, Set.of("Value old repeats old")); + } + + @Test + public void messageBindingNonLiteralConvertedToStringLiteral() throws Exception { + + String shapesTtl = "@prefix ex: .\n" + + "@prefix sh: .\n" + + "\n" + + "ex:PersonShape a sh:NodeShape ;\n" + + " sh:targetClass ex:Person ;\n" + + " sh:sparql [\n" + + " a sh:SPARQLConstraint ;\n" + + " sh:message \"Fallback\" ;\n" + + " sh:select \"\"\"\n" + + " PREFIX ex: \n" + + " SELECT $this ?message WHERE {\n" + + " FILTER NOT EXISTS { $this ex:name ?n }\n" + + " BIND(ex:msg1 AS ?message)\n" + + " }\n" + + " \"\"\" ;\n" + + " ] .\n"; + + String dataTtl = "@prefix ex: .\n" + + "\n" + + "ex:alice a ex:Person .\n"; + + SailRepository shapes = new SailRepository(new MemoryStore()); + try (SailRepositoryConnection connection = shapes.getConnection()) { + connection.add(new StringReader(shapesTtl), RDFFormat.TURTLE); + } + + SailRepository data = new SailRepository(new MemoryStore()); + try (SailRepositoryConnection connection = data.getConnection()) { + connection.add(new StringReader(dataTtl), RDFFormat.TURTLE); + } + + ValidationReport report = ShaclValidator.validate(data.getSail(), shapes.getSail()); + assertFalse(report.conforms()); + assertEquals(1, report.getValidationResult().size()); + + assertAllResultsHaveMessages(report, Set.of("http://example.com/ns#msg1")); + } + + @Test + public void preboundShapesGraphAvailableInSparqlConstraints() throws Exception { + + String shapesTrig = "@prefix ex: .\n" + + "@prefix sh: .\n" + + "@prefix rdf4j: .\n" + + "\n" + + "rdf4j:nil sh:shapesGraph ex:shapesGraph .\n" + + "\n" + + "ex:shapesGraph {\n" + + " ex:PersonShape a sh:NodeShape ;\n" + + " sh:targetClass ex:Person ;\n" + + " sh:sparql [\n" + + " a sh:SPARQLConstraint ;\n" + + " sh:select \"\"\"\n" + + " PREFIX ex: \n" + + " SELECT $this ?message WHERE {\n" + + " FILTER NOT EXISTS { $this ex:name ?n }\n" + + " BIND(CONCAT(\"sg=\", STR($shapesGraph)) AS ?message)\n" + + " }\n" + + " \"\"\" ;\n" + + " ] .\n" + + "}\n"; + + String dataTtl = "@prefix ex: .\n" + + "\n" + + "ex:alice a ex:Person .\n"; + + SailRepository shapes = new SailRepository(new MemoryStore()); + try (SailRepositoryConnection connection = shapes.getConnection()) { + connection.add(new StringReader(shapesTrig), RDFFormat.TRIG); + } + + SailRepository data = new SailRepository(new MemoryStore()); + try (SailRepositoryConnection connection = data.getConnection()) { + connection.add(new StringReader(dataTtl), RDFFormat.TURTLE); + } + + ValidationReport report = ShaclValidator.validate(data.getSail(), shapes.getSail()); + assertFalse(report.conforms()); + assertEquals(1, report.getValidationResult().size()); + + assertAllResultsHaveMessages(report, Set.of("sg=http://example.com/ns#shapesGraph")); + } + + @Test + public void preboundCurrentShapeAvailableInSparqlConstraints() throws Exception { + + String shapesTtl = "@prefix ex: .\n" + + "@prefix sh: .\n" + + "\n" + + "ex:PersonShape a sh:NodeShape ;\n" + + " sh:targetClass ex:Person ;\n" + + " sh:sparql [\n" + + " a sh:SPARQLConstraint ;\n" + + " sh:select \"\"\"\n" + + " PREFIX ex: \n" + + " SELECT $this ?message WHERE {\n" + + " FILTER NOT EXISTS { $this ex:name ?n }\n" + + " BIND(CONCAT(\"cs=\", STR($currentShape)) AS ?message)\n" + + " }\n" + + " \"\"\" ;\n" + + " ] .\n"; + + String dataTtl = "@prefix ex: .\n" + + "\n" + + "ex:alice a ex:Person .\n"; + + SailRepository shapes = new SailRepository(new MemoryStore()); + try (SailRepositoryConnection connection = shapes.getConnection()) { + connection.add(new StringReader(shapesTtl), RDFFormat.TURTLE); + } + + SailRepository data = new SailRepository(new MemoryStore()); + try (SailRepositoryConnection connection = data.getConnection()) { + connection.add(new StringReader(dataTtl), RDFFormat.TURTLE); + } + + ValidationReport report = ShaclValidator.validate(data.getSail(), shapes.getSail()); + assertFalse(report.conforms()); + assertEquals(1, report.getValidationResult().size()); + + assertAllResultsHaveMessages(report, Set.of("cs=http://example.com/ns#PersonShape")); + } + + @Test + public void messageTemplatesSubstitutePreboundVariablesForNodeShapes() throws Exception { + + String shapesTrig = "@prefix ex: .\n" + + "@prefix sh: .\n" + + "@prefix rdf4j: .\n" + + "\n" + + "rdf4j:nil sh:shapesGraph ex:shapesGraph .\n" + + "\n" + + "ex:shapesGraph {\n" + + " ex:PersonShape a sh:NodeShape ;\n" + + " sh:targetClass ex:Person ;\n" + + " sh:sparql [\n" + + " a sh:SPARQLConstraint ;\n" + + " sh:message \"Graph {?shapesGraph} shape {?currentShape} focus {$this}\" ;\n" + + " sh:select \"\"\"\n" + + " PREFIX ex: \n" + + " SELECT $this WHERE {\n" + + " FILTER NOT EXISTS { $this ex:name ?n }\n" + + " }\n" + + " \"\"\" ;\n" + + " ] .\n" + + "}\n"; + + String dataTtl = "@prefix ex: .\n" + + "\n" + + "ex:alice a ex:Person .\n"; + + SailRepository shapes = new SailRepository(new MemoryStore()); + try (SailRepositoryConnection connection = shapes.getConnection()) { + connection.add(new StringReader(shapesTrig), RDFFormat.TRIG); + } + + SailRepository data = new SailRepository(new MemoryStore()); + try (SailRepositoryConnection connection = data.getConnection()) { + connection.add(new StringReader(dataTtl), RDFFormat.TURTLE); + } + + ValidationReport report = ShaclValidator.validate(data.getSail(), shapes.getSail()); + assertFalse(report.conforms()); + assertEquals(1, report.getValidationResult().size()); + + assertAllResultsHaveMessages(report, + Set.of("Graph http://example.com/ns#shapesGraph shape http://example.com/ns#PersonShape focus http://example.com/ns#alice")); + } + + @Test + public void messageTemplatesSubstitutePreboundVariablesForPropertyShapes() throws Exception { + + String shapesTrig = "@prefix ex: .\n" + + "@prefix sh: .\n" + + "@prefix xsd: .\n" + + "@prefix rdf4j: .\n" + + "\n" + + "rdf4j:nil sh:shapesGraph ex:shapesGraph .\n" + + "\n" + + "ex:shapesGraph {\n" + + " ex:PersonShape a sh:NodeShape ;\n" + + " sh:targetClass ex:Person ;\n" + + " sh:property [\n" + + " sh:path ex:age ;\n" + + " sh:sparql [\n" + + " a sh:SPARQLConstraint ;\n" + + " sh:message \"Graph {?shapesGraph} shape {?currentShape} focus {$this} value {?value}\" ;\n" + + " sh:select \"\"\"\n" + + " PREFIX ex: \n" + + " PREFIX xsd: \n" + + " SELECT $this ?value WHERE {\n" + + " $this $PATH ?value .\n" + + " FILTER ( ?value <= 0 )\n" + + " }\n" + + " \"\"\" ;\n" + + " ]\n" + + " ] .\n" + + "}\n"; + + String dataTtl = "@prefix ex: .\n" + + "@prefix xsd: .\n" + + "\n" + + "ex:alice a ex:Person ;\n" + + " ex:age \"-1\"^^xsd:integer .\n"; + + SailRepository shapes = new SailRepository(new MemoryStore()); + try (SailRepositoryConnection connection = shapes.getConnection()) { + connection.add(new StringReader(shapesTrig), RDFFormat.TRIG); + } + + SailRepository data = new SailRepository(new MemoryStore()); + try (SailRepositoryConnection connection = data.getConnection()) { + connection.add(new StringReader(dataTtl), RDFFormat.TURTLE); + } + + ValidationReport report = ShaclValidator.validate(data.getSail(), shapes.getSail()); + assertFalse(report.conforms()); + assertEquals(1, report.getValidationResult().size()); + + Model model = report.asModel(); + ValidationResult result = report.getValidationResult().iterator().next(); + Set messages = toMessageSet(model.filter(result.getId(), SHACL.RESULT_MESSAGE, null).objects()); + assertEquals(1, messages.size()); + String message = messages.iterator().next(); + assertTrue(message.startsWith("Graph http://example.com/ns#shapesGraph shape ")); + assertTrue(message.contains(" focus http://example.com/ns#alice value -1")); + assertFalse(message.contains("{?currentShape}")); + } + + @Test + public void messageTemplatesSubstitutePathPlaceholderForPropertyShapes() throws Exception { + + String shapesTrig = "@prefix ex: .\n" + + "@prefix sh: .\n" + + "@prefix xsd: .\n" + + "\n" + + "ex:PersonShape a sh:NodeShape ;\n" + + " sh:targetClass ex:Person ;\n" + + " sh:property [\n" + + " sh:path ex:age ;\n" + + " sh:sparql [\n" + + " a sh:SPARQLConstraint ;\n" + + " sh:message \"Bad path {$PATH} and {?PATH}\" ;\n" + + " sh:select \"\"\"\n" + + " PREFIX ex: \n" + + " PREFIX xsd: \n" + + " SELECT $this ?value WHERE {\n" + + " $this $PATH ?value .\n" + + " FILTER ( ?value <= 0 )\n" + + " }\n" + + " \"\"\" ;\n" + + " ]\n" + + " ] .\n"; + + String dataTtl = "@prefix ex: .\n" + + "@prefix xsd: .\n" + + "\n" + + "ex:alice a ex:Person ;\n" + + " ex:age \"-1\"^^xsd:integer .\n"; + + SailRepository shapes = new SailRepository(new MemoryStore()); + try (SailRepositoryConnection connection = shapes.getConnection()) { + connection.add(new StringReader(shapesTrig), RDFFormat.TURTLE); + } + + SailRepository data = new SailRepository(new MemoryStore()); + try (SailRepositoryConnection connection = data.getConnection()) { + connection.add(new StringReader(dataTtl), RDFFormat.TURTLE); + } + + ValidationReport report = ShaclValidator.validate(data.getSail(), shapes.getSail()); + assertFalse(report.conforms()); + assertEquals(1, report.getValidationResult().size()); + + assertAllResultsHaveMessages(report, + Set.of("Bad path and ")); + } + + @Test + public void pathPlaceholderWorksForInversePaths() throws Exception { + + String shapesTtl = "@prefix ex: .\n" + + "@prefix sh: .\n" + + "\n" + + "ex:PersonShape a sh:NodeShape ;\n" + + " sh:targetClass ex:Person ;\n" + + " sh:property [\n" + + " sh:path [ sh:inversePath ex:knows ] ;\n" + + " sh:sparql [\n" + + " a sh:SPARQLConstraint ;\n" + + " sh:message \"Inverse knows values must be Person.\" ;\n" + + " sh:select \"\"\"\n" + + " PREFIX ex: \n" + + " SELECT $this ?value WHERE {\n" + + " $this $PATH ?value .\n" + + " FILTER NOT EXISTS { ?value a ex:Person }\n" + + " }\n" + + " \"\"\" ;\n" + + " ]\n" + + " ] .\n"; + + String dataTtl = "@prefix ex: .\n" + + "\n" + + "ex:alice a ex:Person .\n" + + "ex:bob ex:knows ex:alice .\n"; + + SailRepository shapes = new SailRepository(new MemoryStore()); + try (SailRepositoryConnection connection = shapes.getConnection()) { + connection.add(new StringReader(shapesTtl), RDFFormat.TURTLE); + } + + SailRepository data = new SailRepository(new MemoryStore()); + try (SailRepositoryConnection connection = data.getConnection()) { + connection.add(new StringReader(dataTtl), RDFFormat.TURTLE); + } + + ValidationReport report = ShaclValidator.validate(data.getSail(), shapes.getSail()); + assertFalse(report.conforms()); + assertEquals(1, report.getValidationResult().size()); + + assertAllResultsHaveMessages(report, Set.of("Inverse knows values must be Person.")); + } + + @Test + public void illegalPathPlaceholderUseCausesFailure() throws Exception { + + String shapesTtl = "@prefix ex: .\n" + + "@prefix sh: .\n" + + "\n" + + "ex:PersonShape a sh:NodeShape ;\n" + + " sh:targetClass ex:Person ;\n" + + " sh:property [\n" + + " sh:path ex:knows ;\n" + + " sh:sparql [\n" + + " a sh:SPARQLConstraint ;\n" + + " sh:select \"\"\"\n" + + " PREFIX ex: \n" + + " SELECT $this WHERE {\n" + + " BIND($PATH AS ?p)\n" + + " FILTER NOT EXISTS { $this ex:name ?n }\n" + + " }\n" + + " \"\"\" ;\n" + + " ]\n" + + " ] .\n"; + + String dataTtl = "@prefix ex: .\n" + + "\n" + + "ex:alice a ex:Person .\n"; + + SailRepository shapes = new SailRepository(new MemoryStore()); + try (SailRepositoryConnection connection = shapes.getConnection()) { + connection.add(new StringReader(shapesTtl), RDFFormat.TURTLE); + } + + SailRepository data = new SailRepository(new MemoryStore()); + try (SailRepositoryConnection connection = data.getConnection()) { + connection.add(new StringReader(dataTtl), RDFFormat.TURTLE); + } + + SailException ex = assertThrows(SailException.class, + () -> ShaclValidator.validate(data.getSail(), shapes.getSail())); + Throwable root = ex; + while (root.getCause() != null) { + root = root.getCause(); + } + assertTrue(root instanceof IllegalStateException); + } + + @Test + public void nonIriPathBindingIgnoredForPropertyShapes() throws Exception { + + String shapesTtl = "@prefix ex: .\n" + + "@prefix sh: .\n" + + "\n" + + "ex:PersonShape a sh:NodeShape ;\n" + + " sh:targetClass ex:Person ;\n" + + " sh:property [\n" + + " sh:path ex:knows ;\n" + + " sh:sparql [\n" + + " a sh:SPARQLConstraint ;\n" + + " sh:message \"knows values must be IRIs\" ;\n" + + " sh:select \"\"\"\n" + + " PREFIX ex: \n" + + " SELECT $this (\"notAnIRI\" AS ?path) ?value WHERE {\n" + + " $this ex:knows ?value .\n" + + " FILTER ( !isIRI(?value) )\n" + + " }\n" + + " \"\"\" ;\n" + + " ]\n" + + " ] .\n"; + + String dataTtl = "@prefix ex: .\n" + + "\n" + + "ex:alice a ex:Person ;\n" + + " ex:knows \"Bob\" .\n"; + + SailRepository shapes = new SailRepository(new MemoryStore()); + try (SailRepositoryConnection connection = shapes.getConnection()) { + connection.add(new StringReader(shapesTtl), RDFFormat.TURTLE); + } + + SailRepository data = new SailRepository(new MemoryStore()); + try (SailRepositoryConnection connection = data.getConnection()) { + connection.add(new StringReader(dataTtl), RDFFormat.TURTLE); + } + + ValidationReport report = ShaclValidator.validate(data.getSail(), shapes.getSail()); + assertFalse(report.conforms()); + assertEquals(1, report.getValidationResult().size()); + + Model model = report.asModel(); + for (ValidationResult result : report.getValidationResult()) { + Set paths = model.filter(result.getId(), SHACL.RESULT_PATH, null).objects(); + assertEquals(1, paths.size()); + Value path = paths.iterator().next(); + assertTrue(path instanceof IRI, "Expected sh:resultPath to be IRI, got " + path); + assertEquals("http://example.com/ns#knows", path.stringValue()); + } + } + + private static void assertResultMessagesMatchConstraintMessages(ValidationReport report) { + Model model = report.asModel(); + for (ValidationResult result : report.getValidationResult()) { + Resource resultId = result.getId(); + + Set resultMessages = toMessageSet(model.filter(resultId, SHACL.RESULT_MESSAGE, null).objects()); + + Set sourceConstraints = StreamSupport.stream( + model.filter(resultId, SHACL.SOURCE_CONSTRAINT, null).objects().spliterator(), false) + .filter(Value::isResource) + .map(v -> (Resource) v) + .collect(Collectors.toSet()); + + assertEquals(1, sourceConstraints.size(), + "Expected exactly one sh:sourceConstraint for result " + resultId); + + Resource sourceConstraint = sourceConstraints.iterator().next(); + Set constraintMessages = toMessageSet( + model.filter(sourceConstraint, SHACL.MESSAGE, null).objects()); + + assertEquals(constraintMessages, resultMessages, + "Result messages did not match constraint messages for result " + resultId); + } + } + + private static void assertAllResultsHaveMessages(ValidationReport report, Set expectedMessages) { + Model model = report.asModel(); + for (ValidationResult result : report.getValidationResult()) { + Set resultMessages = toMessageSet( + model.filter(result.getId(), SHACL.RESULT_MESSAGE, null).objects()); + assertEquals(expectedMessages, resultMessages); + } + } + + private static Set toMessageSet(Iterable values) { + return StreamSupport.stream(values.spliterator(), false) + .filter(Value::isLiteral) + .map(v -> (Literal) v) + .map(l -> l.getLabel() + l.getLanguage().map(lang -> "@" + lang).orElse("")) + .collect(Collectors.toSet()); + } + +} diff --git a/core/sail/shacl/src/test/resources/recursion/deep/data.trig b/core/sail/shacl/src/test/resources/recursion/deep/data.trig new file mode 100644 index 00000000000..81344fba07d --- /dev/null +++ b/core/sail/shacl/src/test/resources/recursion/deep/data.trig @@ -0,0 +1,28 @@ +@prefix ex: . +@prefix rdf: . + +{ + ex:root a ex:Root ; + ex:next ex:node1 ; + ex:friend ex:friend1 ; + ex:xoneA "A" ; + ex:xoneB "B" ; + ex:notFlag "ban" . + + ex:node1 ex:next ex:node2 . + ex:node2 ex:next ex:node3 . + ex:node3 ex:next ex:node4 . + ex:node4 ex:next ex:node5 . + ex:node5 ex:next ex:node6 . + ex:node6 ex:next ex:node7 . + ex:node7 ex:next ex:node8 . + ex:node8 ex:next ex:node9 . + ex:node9 ex:next ex:node10 . + ex:node10 ex:next ex:node11 . + ex:node11 ex:next ex:node12 . + + ex:node12 ex:value "twelve" . + + ex:friend1 rdf:type ex:Friend . + +} diff --git a/core/sail/shacl/src/test/resources/recursion/deep/shacl.trig b/core/sail/shacl/src/test/resources/recursion/deep/shacl.trig new file mode 100644 index 00000000000..da44678e146 --- /dev/null +++ b/core/sail/shacl/src/test/resources/recursion/deep/shacl.trig @@ -0,0 +1,178 @@ +@prefix ex: . +@prefix sh: . +@prefix xsd: . +@prefix rdf: . +@prefix rdf4j: . + +{ + ex:RootShape a sh:NodeShape ; + sh:targetClass ex:Root ; + sh:property ex:RootNextProperty ; + sh:property ex:RootFriendProperty ; + sh:property ex:RootLoopProperty ; + sh:node ex:SelfCheckShape ; + sh:and ( ex:AndShape ) ; + sh:or ( ex:OrShapeA ex:OrShapeB ) ; + sh:xone ( ex:XoneShapeA ex:XoneShapeB ) ; + sh:not ex:NotShape . + + ex:RootNextProperty a sh:PropertyShape ; + sh:path ex:next ; + sh:minCount 1 ; + sh:node ex:ChainShape1 . + + ex:RootFriendProperty a sh:PropertyShape ; + sh:path ex:friend ; + sh:qualifiedValueShape ex:QualifiedShape ; + sh:qualifiedMinCount 1 . + + ex:RootLoopProperty a sh:PropertyShape ; + sh:path ex:loop ; + sh:minCount 1 ; + sh:node ex:RootShape . + + ex:SelfCheckShape a sh:NodeShape ; + sh:property [ + sh:path ex:selfCheck ; + sh:hasValue "ok" + ] . + + ex:AndShape a sh:NodeShape ; + sh:property [ + sh:path ex:andFlag ; + sh:hasValue "ok" + ] . + + ex:OrShapeA a sh:NodeShape ; + sh:property [ + sh:path ex:orA ; + sh:hasValue "A" + ] . + + ex:OrShapeB a sh:NodeShape ; + sh:property [ + sh:path ex:orB ; + sh:hasValue "B" + ] . + + ex:XoneShapeA a sh:NodeShape ; + sh:property [ + sh:path ex:xoneA ; + sh:hasValue "A" + ] . + + ex:XoneShapeB a sh:NodeShape ; + sh:property [ + sh:path ex:xoneB ; + sh:hasValue "B" + ] . + + ex:NotShape a sh:NodeShape ; + sh:property [ + sh:path ex:notFlag ; + sh:hasValue "ban" + ] . + + ex:QualifiedShape a sh:NodeShape ; + sh:property [ + sh:path ex:friendFlag ; + sh:hasValue "friend" + ] . + + ex:ChainShape1 a sh:NodeShape ; + sh:property ex:ChainShape1Property . + + ex:ChainShape1Property a sh:PropertyShape ; + sh:path ex:next ; + sh:minCount 1 ; + sh:node ex:ChainShape2 . + + ex:ChainShape2 a sh:NodeShape ; + sh:property ex:ChainShape2Property . + + ex:ChainShape2Property a sh:PropertyShape ; + sh:path ex:next ; + sh:minCount 1 ; + sh:node ex:ChainShape3 . + + ex:ChainShape3 a sh:NodeShape ; + sh:property ex:ChainShape3Property . + + ex:ChainShape3Property a sh:PropertyShape ; + sh:path ex:next ; + sh:minCount 1 ; + sh:node ex:ChainShape4 . + + ex:ChainShape4 a sh:NodeShape ; + sh:property ex:ChainShape4Property . + + ex:ChainShape4Property a sh:PropertyShape ; + sh:path ex:next ; + sh:minCount 1 ; + sh:node ex:ChainShape5 . + + ex:ChainShape5 a sh:NodeShape ; + sh:property ex:ChainShape5Property . + + ex:ChainShape5Property a sh:PropertyShape ; + sh:path ex:next ; + sh:minCount 1 ; + sh:node ex:ChainShape6 . + + ex:ChainShape6 a sh:NodeShape ; + sh:property ex:ChainShape6Property . + + ex:ChainShape6Property a sh:PropertyShape ; + sh:path ex:next ; + sh:minCount 1 ; + sh:node ex:ChainShape7 . + + ex:ChainShape7 a sh:NodeShape ; + sh:property ex:ChainShape7Property . + + ex:ChainShape7Property a sh:PropertyShape ; + sh:path ex:next ; + sh:minCount 1 ; + sh:node ex:ChainShape8 . + + ex:ChainShape8 a sh:NodeShape ; + sh:property ex:ChainShape8Property . + + ex:ChainShape8Property a sh:PropertyShape ; + sh:path ex:next ; + sh:minCount 1 ; + sh:node ex:ChainShape9 . + + ex:ChainShape9 a sh:NodeShape ; + sh:property ex:ChainShape9Property . + + ex:ChainShape9Property a sh:PropertyShape ; + sh:path ex:next ; + sh:minCount 1 ; + sh:node ex:ChainShape10 . + + ex:ChainShape10 a sh:NodeShape ; + sh:property ex:ChainShape10Property . + + ex:ChainShape10Property a sh:PropertyShape ; + sh:path ex:next ; + sh:minCount 1 ; + sh:node ex:ChainShape11 . + + ex:ChainShape11 a sh:NodeShape ; + sh:property ex:ChainShape11Property . + + ex:ChainShape11Property a sh:PropertyShape ; + sh:path ex:next ; + sh:minCount 1 ; + sh:node ex:ChainShape12 . + + ex:ChainShape12 a sh:NodeShape ; + sh:property ex:ChainShape12Property . + + ex:ChainShape12Property a sh:PropertyShape ; + sh:path ex:value ; + sh:datatype xsd:integer . + + rdf4j:nil sh:shapesGraph rdf4j:nil . +} diff --git a/core/sail/shacl/src/test/resources/recursion/logical/data.trig b/core/sail/shacl/src/test/resources/recursion/logical/data.trig new file mode 100644 index 00000000000..828c7448b14 --- /dev/null +++ b/core/sail/shacl/src/test/resources/recursion/logical/data.trig @@ -0,0 +1,6 @@ +@prefix ex: . +@prefix rdf: . + +{ + ex:placeholder rdf:type ex:Placeholder . +} diff --git a/core/sail/shacl/src/test/resources/recursion/logical/not/data.trig b/core/sail/shacl/src/test/resources/recursion/logical/not/data.trig new file mode 100644 index 00000000000..918036ec3dc --- /dev/null +++ b/core/sail/shacl/src/test/resources/recursion/logical/not/data.trig @@ -0,0 +1,6 @@ +@prefix ex: . +@prefix rdf: . + +{ + ex:n1 rdf:type ex:Node . +} diff --git a/core/sail/shacl/src/test/resources/recursion/logical/not/shacl.trig b/core/sail/shacl/src/test/resources/recursion/logical/not/shacl.trig new file mode 100644 index 00000000000..fd548b1a592 --- /dev/null +++ b/core/sail/shacl/src/test/resources/recursion/logical/not/shacl.trig @@ -0,0 +1,11 @@ +@prefix ex: . +@prefix sh: . +@prefix rdf4j: . + +{ + ex:RootShape a sh:NodeShape ; + sh:targetClass ex:Node ; + sh:not ex:RootShape . + + rdf4j:nil sh:shapesGraph rdf4j:nil . +} diff --git a/core/sail/shacl/src/test/resources/recursion/logical/or/data.trig b/core/sail/shacl/src/test/resources/recursion/logical/or/data.trig new file mode 100644 index 00000000000..918036ec3dc --- /dev/null +++ b/core/sail/shacl/src/test/resources/recursion/logical/or/data.trig @@ -0,0 +1,6 @@ +@prefix ex: . +@prefix rdf: . + +{ + ex:n1 rdf:type ex:Node . +} diff --git a/core/sail/shacl/src/test/resources/recursion/logical/or/shacl.trig b/core/sail/shacl/src/test/resources/recursion/logical/or/shacl.trig new file mode 100644 index 00000000000..c66869e42c1 --- /dev/null +++ b/core/sail/shacl/src/test/resources/recursion/logical/or/shacl.trig @@ -0,0 +1,14 @@ +@prefix ex: . +@prefix sh: . +@prefix rdf4j: . + +{ + ex:RootShape a sh:NodeShape ; + sh:targetClass ex:Node ; + sh:or ( ex:RootShape ex:OtherShape ) . + + ex:OtherShape a sh:NodeShape ; + sh:property [ sh:path ex:p ; sh:minCount 0 ] . + + rdf4j:nil sh:shapesGraph rdf4j:nil . +} diff --git a/core/sail/shacl/src/test/resources/recursion/logical/shacl.trig b/core/sail/shacl/src/test/resources/recursion/logical/shacl.trig new file mode 100644 index 00000000000..73d15e74d7c --- /dev/null +++ b/core/sail/shacl/src/test/resources/recursion/logical/shacl.trig @@ -0,0 +1,7 @@ +@prefix ex: . +@prefix sh: . + +{ + ex:PlaceholderShape a sh:NodeShape ; + sh:targetClass ex:Placeholder . +} diff --git a/core/sail/shacl/src/test/resources/recursion/node/selfNode/data.trig b/core/sail/shacl/src/test/resources/recursion/node/selfNode/data.trig new file mode 100644 index 00000000000..9977cf1ee67 --- /dev/null +++ b/core/sail/shacl/src/test/resources/recursion/node/selfNode/data.trig @@ -0,0 +1,7 @@ +@prefix ex: . +@prefix rdf: . + +{ + ex:n1 rdf:type ex:Node . +} + diff --git a/core/sail/shacl/src/test/resources/recursion/node/selfNode/shacl.trig b/core/sail/shacl/src/test/resources/recursion/node/selfNode/shacl.trig new file mode 100644 index 00000000000..93d3372b378 --- /dev/null +++ b/core/sail/shacl/src/test/resources/recursion/node/selfNode/shacl.trig @@ -0,0 +1,12 @@ +@prefix ex: . +@prefix sh: . +@prefix rdf4j: . + +{ + ex:RootShape a sh:NodeShape ; + sh:targetClass ex:Node ; + sh:node ex:RootShape . + + rdf4j:nil sh:shapesGraph rdf4j:nil . +} + diff --git a/core/sail/shacl/src/test/resources/recursion/property/selfNodeOnly/data.trig b/core/sail/shacl/src/test/resources/recursion/property/selfNodeOnly/data.trig new file mode 100644 index 00000000000..6bfa177412a --- /dev/null +++ b/core/sail/shacl/src/test/resources/recursion/property/selfNodeOnly/data.trig @@ -0,0 +1,8 @@ +@prefix ex: . +@prefix rdf: . + +{ + ex:n1 rdf:type ex:Node ; + ex:p ex:n1 . +} + diff --git a/core/sail/shacl/src/test/resources/recursion/property/selfNodeOnly/shacl.trig b/core/sail/shacl/src/test/resources/recursion/property/selfNodeOnly/shacl.trig new file mode 100644 index 00000000000..1df7574e95c --- /dev/null +++ b/core/sail/shacl/src/test/resources/recursion/property/selfNodeOnly/shacl.trig @@ -0,0 +1,16 @@ +@prefix ex: . +@prefix sh: . +@prefix rdf4j: . + +{ + ex:RootShape a sh:NodeShape ; + sh:targetClass ex:Node ; + sh:property ex:RecursiveProperty . + + ex:RecursiveProperty a sh:PropertyShape ; + sh:path ex:p ; + sh:node ex:RootShape . + + rdf4j:nil sh:shapesGraph rdf4j:nil . +} + diff --git a/core/sail/solr/pom.xml b/core/sail/solr/pom.xml index b51f3741778..d7259a5a28f 100644 --- a/core/sail/solr/pom.xml +++ b/core/sail/solr/pom.xml @@ -4,7 +4,7 @@ org.eclipse.rdf4j rdf4j-sail - 5.2.2-SNAPSHOT + 5.2.3-SNAPSHOT rdf4j-sail-solr RDF4J: Solr Sail Index diff --git a/core/sparqlbuilder/pom.xml b/core/sparqlbuilder/pom.xml index 37df81a166c..7328aa4af15 100644 --- a/core/sparqlbuilder/pom.xml +++ b/core/sparqlbuilder/pom.xml @@ -4,7 +4,7 @@ org.eclipse.rdf4j rdf4j-core - 5.2.2-SNAPSHOT + 5.2.3-SNAPSHOT rdf4j-sparqlbuilder RDF4J: SparqlBuilder diff --git a/core/spin/pom.xml b/core/spin/pom.xml index d5b1ce977e8..ac9ade8683f 100644 --- a/core/spin/pom.xml +++ b/core/spin/pom.xml @@ -4,7 +4,7 @@ org.eclipse.rdf4j rdf4j-core - 5.2.2-SNAPSHOT + 5.2.3-SNAPSHOT rdf4j-spin RDF4J: SPIN diff --git a/core/storage/pom.xml b/core/storage/pom.xml index c598f022e73..bf85ad46c8d 100644 --- a/core/storage/pom.xml +++ b/core/storage/pom.xml @@ -4,7 +4,7 @@ org.eclipse.rdf4j rdf4j-core - 5.2.2-SNAPSHOT + 5.2.3-SNAPSHOT rdf4j-storage RDF4J: Storage Libraries diff --git a/examples/pom.xml b/examples/pom.xml index 6e2849a7e1d..f97d064ae51 100644 --- a/examples/pom.xml +++ b/examples/pom.xml @@ -7,7 +7,7 @@ org.eclipse.rdf4j rdf4j - 5.2.2-SNAPSHOT + 5.2.3-SNAPSHOT diff --git a/pom.xml b/pom.xml index 45d8a39175c..8a1c0ac453f 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ 4.0.0 org.eclipse.rdf4j rdf4j - 5.2.2-SNAPSHOT + 5.2.3-SNAPSHOT pom Eclipse RDF4J An extensible Java framework for RDF and SPARQL diff --git a/site/content/download.md b/site/content/download.md index 4c1a39b7e30..badc4607c23 100644 --- a/site/content/download.md +++ b/site/content/download.md @@ -5,15 +5,15 @@ toc: true You can either retrieve RDF4J via Apache Maven, or download the SDK or onejar directly. -## RDF4J 5.2.1 (latest) +## RDF4J 5.2.2 (latest) -RDF4J 5.2.1 is our latest stable release. It requires Java 11 minimally. -For details on what’s new and how to upgrade, see the [release and upgrade notes](/release-notes/5.2.1). +RDF4J 5.2.2 is our latest stable release. It requires Java 11 minimally. +For details on what’s new and how to upgrade, see the [release and upgrade notes](/release-notes/5.2.2). -- [RDF4J 5.2.1 SDK (zip)](http://www.eclipse.org/downloads/download.php?file=/rdf4j/eclipse-rdf4j-5.2.1-sdk.zip)
+- [RDF4J 5.2.2 SDK (zip)](http://www.eclipse.org/downloads/download.php?file=/rdf4j/eclipse-rdf4j-5.2.2-sdk.zip)
Full Eclipse RDF4J SDK, containing all libraries, RDF4J Server, Workbench, and Console applications, and Javadoc API. -- [RDF4J 5.2.1 onejar](http://www.eclipse.org/downloads/download.php?file=/rdf4j/eclipse-rdf4j-5.2.1-onejar.jar)
+- [RDF4J 5.2.2 onejar](http://www.eclipse.org/downloads/download.php?file=/rdf4j/eclipse-rdf4j-5.2.2-onejar.jar)
Single jar file for easy inclusion of the full RDF4J toolkit in your Java project. - [RDF4J artifacts](https://search.maven.org/search?q=org.eclipse.rdf4j) on the [Maven Central Repository](http://search.maven.org/) @@ -28,7 +28,7 @@ You can include RDF4J as a Maven dependency in your Java project by including th org.eclipse.rdf4j rdf4j-bom - 5.2.1 + 5.2.2 pom import diff --git a/site/content/news/rdf4j-522.md b/site/content/news/rdf4j-522.md new file mode 100644 index 00000000000..9e6ae071e3f --- /dev/null +++ b/site/content/news/rdf4j-522.md @@ -0,0 +1,14 @@ +--- +title: "RDF4J 5.2.2 released" +date: 2025-12-15T15:42:53+0100 +layout: "single" +categories: ["news"] +--- +RDF4J 5.2.2 is now available. This is a patch release fixing 3 bugs. + +For more details, have a look at the [release notes](/release-notes/5.2.2). + +### Links + +- [Download RDF4J](/download/) +- [release notes](/release-notes/5.2.2). diff --git a/site/content/release-notes/5.2.2.md b/site/content/release-notes/5.2.2.md new file mode 100644 index 00000000000..1d2ed5da253 --- /dev/null +++ b/site/content/release-notes/5.2.2.md @@ -0,0 +1,11 @@ +--- +title: "5.2.2" +toc: true +--- +RDF4J 5.2.2 is a patch release that fixes 3 issues. + +For a complete overview, see [all issues fixed in 5.2.2](https://github.com/eclipse/rdf4j/milestone/126?closed=1). + +### Acknowledgements + +This release was made possible by contributions from [Piotr Sowiński](https://github.com/Ostrzyciel) and [Håvard M. Ottestad](https://github.com/hmottestad). diff --git a/site/static/404.html b/site/static/404.html new file mode 100644 index 00000000000..40cb6c70f11 --- /dev/null +++ b/site/static/404.html @@ -0,0 +1,39 @@ + + + + + Page not found | RDF4J + + + + +
+

Page not found

+

We could not find the page you were looking for.

+

If you followed a link to a specific Javadoc version, we will try to forward you to the closest available version automatically.

+
+ + + diff --git a/site/static/javadoc/5.2.2.tar.xz b/site/static/javadoc/5.2.2.tar.xz new file mode 100644 index 00000000000..5a07b987c8d Binary files /dev/null and b/site/static/javadoc/5.2.2.tar.xz differ diff --git a/site/static/javadoc/latest.tar.xz b/site/static/javadoc/latest.tar.xz index 42c881e3e6d..5a07b987c8d 100644 Binary files a/site/static/javadoc/latest.tar.xz and b/site/static/javadoc/latest.tar.xz differ diff --git a/site/static/js/javadoc-redirect.js b/site/static/js/javadoc-redirect.js new file mode 100644 index 00000000000..dd5c1599a3c --- /dev/null +++ b/site/static/js/javadoc-redirect.js @@ -0,0 +1,149 @@ +function parseSemver(version) { + if (typeof version !== 'string') { + return null; + } + + const parts = version.match(/\d+/g); + if (!parts || parts.length === 0) { + return null; + } + + return parts.map((part) => Number.parseInt(part, 10)); +} + +function compareParts(aParts, bParts) { + const maxLength = Math.max(aParts.length, bParts.length); + for (let i = 0; i < maxLength; i += 1) { + const a = aParts[i] ?? 0; + const b = bParts[i] ?? 0; + if (a !== b) { + return a - b; + } + } + return 0; +} + +function compareSemver(a, b) { + const parsedA = parseSemver(a); + const parsedB = parseSemver(b); + + if (!parsedA || !parsedB) { + return 0; + } + + return compareParts(parsedA, parsedB); +} + +function findClosestVersion(requestedVersion, availableVersions) { + const requestedParts = parseSemver(requestedVersion); + if (!requestedParts) { + return null; + } + + const parsedVersions = availableVersions + .map((version) => ({ version, parts: parseSemver(version) })) + .filter((entry) => entry.parts) + .sort((a, b) => compareParts(a.parts, b.parts)); + + let lower = null; + for (const entry of parsedVersions) { + const comparison = compareParts(entry.parts, requestedParts); + if (comparison === 0) { + return entry.version; + } + if (comparison > 0) { + return entry.version; + } + lower = entry.version; + } + + return lower; +} + +function isJavadocPath(pathname) { + return typeof pathname === 'string' && pathname.startsWith('/javadoc/'); +} + +export function findRedirectPath(pathname, availableVersions) { + if (!isJavadocPath(pathname)) { + return null; + } + + const segments = pathname.split('/'); + // ['', 'javadoc', '', ...] + const requestedVersion = segments[2]; + if (!requestedVersion) { + return null; + } + + const targetVersion = findClosestVersion(requestedVersion, availableVersions ?? []); + if (!targetVersion || targetVersion === requestedVersion) { + return null; + } + + const nextSegments = segments.slice(); + nextSegments[2] = targetVersion; + + return nextSegments.join('/') || '/'; +} + +export function extractVersions(manifestEntries) { + if (!Array.isArray(manifestEntries)) { + return []; + } + + return manifestEntries + .map((entry) => (typeof entry === 'string' ? entry : entry?.name)) + .filter((value) => typeof value === 'string') + .filter((value) => parseSemver(value)); +} + +export async function attemptJavadocRedirect(manifestUrl = '/javadoc/manifest.json') { + if (typeof window === 'undefined' || typeof window.location === 'undefined') { + return null; + } + + const { pathname, search, hash } = window.location; + if (!isJavadocPath(pathname)) { + return null; + } + + let versions = []; + try { + const response = await fetch(manifestUrl, { cache: 'no-store' }); + if (!response.ok) { + throw new Error(`Unexpected response ${response.status}`); + } + const manifest = await response.json(); + versions = extractVersions(manifest); + } catch (error) { + console.warn('Unable to load javadoc manifest for redirect:', error); + return null; + } + + const targetPath = findRedirectPath(pathname, versions); + if (!targetPath) { + return null; + } + + const nextUrl = new URL(window.location.href); + nextUrl.pathname = targetPath; + nextUrl.search = search; + nextUrl.hash = hash; + window.location.replace(nextUrl.toString()); + return nextUrl.toString(); +} + +if (typeof window !== 'undefined') { + window.JavadocRedirect = { + attemptJavadocRedirect, + extractVersions, + findRedirectPath, + }; +} + +export default { + attemptJavadocRedirect, + extractVersions, + findRedirectPath, +}; diff --git a/site/static/js/javadoc-redirect.test.js b/site/static/js/javadoc-redirect.test.js new file mode 100644 index 00000000000..ffc716e33d6 --- /dev/null +++ b/site/static/js/javadoc-redirect.test.js @@ -0,0 +1,25 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { findRedirectPath } from './javadoc-redirect.js'; + +const versions = ['5.1.0', '5.1.2', '5.2.0', '4.3.16']; + +test('redirects to the next patch version when available', () => { + const result = findRedirectPath('/javadoc/5.1.1/org/example/Foo.html', versions); + assert.equal(result, '/javadoc/5.1.2/org/example/Foo.html'); +}); + +test('redirects to the next minor or major version when patch is missing', () => { + const result = findRedirectPath('/javadoc/5.1.2/org/example/Foo.html', ['5.1.0', '5.2.0']); + assert.equal(result, '/javadoc/5.2.0/org/example/Foo.html'); +}); + +test('falls back to the highest available lower version when no newer version exists', () => { + const result = findRedirectPath('/javadoc/9.0.0/org/example/Foo.html', versions); + assert.equal(result, '/javadoc/5.2.0/org/example/Foo.html'); +}); + +test('ignores non-semver entries when calculating redirects', () => { + const result = findRedirectPath('/javadoc/5.1.1/org/example/Foo.html', ['latest', '5.2.0']); + assert.equal(result, '/javadoc/5.2.0/org/example/Foo.html'); +}); diff --git a/spring-components/pom.xml b/spring-components/pom.xml index 65f3333e02c..b5b257734d0 100644 --- a/spring-components/pom.xml +++ b/spring-components/pom.xml @@ -4,7 +4,7 @@ org.eclipse.rdf4j rdf4j - 5.2.2-SNAPSHOT + 5.2.3-SNAPSHOT pom diff --git a/spring-components/rdf4j-spring-demo/pom.xml b/spring-components/rdf4j-spring-demo/pom.xml index 7f4c40f81af..5809f54c309 100644 --- a/spring-components/rdf4j-spring-demo/pom.xml +++ b/spring-components/rdf4j-spring-demo/pom.xml @@ -7,7 +7,7 @@ org.eclipse.rdf4j rdf4j-spring-components - 5.2.2-SNAPSHOT + 5.2.3-SNAPSHOT diff --git a/spring-components/rdf4j-spring/pom.xml b/spring-components/rdf4j-spring/pom.xml index fa69f1a93e2..6087177c9e8 100644 --- a/spring-components/rdf4j-spring/pom.xml +++ b/spring-components/rdf4j-spring/pom.xml @@ -4,7 +4,7 @@ org.eclipse.rdf4j rdf4j-spring-components - 5.2.2-SNAPSHOT + 5.2.3-SNAPSHOT rdf4j-spring RDF4J: Spring diff --git a/spring-components/spring-boot-sparql-web/pom.xml b/spring-components/spring-boot-sparql-web/pom.xml index 12b123c1a27..40269e277c2 100644 --- a/spring-components/spring-boot-sparql-web/pom.xml +++ b/spring-components/spring-boot-sparql-web/pom.xml @@ -4,7 +4,7 @@ org.eclipse.rdf4j rdf4j-spring-components - 5.2.2-SNAPSHOT + 5.2.3-SNAPSHOT rdf4j-spring-boot-sparql-web RDF4J: Spring boot component for a HTTP sparql server diff --git a/testsuites/benchmark/pom.xml b/testsuites/benchmark/pom.xml index 12d31b8b77f..abb9d8a0f15 100644 --- a/testsuites/benchmark/pom.xml +++ b/testsuites/benchmark/pom.xml @@ -4,7 +4,7 @@ org.eclipse.rdf4j rdf4j-testsuites - 5.2.2-SNAPSHOT + 5.2.3-SNAPSHOT rdf4j-benchmark RDF4J: benchmarks diff --git a/testsuites/geosparql/pom.xml b/testsuites/geosparql/pom.xml index 8b859648fb1..e436f89c7a5 100644 --- a/testsuites/geosparql/pom.xml +++ b/testsuites/geosparql/pom.xml @@ -4,7 +4,7 @@ org.eclipse.rdf4j rdf4j-testsuites - 5.2.2-SNAPSHOT + 5.2.3-SNAPSHOT rdf4j-geosparql-testsuite RDF4J: GeoSPARQL compliance test suite diff --git a/testsuites/lucene/pom.xml b/testsuites/lucene/pom.xml index 51567f98e2b..750edd2750b 100644 --- a/testsuites/lucene/pom.xml +++ b/testsuites/lucene/pom.xml @@ -4,7 +4,7 @@ org.eclipse.rdf4j rdf4j-testsuites - 5.2.2-SNAPSHOT + 5.2.3-SNAPSHOT rdf4j-lucene-testsuite RDF4J: Lucene Sail Tests diff --git a/testsuites/model/pom.xml b/testsuites/model/pom.xml index 1be3cac975b..6a8e020d62e 100644 --- a/testsuites/model/pom.xml +++ b/testsuites/model/pom.xml @@ -4,7 +4,7 @@ org.eclipse.rdf4j rdf4j-testsuites - 5.2.2-SNAPSHOT + 5.2.3-SNAPSHOT rdf4j-model-testsuite RDF4J: Model API testsuite diff --git a/testsuites/pom.xml b/testsuites/pom.xml index 41a295daa05..7cdc50764cb 100644 --- a/testsuites/pom.xml +++ b/testsuites/pom.xml @@ -4,7 +4,7 @@ org.eclipse.rdf4j rdf4j - 5.2.2-SNAPSHOT + 5.2.3-SNAPSHOT rdf4j-testsuites pom diff --git a/testsuites/queryresultio/pom.xml b/testsuites/queryresultio/pom.xml index 9020b8a41ad..e818855f7bd 100644 --- a/testsuites/queryresultio/pom.xml +++ b/testsuites/queryresultio/pom.xml @@ -4,7 +4,7 @@ org.eclipse.rdf4j rdf4j-testsuites - 5.2.2-SNAPSHOT + 5.2.3-SNAPSHOT rdf4j-queryresultio-testsuite RDF4J: QueryResultIO testsuite diff --git a/testsuites/repository/pom.xml b/testsuites/repository/pom.xml index 46f0944ae65..df70e36a8c1 100644 --- a/testsuites/repository/pom.xml +++ b/testsuites/repository/pom.xml @@ -4,7 +4,7 @@ org.eclipse.rdf4j rdf4j-testsuites - 5.2.2-SNAPSHOT + 5.2.3-SNAPSHOT rdf4j-repository-testsuite RDF4J: Repository API testsuite diff --git a/testsuites/rio/pom.xml b/testsuites/rio/pom.xml index 900e7eb651c..536d5f73286 100644 --- a/testsuites/rio/pom.xml +++ b/testsuites/rio/pom.xml @@ -4,7 +4,7 @@ org.eclipse.rdf4j rdf4j-testsuites - 5.2.2-SNAPSHOT + 5.2.3-SNAPSHOT rdf4j-rio-testsuite RDF4J: Rio compliance test suite diff --git a/testsuites/sail/pom.xml b/testsuites/sail/pom.xml index 004dd5ce306..6123f19e394 100644 --- a/testsuites/sail/pom.xml +++ b/testsuites/sail/pom.xml @@ -4,7 +4,7 @@ org.eclipse.rdf4j rdf4j-testsuites - 5.2.2-SNAPSHOT + 5.2.3-SNAPSHOT rdf4j-sail-testsuite RDF4J: Sail API testsuite diff --git a/testsuites/sparql/pom.xml b/testsuites/sparql/pom.xml index 0bd22460c2c..04033abed01 100644 --- a/testsuites/sparql/pom.xml +++ b/testsuites/sparql/pom.xml @@ -4,7 +4,7 @@ org.eclipse.rdf4j rdf4j-testsuites - 5.2.2-SNAPSHOT + 5.2.3-SNAPSHOT rdf4j-sparql-testsuite RDF4J: SPARQL compliance test suite diff --git a/tools/config/pom.xml b/tools/config/pom.xml index 8ab7be811c7..034b322135f 100644 --- a/tools/config/pom.xml +++ b/tools/config/pom.xml @@ -4,7 +4,7 @@ org.eclipse.rdf4j rdf4j-tools - 5.2.2-SNAPSHOT + 5.2.3-SNAPSHOT rdf4j-config RDF4J: application configuration diff --git a/tools/console/pom.xml b/tools/console/pom.xml index df977557997..ff8eaa6e40a 100644 --- a/tools/console/pom.xml +++ b/tools/console/pom.xml @@ -4,7 +4,7 @@ org.eclipse.rdf4j rdf4j-tools - 5.2.2-SNAPSHOT + 5.2.3-SNAPSHOT rdf4j-console RDF4J: Console diff --git a/tools/federation/pom.xml b/tools/federation/pom.xml index 9bdbbc5726a..ebca36cf30e 100644 --- a/tools/federation/pom.xml +++ b/tools/federation/pom.xml @@ -8,7 +8,7 @@ org.eclipse.rdf4j rdf4j-tools - 5.2.2-SNAPSHOT + 5.2.3-SNAPSHOT diff --git a/tools/pom.xml b/tools/pom.xml index 2a6fce8e9bd..4572cf106bb 100644 --- a/tools/pom.xml +++ b/tools/pom.xml @@ -4,7 +4,7 @@ org.eclipse.rdf4j rdf4j - 5.2.2-SNAPSHOT + 5.2.3-SNAPSHOT rdf4j-tools pom diff --git a/tools/runtime-osgi/pom.xml b/tools/runtime-osgi/pom.xml index f3749d957f1..26fb0b6c6fb 100644 --- a/tools/runtime-osgi/pom.xml +++ b/tools/runtime-osgi/pom.xml @@ -4,7 +4,7 @@ org.eclipse.rdf4j rdf4j-tools - 5.2.2-SNAPSHOT + 5.2.3-SNAPSHOT rdf4j-runtime-osgi bundle diff --git a/tools/runtime/pom.xml b/tools/runtime/pom.xml index 84a0240226f..f285edfc62c 100644 --- a/tools/runtime/pom.xml +++ b/tools/runtime/pom.xml @@ -4,7 +4,7 @@ org.eclipse.rdf4j rdf4j-tools - 5.2.2-SNAPSHOT + 5.2.3-SNAPSHOT rdf4j-runtime RDF4J: Runtime diff --git a/tools/server-spring/pom.xml b/tools/server-spring/pom.xml index aa290b78a64..e4dcfede31d 100644 --- a/tools/server-spring/pom.xml +++ b/tools/server-spring/pom.xml @@ -4,7 +4,7 @@ org.eclipse.rdf4j rdf4j-tools - 5.2.2-SNAPSHOT + 5.2.3-SNAPSHOT rdf4j-http-server-spring RDF4J: HTTP server - core diff --git a/tools/server/pom.xml b/tools/server/pom.xml index d5e8e3ad407..89745662725 100644 --- a/tools/server/pom.xml +++ b/tools/server/pom.xml @@ -4,7 +4,7 @@ org.eclipse.rdf4j rdf4j-tools - 5.2.2-SNAPSHOT + 5.2.3-SNAPSHOT rdf4j-http-server war diff --git a/tools/workbench/pom.xml b/tools/workbench/pom.xml index 27cd9b07178..831b7581ea1 100644 --- a/tools/workbench/pom.xml +++ b/tools/workbench/pom.xml @@ -4,7 +4,7 @@ org.eclipse.rdf4j rdf4j-tools - 5.2.2-SNAPSHOT + 5.2.3-SNAPSHOT rdf4j-http-workbench war