From 103e236e9a35955b290272fe38f5597ccbcef89d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ha=CC=8Avard=20Ottestad?= Date: Wed, 24 Dec 2025 08:39:41 +0100 Subject: [PATCH 1/2] GH-5646 add fluent api to ShaclValidator --- .codex/skills/debug-surefire/SKILL.md | 26 + .../debug-surefire/scripts/debug-surefire.sh | 183 ++ .codex/skills/mvnf/SKILL.md | 32 + .codex/skills/mvnf/scripts/mvnf.py | 325 ++++ AGENTS.md | 122 +- .../rdf4j/rio/jsonld/JSONLDWriterTest.java | 3 - .../rdf4j/sail/shacl/ShaclValidator.java | 1651 ++++++++++++++++- .../rdf4j/sail/shacl/AbstractShaclTest.java | 48 +- .../ShaclTestWithoutRdfsReasonerTest.java | 6 +- .../shacl/ShaclValidatorFluentApiTest.java | 1573 ++++++++++++++++ .../rdf4j/sail/shacl/W3cComplianceTest.java | 8 +- .../results/ShaclValidatorNamedGraphTest.java | 30 +- .../ShaclValidatorSparqlMessagesTest.java | 85 +- .../shacl/results/ShaclValidatorTest.java | 22 +- .../test/resources/junit-platform.properties | 2 +- dist/debug-surefire.skill | Bin 0 -> 3395 bytes dist/mvnf.skill | Bin 0 -> 3981 bytes pom.xml | 21 +- .../documentation/programming/shacl.md | 54 +- 19 files changed, 4007 insertions(+), 184 deletions(-) create mode 100644 .codex/skills/debug-surefire/SKILL.md create mode 100755 .codex/skills/debug-surefire/scripts/debug-surefire.sh create mode 100644 .codex/skills/mvnf/SKILL.md create mode 100755 .codex/skills/mvnf/scripts/mvnf.py create mode 100644 core/sail/shacl/src/test/java/org/eclipse/rdf4j/sail/shacl/ShaclValidatorFluentApiTest.java create mode 100644 dist/debug-surefire.skill create mode 100644 dist/mvnf.skill diff --git a/.codex/skills/debug-surefire/SKILL.md b/.codex/skills/debug-surefire/SKILL.md new file mode 100644 index 00000000000..4c25c72e373 --- /dev/null +++ b/.codex/skills/debug-surefire/SKILL.md @@ -0,0 +1,26 @@ +--- +name: debug-surefire +description: Debug Maven Surefire unit tests by running them in JDWP "wait for debugger" mode (`-Dmaven.surefire.debug`) and attaching IntelliJ/VS Code/jdb. Use when asked to debug/step through a failing JUnit test, attach a debugger to a Maven test run, or run `mvn test -Dtest=Class[#method]` suspended on a port (including multi-module `-pl` runs). +--- + +# debug-surefire + +Run Maven Surefire tests suspended in JDWP so you can attach a debugger and step through the code. + +## Quick start + +- Debug a test class: + - `.codex/skills/debug-surefire/scripts/debug-surefire.sh --test-class MyTest` +- Debug a single test method (quote the `#`): + - `.codex/skills/debug-surefire/scripts/debug-surefire.sh --test 'MyTest#shouldDoThing'` +- Debug a test in a specific module: + - `.codex/skills/debug-surefire/scripts/debug-surefire.sh --module core/sail/shacl --test-class ShaclSailTest` + - `.codex/skills/debug-surefire/scripts/debug-surefire.sh --module rdf4j-sail-shacl --test 'ShaclSailTest#testSomething'` + +## Notes + +- The script runs a fast pre-test install (`-Pquick` into the repo-local `.m2_repo`) and then runs `mvn test` with Surefire in debug mode. +- Use `SUREFIRE_DEBUG_PORT=8000` to change the port (default: `55005`). +- Use `--no-offline` / `--online` if offline (`-o`) resolution fails. +- Everything after `--` is passed to Maven, e.g.: + - `.codex/skills/debug-surefire/scripts/debug-surefire.sh --test-class MyTest -- -DtrimStackTrace=false -DfailIfNoTests=false -DforkCount=1 -DreuseForks=false` diff --git a/.codex/skills/debug-surefire/scripts/debug-surefire.sh b/.codex/skills/debug-surefire/scripts/debug-surefire.sh new file mode 100755 index 00000000000..87397323376 --- /dev/null +++ b/.codex/skills/debug-surefire/scripts/debug-surefire.sh @@ -0,0 +1,183 @@ +#!/usr/bin/env bash +set -euo pipefail + +# debug-surefire.sh +# +# Run Maven Surefire tests in "wait for debugger" mode (JDWP). +# +# Optional inputs: +# --module Reactor project selector passed to "mvn -pl". +# Convenience: if you pass just an artifactId (no ':' or '/'), +# the script prefixes it with ':' (e.g., "foo" -> ":foo"). +# --test-class Runs a single test class (Surefire -Dtest=...). +# --test Runs a single test method/pattern (Surefire -Dtest=Class#method). +# IMPORTANT: quote values containing '#', e.g. --test 'MyTest#myMethod' +# --skip-install Skip the pre-test quick install step. +# --no-offline | --online Run Maven without "-o" (useful if offline resolution fails). +# +# Extras: +# Everything after "--" is passed through to Maven unchanged. +# +# Environment: +# SUREFIRE_DEBUG_PORT If set (e.g. 8000), uses that port instead of 55005. +# Binds to localhost for safety. + +usage() { + cat <<'USAGE' +Usage: + debug-surefire.sh [--module ] [--test-class ] [--test ] [--skip-install] [--no-offline|--online] [-- ] + +Examples: + # Debug a single test class (current module / reactor defaults) + ./debug-surefire.sh --test-class MyTest + + # Debug a single test method (quote the '#') + ./debug-surefire.sh --test 'MyTest#shouldDoThing' + + # Debug a test in one module of a multi-module build (artifactId shorthand) + ./debug-surefire.sh --module my-module --test-class MyTest + + # Pass through extra Maven args + ./debug-surefire.sh --test-class MyTest -- -DtrimStackTrace=false -DfailIfNoTests=false + + # Use a different debug port + SUREFIRE_DEBUG_PORT=8000 ./debug-surefire.sh --test-class MyTest +USAGE +} + +MODULE="" +TEST_CLASS="" +TEST_TARGET="" +SKIP_INSTALL="0" +OFFLINE="1" +PASSTHRU=() + +while [[ $# -gt 0 ]]; do + case "$1" in + -m|--module) + MODULE="${2:-}"; shift 2 ;; + -c|--class|--test-class) + TEST_CLASS="${2:-}"; shift 2 ;; + -t|--test|--test-target) + TEST_TARGET="${2:-}"; shift 2 ;; + --skip-install) + SKIP_INSTALL="1"; shift ;; + --no-offline|--online) + OFFLINE="0"; shift ;; + -h|--help) + usage; exit 0 ;; + --) + shift + PASSTHRU+=("$@") + break + ;; + *) + echo "Unknown argument: $1" >&2 + echo >&2 + usage >&2 + exit 2 + ;; + esac +done + +if [[ -n "$TEST_CLASS" && -n "$TEST_TARGET" ]]; then + echo "ERROR: Use either --test-class OR --test (Class#method), not both." >&2 + exit 2 +fi + +ROOT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)" +cd "$ROOT" + +# Prefer Maven Wrapper if present. +MVN="mvn" +if [[ -x "./mvnw" ]]; then + MVN="./mvnw" +fi + +PORT="${SUREFIRE_DEBUG_PORT:-55005}" +REPO_LOCAL="-Dmaven.repo.local=$ROOT/.m2_repo" + +OFFLINE_ARGS=() +if [[ "$OFFLINE" == "1" ]]; then + OFFLINE_ARGS+=("-o") +fi + +# If it looks like a bare artifactId, convert to :artifactId for -pl convenience. +PL="" +if [[ -n "$MODULE" ]]; then + PL="$MODULE" + if [[ "$PL" != *":"* && "$PL" != *"/"* ]]; then + PL=":$PL" + fi +fi + +if [[ "$SKIP_INSTALL" != "1" ]]; then + INSTALL_CMD=("$MVN" "-T" "1C" "${OFFLINE_ARGS[@]}" "$REPO_LOCAL" "-Pquick") + if [[ -n "$PL" ]]; then + INSTALL_CMD+=("-pl" "$PL" "-am") + fi + INSTALL_CMD+=("clean" "install") + echo "=== Pre-test install (fast, no tests) ===" >&2 + printf ' %q' "${INSTALL_CMD[@]}"; echo >&2 + "${INSTALL_CMD[@]}" + echo >&2 +fi + +# Build Maven test command. +CMD=("$MVN" "${OFFLINE_ARGS[@]}" "$REPO_LOCAL") + +if [[ -n "$PL" ]]; then + CMD+=("-pl" "$PL") +fi + +# Enable Surefire's debug mode. +CMD+=("-Dmaven.surefire.debug=-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=localhost:${PORT}") + + +# Narrow the test scope if requested. +if [[ -n "$TEST_TARGET" ]]; then + CMD+=("-Dtest=$TEST_TARGET") +elif [[ -n "$TEST_CLASS" ]]; then + CMD+=("-Dtest=$TEST_CLASS") +fi + +# Pass-through args (e.g., -DfailIfNoTests=false) +if [[ ${#PASSTHRU[@]} -gt 0 ]]; then + CMD+=("${PASSTHRU[@]}") +fi + +CMD+=("test") + +{ + echo "=== Maven Surefire Debug Runner ===" + echo "Root: $ROOT" + echo "Maven: $MVN" + if [[ -n "$PL" ]]; then + echo "Module selector (-pl): $PL" + fi + if [[ -n "$TEST_TARGET" ]]; then + echo "Test selector: -Dtest=$TEST_TARGET" + elif [[ -n "$TEST_CLASS" ]]; then + echo "Test selector: -Dtest=$TEST_CLASS" + else + echo "Test selector: (all tests selected by Maven/Surefire)" + fi + if [[ "$OFFLINE" == "1" ]]; then + echo "Offline: yes (-o)" + else + echo "Offline: no" + fi + echo "Local repo: $ROOT/.m2_repo" + echo "JDWP port: $PORT (attach to localhost)" + echo + echo "Attach a debugger, e.g.:" + echo " jdb -attach $PORT" + echo + echo "Running:" + printf ' %q' "${CMD[@]}"; echo + echo "==================================" + echo +} >&2 + +exec "${CMD[@]}" + diff --git a/.codex/skills/mvnf/SKILL.md b/.codex/skills/mvnf/SKILL.md new file mode 100644 index 00000000000..4c09bd082de --- /dev/null +++ b/.codex/skills/mvnf/SKILL.md @@ -0,0 +1,32 @@ +--- +name: mvnf +description: Run Maven tests in this repo with a consistent workflow (module clean, root -Pquick clean install to refresh .m2_repo, then module verify or a single test class/method). Use when asked to run tests/verify in the rdf4j multi-module build or when the user says mvnf. +--- + +# mvnf + +Run Maven tests with repeatable commands and useful failure pointers. + +## Quick start + +- Run a module's full test suite: + - `python3 .codex/skills/mvnf/scripts/mvnf.py core/sail/shacl` +- Run a unit test class or method (module auto-detected): + - `python3 .codex/skills/mvnf/scripts/mvnf.py ShaclSailTest` + - `python3 .codex/skills/mvnf/scripts/mvnf.py ShaclSailTest#testSomething` +- Run an integration test (Failsafe): + - `python3 .codex/skills/mvnf/scripts/mvnf.py --it ShaclSailIT#testSomething` + +## What it does + +1. `mvn -o -Dmaven.repo.local=.m2_repo -pl clean` +2. `mvn -T 1C -o -Dmaven.repo.local=.m2_repo -Pquick clean install` +3. `mvn -o -Dmaven.repo.local=.m2_repo -pl verify` (optionally with `-Dtest=` / `-Dit.test=`) + +If the test run fails, it prints the list of Surefire/Failsafe report files under the module's `target/*-reports/` directories. + +## Options + +- `--module `: Force the module when the test class name exists in multiple modules. +- `--it`: Treat the selector as an integration test and pass it via `-Dit.test=...`. +- `--no-offline`: Run Maven commands without `-o` (useful if offline resolution fails). diff --git a/.codex/skills/mvnf/scripts/mvnf.py b/.codex/skills/mvnf/scripts/mvnf.py new file mode 100755 index 00000000000..d7e600fcaff --- /dev/null +++ b/.codex/skills/mvnf/scripts/mvnf.py @@ -0,0 +1,325 @@ +#!/usr/bin/env python3 + +from __future__ import annotations + +import argparse +import datetime +import os +import shlex +import subprocess +import sys +from collections import deque +from pathlib import Path + + +def _quote_cmd(cmd: list[str]) -> str: + return " ".join(shlex.quote(part) for part in cmd) + + +def _find_git_root(start: Path) -> Path | None: + for current in [start] + list(start.parents): + if (current / ".git").exists(): + return current + return None + + +def _find_repo_root() -> Path: + candidates = [Path.cwd(), Path(__file__).resolve()] + + for candidate in candidates: + start = candidate if candidate.is_dir() else candidate.parent + + git_root = _find_git_root(start) + if git_root is not None and (git_root / "pom.xml").is_file(): + return git_root + + pom_roots: list[Path] = [] + for current in [start] + list(start.parents): + if (current / "pom.xml").is_file(): + pom_roots.append(current) + if pom_roots: + return pom_roots[-1] + + raise SystemExit("Could not locate a Maven repo root (no pom.xml found in parent dirs).") + + +def _default_maven_cmd(repo_root: Path) -> list[str]: + mvnw = repo_root / "mvnw" + if mvnw.is_file(): + if os.access(mvnw, os.X_OK): + return [str(mvnw)] + return ["sh", str(mvnw)] + return ["mvn"] + + +def _resolve_module_dir(repo_root: Path, module: str) -> Path | None: + module_path = (repo_root / module).resolve() + try: + module_path.relative_to(repo_root.resolve()) + except ValueError: + return None + if not module_path.is_dir(): + return None + if not (module_path / "pom.xml").is_file(): + return None + return module_path + + +def _split_test_selector(selector: str) -> tuple[str, str | None]: + if "#" not in selector: + return selector, None + class_part, method_part = selector.split("#", 1) + return class_part, method_part + + +def _is_existing_file_path(repo_root: Path, maybe_path: str) -> Path | None: + candidate = Path(maybe_path) + if not candidate.is_absolute(): + candidate = repo_root / candidate + if candidate.is_file(): + return candidate.resolve() + return None + + +def _find_test_files(repo_root: Path, class_name: str) -> list[Path]: + simple = class_name.split(".")[-1] + + patterns = [ + f"**/src/test/java/**/{simple}.java", + f"**/src/test/java/**/{simple}.kt", + f"**/src/test/**/{simple}.java", + f"**/src/test/**/{simple}.kt", + ] + + matches: list[Path] = [] + for pattern in patterns: + matches.extend(repo_root.glob(pattern)) + + unique = sorted({match.resolve() for match in matches}) + + if "." in class_name: + expected_java = "/".join(class_name.split(".")) + ".java" + expected_kt = "/".join(class_name.split(".")) + ".kt" + unique = [m for m in unique if m.as_posix().endswith(expected_java) or m.as_posix().endswith(expected_kt)] + + return unique + + +def _find_nearest_module_dir(repo_root: Path, file_path: Path) -> Path: + for current in [file_path.parent] + list(file_path.parents): + if (current / "pom.xml").is_file(): + return current + if current == repo_root: + break + return repo_root + + +def _infer_module_and_selector(repo_root: Path, selector: str, forced_module: str | None) -> tuple[str, str]: + class_part, method_part = _split_test_selector(selector) + + test_file = _is_existing_file_path(repo_root, class_part) + if test_file is None and (class_part.endswith(".java") or class_part.endswith(".kt")): + raise SystemExit(f"Test file not found: {class_part}") + + if test_file is None: + matches = _find_test_files(repo_root, class_part) + else: + matches = [test_file] + + if forced_module is not None: + module_dir = _resolve_module_dir(repo_root, forced_module) + if module_dir is None: + raise SystemExit(f"Module not found (expected a path containing pom.xml): {forced_module}") + matches = [m for m in matches if str(m).startswith(str(module_dir.resolve()) + os.sep)] + + if not matches: + raise SystemExit( + f"Could not locate test source for '{class_part}'. " + "Pass a module path (e.g. core/sail/shacl) or use --module to disambiguate.", + ) + + if len(matches) > 1: + details: list[str] = [] + for match in matches: + module_dir = _find_nearest_module_dir(repo_root, match) + module_rel = module_dir.relative_to(repo_root).as_posix() + match_rel = match.relative_to(repo_root).as_posix() + details.append(f"- {module_rel}: {match_rel}") + detail_text = "\n".join(details) + raise SystemExit( + f"Test class '{class_part}' exists in multiple modules:\n{detail_text}\n" + "Re-run with --module to pick the right one.", + ) + + match = matches[0] + module_dir = _find_nearest_module_dir(repo_root, match) + module = module_dir.relative_to(repo_root).as_posix() + + class_name = class_part + if test_file is not None: + class_name = match.stem + + final_selector = class_name if method_part is None else f"{class_name}#{method_part}" + return module, final_selector + + +def _log_dir(repo_root: Path) -> Path: + log_dir = repo_root / "logs" / "mvnf" + log_dir.mkdir(parents=True, exist_ok=True) + return log_dir + + +def _run( + cmd: list[str], + cwd: Path, + tail_lines: int, + log_path: Path, + stream: bool, +) -> tuple[int, list[str]]: + print(f"\n$ {_quote_cmd(cmd)}") + print(f"[mvnf] Log: {log_path.relative_to(cwd).as_posix()}") + sys.stdout.flush() + + output_tail: deque[str] = deque(maxlen=tail_lines) + log_path.parent.mkdir(parents=True, exist_ok=True) + with log_path.open("w", encoding="utf-8", errors="replace") as log_file: + with subprocess.Popen( + cmd, + cwd=str(cwd), + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True, + bufsize=1, + ) as proc: + assert proc.stdout is not None + for line in proc.stdout: + log_file.write(line) + output_tail.append(line.rstrip("\n")) + if stream: + print(line, end="") + + tail = list(output_tail) + if not stream and tail: + print("[mvnf] Output tail:") + print("\n".join(tail)) + + return proc.returncode, tail + + +def _delete_logs(log_paths: list[Path]) -> None: + for log_path in log_paths: + try: + log_path.unlink(missing_ok=True) + except OSError as exc: + print(f"[mvnf] Warning: could not delete log {log_path}: {exc}") + + +def _list_report_files(repo_root: Path, module: str) -> list[Path]: + module_dir = (repo_root / module).resolve() + report_dirs = [ + module_dir / "target" / "surefire-reports", + module_dir / "target" / "failsafe-reports", + ] + + files: list[Path] = [] + for report_dir in report_dirs: + if not report_dir.is_dir(): + continue + files.extend(sorted(report_dir.glob("*.txt"))) + files.extend(sorted(report_dir.glob("*.xml"))) + + unique = sorted({f.resolve() for f in files}) + return unique + + +def main() -> int: + parser = argparse.ArgumentParser( + prog="mvnf", + description="Clean module, install everything (quick), then run module verify or a single test.", + ) + parser.add_argument( + "target", + help="Module path (e.g. core/sail/shacl) OR test class[#method] (e.g. ShaclSailTest#testX).", + ) + parser.add_argument( + "--module", + help="Force the module path when target is a test class/method (useful if duplicates exist).", + ) + parser.add_argument("--it", action="store_true", help="Run via Failsafe (-Dit.test) instead of Surefire (-Dtest).") + parser.add_argument("--no-offline", action="store_true", help="Disable Maven offline mode (-o).") + parser.add_argument("--stream", action="store_true", help="Stream full Maven output to stdout (can be very long).") + parser.add_argument( + "--retain-logs", + action="store_true", + help="Keep clean/install/verify logs even when tests pass.", + ) + parser.add_argument("--tail", type=int, default=200, help="Keep the last N Maven output lines for failures.") + parser.add_argument("--mvn", help="Override the Maven command (default: mvn or ./mvnw).") + args = parser.parse_args() + + repo_root = _find_repo_root() + mvn_cmd = shlex.split(args.mvn) if args.mvn else _default_maven_cmd(repo_root) + + offline_flag = [] if args.no_offline else ["-o"] + common_flags = offline_flag + ["-Dmaven.repo.local=.m2_repo"] + + module_dir = _resolve_module_dir(repo_root, args.target.strip()) + if args.module is None and module_dir is not None: + module = module_dir.relative_to(repo_root).as_posix() + test_selector = None + else: + module, test_selector = _infer_module_and_selector(repo_root, args.target.strip(), args.module) + + print(f"Repo root: {repo_root}") + print(f"Module: {module}") + if test_selector is not None: + print(f"Test selector: {test_selector} ({'failsafe' if args.it else 'surefire'})") + + clean_cmd = mvn_cmd + common_flags + ["-pl", module, "clean"] + install_cmd = mvn_cmd + (offline_flag + ["-T", "1C", "-Dmaven.repo.local=.m2_repo", "-Pquick", "clean", "install"]) + + verify_cmd = mvn_cmd + common_flags + ["-pl", module] + if test_selector is not None: + verify_cmd.append(f"-Dit.test={test_selector}" if args.it else f"-Dtest={test_selector}") + verify_cmd.append("verify") + + run_id = datetime.datetime.now(datetime.UTC).strftime("%Y%m%d-%H%M%S") + log_dir = _log_dir(repo_root) + log_paths = [ + log_dir / f"{run_id}-clean.log", + log_dir / f"{run_id}-install.log", + log_dir / f"{run_id}-verify.log", + ] + + rc, _ = _run(clean_cmd, repo_root, args.tail, log_paths[0], args.stream) + if rc != 0: + print("\n[mvnf] Module clean failed.") + return rc + + rc, _ = _run(install_cmd, repo_root, args.tail, log_paths[1], args.stream) + if rc != 0: + print("\n[mvnf] Root clean install failed.") + return rc + + rc, _ = _run(verify_cmd, repo_root, args.tail, log_paths[2], args.stream) + if rc == 0: + print("\n[mvnf] Tests passed.") + if not args.retain_logs: + _delete_logs(log_paths) + return 0 + + print("\n[mvnf] Tests failed.") + + reports = _list_report_files(repo_root, module) + if reports: + print("\n[mvnf] Reports:") + for report in reports: + print(f"- {report.relative_to(repo_root).as_posix()}") + else: + print("\n[mvnf] No surefire/failsafe reports found for this module.") + + return rc + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/AGENTS.md b/AGENTS.md index b203d6ea055..d0b254e8b0f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -81,6 +81,7 @@ Before taking any action (either tool calls *or* responses to the user), you mus It is illegal to `-am` when running tests! It is illegal to `-q` when running tests! +Always keep untracked artifacts! > **Clarification:** For **strictly behavior‑neutral refactors** that are already **fully exercised by existing tests**, or for **bugfixes with an existing failing test**, you may use **Routine B — Change without new tests**. In that case you must capture **pre‑change passing evidence** at the smallest scope that hits the code you’re about to edit, prove **Hit Proof**, then show **post‑change passing evidence** from the **same selection**. > **No exceptions for any behavior‑changing change** — for those, you must follow **Routine A — Full TDD** or **Routine D — ExecPlans**. @@ -126,6 +127,48 @@ When writing complex features or significant refactors, use an ExecPlan (as desc 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. + +### P — **Problem** + +**Goal:** State the core problem and what “good” looks like. +**Ask:** Who’s affected? What outcome is required? What happens if we do nothing? +**Tip:** Include measurable target(s): error rate ↓, latency p95 ↓, revenue ↑, risk ↓. + +### I — **Information** + +**Goal:** Gather only the facts needed to move. +**Ask:** What do logs/metrics/user feedback say? What constraints (security, compliance, budget, SLA/SLO)? What assumptions must we test? + +### O — **Options** + +**Goal:** Generate viable ways forward, including “do nothing.” +**Ask:** What are 2–4 distinct approaches (patch, redesign, buy vs. build, defer)? What risks, costs, and second‑order effects? +**Tip:** Check guardrails: reliability, security/privacy, accessibility, performance, operability, unit economics. + +### S — **Select** + +**Goal:** Decide deliberately and document why. +**Ask:** Which option best meets the success criteria under constraints? Who is the decision owner? What’s the fallback/abort condition? +**Tip:** Use lightweight scoring (e.g., Impact×Confidence÷Effort) to avoid bike‑shedding. + +### E — **Execute** + +**Goal:** Ship safely and visibly. +**Ask:** What is the smallest safe slice? How do we de‑risk (feature flag, canary, dark launch, rollback)? Who owns what? +**Checklist:** Traces/logs/alerts; security & privacy checks; docs & changelog; incident plan if relevant. + +### E — **Evaluate** + +**Goal:** Verify outcomes and learn. +**Ask:** Did metrics hit targets? Any regressions or side effects? What will we keep/change next loop? +**Output:** Post‑release review (or retro), decision log entry, follow‑ups (tickets), debt captured. +**Tip:** If outcomes miss, either **iterate** (new Options) or **reframe** (back to Problem). + +--- + ### Benchmarking workflow (repository-wide) The `scripts/run-single-benchmark.sh` helper is the supported path for spot-checking performance optimisations. It builds the chosen module with the `benchmarks` profile, constrains the benchmark selection to a single `@Benchmark` method, and when `--enable-jfr` is supplied it enforces repeatable profiling defaults (no warmup, ten 10-second measurements, one fork) while clearly reporting the destination of the generated JFR recording. Lean on this script whenever you need a reproducible measurement harness. @@ -200,7 +243,7 @@ After each grouped action, post an **Evidence block**, then continue working: **Evidence template** ``` Evidence: -Command: mvn -o -Dmaven.repo.local=.m2_repo -pl -Dtest=Class#method verify +Command: python3 .codex/skills/mvnf/scripts/mvnf.py Class#method (preferred) OR mvn -o -Dmaven.repo.local=.m2_repo -pl -Dtest=Class#method verify Report: /target/surefire-reports/.txt Snippet: \ @@ -222,8 +265,11 @@ To avoid losing the first test evidence when later runs overwrite `target/*-repo • On a fully green verify run: -- Capture and store the last 200 lines of the Maven verify output. -- Example (module‑scoped): +- Capture and store the last 200 lines of the verify output. +- Example (mvnf): + - `python3 .codex/skills/mvnf/scripts/mvnf.py --retain-logs --stream` + - `tail -200 "$(ls -t logs/mvnf/*-verify.log | head -1)" > initial-evidence.txt` +- Example (manual Maven): - `mvn -o -Dmaven.repo.local=.m2_repo -pl verify | tee .initial-verify.log` - `tail -200 .initial-verify.log > initial-evidence.txt` @@ -286,6 +332,7 @@ Plan It is illegal to `-am` when running tests! It is illegal to `-q` when running tests! +Always keep untracked artifacts! --- @@ -301,6 +348,7 @@ Running `install` publishes your changed modules there so downstream modules and * 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. +* If you run tests via `mvnf`, it already performs module clean + root `-Pquick` install before verify. Why this is mandatory @@ -310,6 +358,17 @@ Why this is mandatory - 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`. --- +## Skills (Preferred Runners) + +Prefer these skills over manual Maven test commands. Manual commands remain available as a fallback when needed. + +- `mvnf`: Consistent test runner that does module clean, root `-Pquick` install, then module verify or a single test class/method. Use this as the default way to run tests. Logs are deleted on success unless `--retain-logs`. +- `debug-surefire`: Runs Surefire tests in JDWP wait-for-debugger mode so you can attach a debugger (jdb/IDE) and step through tests. + +If you need manual control or a skill does not fit, use the Maven commands below. + +--- + ## Quick Start (First 10 Minutes) 1. **Discover** @@ -318,19 +377,24 @@ Why this is mandatory 2. **Build sanity (fast, skip tests)** * `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` + * `mvn -o -Dmaven.repo.local=.m2_repo -q -T 2C process-resources` * Ensure every touched Java file has the correct agent signature comment (`// Some portions generated by Codex` for Codex, `// Some portions generated by Co-Pilot` for GitHub Co-Pilot) inserted immediately below the header before formatting. * Before invoking the formatter, `cd scripts && ./checkCopyrightPresent.sh` (or use `pushd/popd`) to ensure every new or edited source file still carries the required header; fix any findings before formatting. -4. **Targeted tests (tight loops)** - * Module: `mvn -o -Dmaven.repo.local=.m2_repo -pl verify | tail -500` - * Class: `mvn -o -Dmaven.repo.local=.m2_repo -pl -Dtest=ClassName verify | tail -500` - * Method: `mvn -o -Dmaven.repo.local=.m2_repo -pl -Dtest=ClassName#method verify | tail -500` +4. **Targeted tests (tight loops, prefer `mvnf`)** + * Module: `python3 .codex/skills/mvnf/scripts/mvnf.py ` + * Class: `python3 .codex/skills/mvnf/scripts/mvnf.py ClassName` + * Method: `python3 .codex/skills/mvnf/scripts/mvnf.py ClassName#method` + * Optional Maven fallback: + * Module: `mvn -o -Dmaven.repo.local=.m2_repo -pl verify | tail -500` + * Class: `mvn -o -Dmaven.repo.local=.m2_repo -pl -Dtest=ClassName verify | tail -500` + * Method: `mvn -o -Dmaven.repo.local=.m2_repo -pl -Dtest=ClassName#method verify | tail -500` 5. **Inspect failures** * **Unit (Surefire):** `/target/surefire-reports/` * **IT (Failsafe):** `/target/failsafe-reports/` It is illegal to `-am` when running tests! It is illegal to `-q` when running tests! +Always keep untracked artifacts! --- @@ -424,35 +488,38 @@ When writing complex features or significant refactors, use an ExecPlan (as desc ## Working Loop +* **PIOSEE first:** restate Problem, gather Information, list Options; then Select, Execute, Evaluate. * **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` +* **Format:** `mvn -o -Dmaven.repo.local=.m2_repo -q -T 2C process-resources` * **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`. +* **Test (prefer `mvnf`):** start smallest (class/method → module); use `--it` for integration tests. Use manual Maven only when you need profiles/flags not supported by `mvnf`. * **Triage:** read reports; fix root cause; expand scope only when needed. * **Iterate:** keep momentum; escalate only when blocked or irreversible. It is illegal to `-am` when running tests! It is illegal to `-q` when running tests! +Always keep untracked artifacts! --- ## Testing Strategy -* **Prefer module tests you touched:** `-pl ` +* **Prefer `mvnf`:** start with `python3 .codex/skills/mvnf/scripts/mvnf.py Class#method`, then `Class`, then ``. +* **Integration tests:** use `--it` (e.g., `python3 .codex/skills/mvnf/scripts/mvnf.py --it ITClass#method`). * **Narrow further** to a class/method; then broaden to the module. * **Expand scope** when changes cross boundaries or neighbor modules fail. * **Read reports** * Surefire (unit): `target/surefire-reports/` * Failsafe (IT): `target/failsafe-reports/` -* **Helpful flags** +* **Manual Maven fallback flags (when `mvnf` doesn't fit)** * `-Dtest=Class#method` (unit selection) * `-Dit.test=ITClass#method` (integration selection) * `-DtrimStackTrace=false` (full traces) * `-DskipITs` (focus on unit tests) * `-DfailIfNoTests=false` (when selecting a class that has no tests on some platforms) -### Optional: Redirect test stdout/stderr to files +### Optional: Redirect test stdout/stderr to files (manual Maven) ```bash mvn -o -Dmaven.repo.local=.m2_repo -pl -Dtest=ClassName[#method] -Dmaven.test.redirectTestOutputToFile=true verify | tail -500 ```` @@ -498,7 +565,7 @@ Assertions are executable claims about what must be true. Use **temporary tripwi * Always run before finalizing: - * `mvn -o -Dmaven.repo.local=.m2_repo -q -T 2C formatter:format impsort:sort xml-format:xml-format` + * `mvn -o -Dmaven.repo.local=.m2_repo -q -T 2C process-resources` * Style: no wildcard imports; 120‑char width; curly braces always; LF endings. ### Import hygiene (always) @@ -537,9 +604,9 @@ Immediately after creating any new Java source file, add the signature comment ( ## Pre‑Commit Checklist -* **Format:** `mvn -o -Dmaven.repo.local=.m2_repo -q -T 2C formatter:format impsort:sort xml-format:xml-format` +* **Format:** `mvn -o -Dmaven.repo.local=.m2_repo -q -T 2C process-resources` * **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) +* **Tests (targeted, prefer `mvnf`):** `python3 .codex/skills/mvnf/scripts/mvnf.py ` (broaden as needed; use Maven fallback if you need profiles/flags) * **Reports:** zero new failures in Surefire/Failsafe, or explain precisely. * **Evidence:** Routine A — failing pre‑fix + passing post‑fix. Routine B — **pre/post green** from same selection + **Hit Proof**. @@ -601,7 +668,7 @@ Immediately after creating any new Java source file, add the signature comment ( **Defaults** -* **Tests:** start with `-pl `, then `-Dtest=Class#method` / `-Dit.test=ITClass#method`. +* **Tests:** start with `python3 .codex/skills/mvnf/scripts/mvnf.py Class#method` (or `--it ITClass#method`), then broaden to class/module. Use Maven flags only when `mvnf` cannot express the required profile/flags. * **Build:** use `-o`; drop `-o` once only to fetch; return offline. * **Formatting:** run formatter/import/XML before verify. * **Reports:** read surefire/failsafe locally; expand scope only when necessary. @@ -614,6 +681,7 @@ Immediately after creating any new Java source file, add the signature comment ( * **Files touched:** list file paths. * **Commands run:** key build/test commands. * **Verification:** which tests passed, where you checked reports. +* **PIOSEE trace (concise):** P/I/O summary, selected option/routine, key evaluate outcomes. * **Evidence:** *Routine A:* failing output (pre‑fix) and passing output (post‑fix). *Routine B:* pre‑ and post‑green snippets from the **same selection** + **Hit Proof**. @@ -627,6 +695,16 @@ Immediately after creating any new Java source file, add the signature comment ( ## Running Tests +**Preferred (`mvnf`)** + +* Module: `python3 .codex/skills/mvnf/scripts/mvnf.py core/sail/shacl` +* Class: `python3 .codex/skills/mvnf/scripts/mvnf.py ShaclSailTest` +* Method: `python3 .codex/skills/mvnf/scripts/mvnf.py ShaclSailTest#testSomething` +* Integration test: `python3 .codex/skills/mvnf/scripts/mvnf.py --it ShaclSailIT#testSomething` +* Keep logs on success: add `--retain-logs` + +**Manual Maven fallback (profiles/extra flags/full repo)** + * By module: `mvn -o -Dmaven.repo.local=.m2_repo -pl core/sail/shacl verify | tail -500` * Entire repo: `mvn -o -Dmaven.repo.local=.m2_repo verify` (long; only when appropriate) * Slow tests (entire repo): @@ -653,9 +731,9 @@ Immediately after creating any new Java source file, add the signature comment ( * **Build without tests (fast path):** `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) +* **Verify with tests (prefer `mvnf`):** + Targeted module(s): `python3 .codex/skills/mvnf/scripts/mvnf.py ` + Entire repo (fallback): `mvn -o -Dmaven.repo.local=.m2_repo verify` (use judiciously) * **When offline fails due to missing deps:** Re‑run the **exact** command **without** `-o` once to fetch, then return to `-o`. @@ -665,6 +743,8 @@ Immediately after creating any new Java source file, add the signature comment ( JaCoCo is configured via the `jacoco` Maven profile in the root POM. Surefire/Failsafe honor the prepared agent `argLine`, so no extra flags are required beyond `-Pjacoco`. +- Use manual Maven here (profiles are not supported by `mvnf`). + - Run with coverage - Module: `mvn -o -Dmaven.repo.local=.m2_repo -pl -Pjacoco verify | tail -500` - Class: `mvn -o -Dmaven.repo.local=.m2_repo -pl -Pjacoco -Dtest=ClassName verify | tail -500` @@ -834,4 +914,6 @@ rdf4j: root project It is illegal to `-am` when running tests! It is illegal to `-q` when running tests! +Always keep untracked artifacts! + You must follow these rules and instructions exactly as stated. diff --git a/core/rio/jsonld/src/test/java/org/eclipse/rdf4j/rio/jsonld/JSONLDWriterTest.java b/core/rio/jsonld/src/test/java/org/eclipse/rdf4j/rio/jsonld/JSONLDWriterTest.java index 97a49acabe2..264cdd2f6dc 100644 --- a/core/rio/jsonld/src/test/java/org/eclipse/rdf4j/rio/jsonld/JSONLDWriterTest.java +++ b/core/rio/jsonld/src/test/java/org/eclipse/rdf4j/rio/jsonld/JSONLDWriterTest.java @@ -22,7 +22,6 @@ import org.eclipse.rdf4j.model.IRI; import org.eclipse.rdf4j.model.Literal; import org.eclipse.rdf4j.model.Model; -import org.eclipse.rdf4j.model.Namespace; import org.eclipse.rdf4j.model.Statement; import org.eclipse.rdf4j.model.impl.LinkedHashModel; import org.eclipse.rdf4j.model.util.ModelBuilder; @@ -49,8 +48,6 @@ import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; -import jakarta.json.Json; -import jakarta.json.JsonObjectBuilder; import no.hasmac.jsonld.JsonLdError; import no.hasmac.jsonld.document.Document; import no.hasmac.jsonld.document.JsonDocument; diff --git a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ShaclValidator.java b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ShaclValidator.java index 03f2177ce3e..ac23b2b8871 100644 --- a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ShaclValidator.java +++ b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ShaclValidator.java @@ -8,23 +8,53 @@ * * SPDX-License-Identifier: BSD-3-Clause ******************************************************************************/ +// Some portions generated by Codex package org.eclipse.rdf4j.sail.shacl; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.StringReader; +import java.net.URL; +import java.nio.file.Path; +import java.util.ArrayList; import java.util.Arrays; +import java.util.LinkedHashSet; import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; import java.util.stream.Collectors; -import java.util.stream.Stream; import org.eclipse.rdf4j.common.annotation.Experimental; import org.eclipse.rdf4j.common.transaction.IsolationLevels; +import org.eclipse.rdf4j.model.IRI; import org.eclipse.rdf4j.model.Resource; +import org.eclipse.rdf4j.model.vocabulary.RDF; import org.eclipse.rdf4j.model.vocabulary.RDF4J; +import org.eclipse.rdf4j.model.vocabulary.RSX; +import org.eclipse.rdf4j.model.vocabulary.SESAME; +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.rio.RDFParseException; +import org.eclipse.rdf4j.rio.Rio; import org.eclipse.rdf4j.sail.InterruptedSailException; import org.eclipse.rdf4j.sail.Sail; import org.eclipse.rdf4j.sail.SailConnection; +import org.eclipse.rdf4j.sail.SailException; +import org.eclipse.rdf4j.sail.memory.MemoryStore; +import org.eclipse.rdf4j.sail.shacl.ast.Cache; import org.eclipse.rdf4j.sail.shacl.ast.ContextWithShape; import org.eclipse.rdf4j.sail.shacl.ast.Shape; +import org.eclipse.rdf4j.sail.shacl.config.ShaclSailConfig; import org.eclipse.rdf4j.sail.shacl.results.ValidationReport; import org.eclipse.rdf4j.sail.shacl.results.lazy.LazyValidationReport; import org.eclipse.rdf4j.sail.shacl.results.lazy.ValidationResultIterator; @@ -42,81 +72,1589 @@ public class ShaclValidator { private static final Resource[] ALL_CONTEXTS = {}; private static final Logger logger = LoggerFactory.getLogger(ShaclValidator.class); - // tests can write to this field using reflection - @SuppressWarnings("FieldMayBeFinal") - private static Resource[] SHAPE_CONTEXTS = ALL_CONTEXTS; + /** + * Create a new builder for configuring a SHACL validator. + * + * @return a new builder instance + */ + public static Builder builder() { + return new Builder(); + } - public static ValidationReport validate(Sail dataRepo, Sail shapesRepo) { + /** + * Create a builder seeded from an existing {@link ShaclSail}. + * + * @param shaclSail the configured SHACL sail supplying shapes and settings + * @return a builder preconfigured from the supplied sail + */ + public static BuilderWithShapes from(ShaclSail shaclSail) { + return new BuilderWithShapes(Builder.settingsFrom(shaclSail), shaclSail); + } - List shapes; - try (SailConnection shapesConnection = shapesRepo.getConnection()) { - shapesConnection.begin(IsolationLevels.NONE); - try (ShapeSource shapeSource = new CombinedShapeSource(shapesConnection, - shapesConnection)) { - Stream allShapeContexts = shapeSource - .withContext(SHAPE_CONTEXTS) - .getAllShapeContexts(); - if (SHAPE_CONTEXTS.length == 0) { - allShapeContexts = Stream.concat(allShapeContexts, - Stream.of(new ShapeSource.ShapesGraph(RDF4J.NIL))); - } - List parsed = allShapeContexts - .map(context -> Shape.Factory.parse(shapeSource.withContext(context.getShapesGraph()), context, - new Shape.ParseSettings(true, true))) - .flatMap(List::stream) - .collect(Collectors.toList()); - - shapes = Shape.Factory.getShapes(parsed).stream().distinct().collect(Collectors.toList()); - - if (logger.isDebugEnabled()) { - for (ContextWithShape shape : shapes) { - logger.debug("Using data graph(s) {} and shape graph(s) {} with shape {}", - Arrays.toString(shape.getDataGraph()), Arrays.toString(shape.getShapeGraph()), - shape.getShape()); - } - } + @FunctionalInterface + private interface SailLoader { + void load(SailRepositoryConnection connection) throws IOException, RDFParseException; + } + + private static Sail loadShapes(String description, SailLoader loader) { + return loadSail("SHACL shapes", description, loader); + } + private static Sail loadData(String description, SailLoader loader) { + return loadSail("data", description, loader); + } + + private static Sail loadSail(String kind, String description, SailLoader loader) { + SailRepository repo = new SailRepository(new MemoryStore()); + repo.init(); + try (SailRepositoryConnection connection = repo.getConnection()) { + connection.begin(IsolationLevels.NONE); + loader.load(connection); + connection.commit(); + } catch (IOException | RDFParseException e) { + throw new SailException("Failed to read " + kind + " from " + description, e); + } catch (RuntimeException e) { + if (e instanceof SailException) { + throw e; } - shapesConnection.commit(); - } catch (Throwable e) { - logger.warn("Failed to read shapes", e); - throw e; + throw new SailException("Failed to read " + kind + " from " + description, e); } + return repo.getSail(); + } - try (SailConnection dataRepoConnection = dataRepo.getConnection()) { + private static RDFFormat detectRdfFormat(String description, String... candidates) { + for (String candidate : candidates) { + if (candidate == null || candidate.isBlank()) { + continue; + } + var format = Rio.getParserFormatForFileName(candidate); + if (format.isPresent()) { + return format.get(); + } + } + throw new SailException( + "Could not determine RDF format for " + description + ". Provide RDFFormat explicitly."); + } + + public static class Builder extends InternalBuilder implements Cloneable { + + private Builder() { + } + + /** + * Create a builder with settings copied from an existing {@link ShaclSail}. + * + * @param shaclSail the configured SHACL sail + * @return a builder initialized from the supplied sail + */ + public static Builder settingsFrom(ShaclSail shaclSail) { + Builder builder = new Builder(); + builder.setShapesGraphs(shaclSail.getShapesGraphs()); + builder.setParallelValidation(shaclSail.isParallelValidation()); + builder.setLogValidationPlans(shaclSail.isLogValidationPlans()); + builder.setLogValidationViolations(shaclSail.isLogValidationViolations()); + builder.setGlobalLogValidationExecution(shaclSail.isGlobalLogValidationExecution()); + builder.setCacheSelectNodes(shaclSail.isCacheSelectNodes()); + builder.setRdfsSubClassReasoning(shaclSail.isRdfsSubClassReasoning()); + builder.setSerializableValidation(shaclSail.isSerializableValidation()); + builder.setPerformanceLogging(shaclSail.isPerformanceLogging()); + builder.setEclipseRdf4jShaclExtensions(shaclSail.isEclipseRdf4jShaclExtensions()); + builder.setDashDataShapes(shaclSail.isDashDataShapes()); + builder.setValidationResultsLimitTotal(shaclSail.getValidationResultsLimitTotal()); + builder.setValidationResultsLimitPerConstraint(shaclSail.getValidationResultsLimitPerConstraint()); + builder.setTransactionalValidationLimit(shaclSail.getTransactionalValidationLimit()); + + if (shaclSail.isValidationEnabled()) { + builder.enableValidation(); + } else { + builder.disableValidation(); + } + return builder; + } + + /** + * Use the supplied SHACL shapes sail for validation. + * + * @param shapes the shapes sail + * @return a builder that validates with the supplied shapes + */ + public BuilderWithShapes withShapes(Sail shapes) { + return new BuilderWithShapes(this, shapes); + } + + /** + * Load SHACL shapes from a file using an auto-detected RDF format. + */ + public BuilderWithShapes withShapes(File shapesFile, String baseURI) { + Objects.requireNonNull(shapesFile, "shapesFile"); + RDFFormat format = detectRdfFormat("file " + shapesFile, shapesFile.getName(), baseURI); + return withShapes(shapesFile, baseURI, format); + } + + /** + * Load SHACL shapes from a file using an auto-detected RDF format. + */ + public BuilderWithShapes withShapes(File shapesFile) { + Objects.requireNonNull(shapesFile, "shapesFile"); + RDFFormat format = detectRdfFormat("file " + shapesFile, shapesFile.getName()); + return withShapes(shapesFile, format); + } + + /** + * Load SHACL shapes from a path using an auto-detected RDF format. + */ + public BuilderWithShapes withShapes(Path shapesPath, String baseURI) { + Objects.requireNonNull(shapesPath, "shapesPath"); + RDFFormat format = detectRdfFormat("path " + shapesPath, shapesPath.getFileName().toString(), baseURI); + return withShapes(shapesPath, baseURI, format); + } + + /** + * Load SHACL shapes from a path using an auto-detected RDF format. + */ + public BuilderWithShapes withShapes(Path shapesPath) { + Objects.requireNonNull(shapesPath, "shapesPath"); + RDFFormat format = detectRdfFormat("path " + shapesPath, shapesPath.getFileName().toString()); + return withShapes(shapesPath, format); + } + + /** + * Load SHACL shapes from a URL using an auto-detected RDF format. + */ + public BuilderWithShapes withShapes(URL shapesUrl, String baseURI) { + Objects.requireNonNull(shapesUrl, "shapesUrl"); + RDFFormat format = detectRdfFormat("URL " + shapesUrl, shapesUrl.getPath(), baseURI); + return withShapes(shapesUrl, baseURI, format); + } + + /** + * Load SHACL shapes from a URL using an auto-detected RDF format. + */ + public BuilderWithShapes withShapes(URL shapesUrl) { + Objects.requireNonNull(shapesUrl, "shapesUrl"); + RDFFormat format = detectRdfFormat("URL " + shapesUrl, shapesUrl.getPath()); + return withShapes(shapesUrl, format); + } + + /** + * Load SHACL shapes from an input stream using an auto-detected RDF format. The input stream is not closed by + * this method. + */ + public BuilderWithShapes withShapes(InputStream shapesInputStream, String baseURI) { + Objects.requireNonNull(shapesInputStream, "shapesInputStream"); + RDFFormat format = detectRdfFormat("input stream", baseURI); + return withShapes(shapesInputStream, baseURI, format); + } + + /** + * Load SHACL shapes from RDF content in a string using an auto-detected RDF format. + */ + public BuilderWithShapes withShapes(String shapes, String baseURI) { + Objects.requireNonNull(shapes, "shapes"); + RDFFormat format = detectRdfFormat("string content", baseURI); + return withShapes(shapes, baseURI, format); + } + + /** + * Load SHACL shapes from a file using the supplied base URI and RDF format. + */ + public BuilderWithShapes withShapes(File shapesFile, String baseURI, RDFFormat format) { + Objects.requireNonNull(shapesFile, "shapesFile"); + Objects.requireNonNull(format, "rdfFormat"); + Sail shapes = loadShapes("file " + shapesFile, connection -> connection.add(shapesFile, baseURI, format)); + return withShapes(shapes); + } + + /** + * Load SHACL shapes from a file using the supplied RDF format. + */ + public BuilderWithShapes withShapes(File shapesFile, RDFFormat format) { + Objects.requireNonNull(shapesFile, "shapesFile"); + Objects.requireNonNull(format, "rdfFormat"); + Sail shapes = loadShapes("file " + shapesFile, connection -> connection.add(shapesFile, format)); + return withShapes(shapes); + } + + /** + * Load SHACL shapes from a path using the supplied base URI and RDF format. + */ + public BuilderWithShapes withShapes(Path shapesPath, String baseURI, RDFFormat format) { + Objects.requireNonNull(shapesPath, "shapesPath"); + Objects.requireNonNull(format, "rdfFormat"); + return withShapes(shapesPath.toFile(), baseURI, format); + } + + /** + * Load SHACL shapes from a path using the supplied RDF format. + */ + public BuilderWithShapes withShapes(Path shapesPath, RDFFormat format) { + Objects.requireNonNull(shapesPath, "shapesPath"); + Objects.requireNonNull(format, "rdfFormat"); + return withShapes(shapesPath.toFile(), format); + } + + /** + * Load SHACL shapes from a URL using the supplied base URI and RDF format. + */ + public BuilderWithShapes withShapes(URL shapesUrl, String baseURI, RDFFormat format) { + Objects.requireNonNull(shapesUrl, "shapesUrl"); + Objects.requireNonNull(format, "rdfFormat"); + Sail shapes = loadShapes("URL " + shapesUrl, connection -> connection.add(shapesUrl, baseURI, format)); + return withShapes(shapes); + } + + /** + * Load SHACL shapes from a URL using the supplied RDF format. + */ + public BuilderWithShapes withShapes(URL shapesUrl, RDFFormat format) { + Objects.requireNonNull(shapesUrl, "shapesUrl"); + Objects.requireNonNull(format, "rdfFormat"); + Sail shapes = loadShapes("URL " + shapesUrl, connection -> connection.add(shapesUrl, format)); + return withShapes(shapes); + } + + /** + * Load SHACL shapes from an input stream using the supplied base URI and RDF format. The input stream is not + * closed by this method. + */ + public BuilderWithShapes withShapes(InputStream shapesInputStream, String baseURI, RDFFormat format) { + Objects.requireNonNull(shapesInputStream, "shapesInputStream"); + Objects.requireNonNull(format, "rdfFormat"); + Sail shapes = loadShapes("input stream", connection -> connection.add(shapesInputStream, baseURI, format)); + return withShapes(shapes); + } + + /** + * Load SHACL shapes from an input stream using the supplied RDF format. The input stream is not closed by this + * method. + */ + public BuilderWithShapes withShapes(InputStream shapesInputStream, RDFFormat format) { + Objects.requireNonNull(shapesInputStream, "shapesInputStream"); + Objects.requireNonNull(format, "rdfFormat"); + Sail shapes = loadShapes("input stream", connection -> connection.add(shapesInputStream, format)); + return withShapes(shapes); + } + + /** + * Load SHACL shapes from RDF content in a string using the supplied base URI and RDF format. + */ + public BuilderWithShapes withShapes(String shapes, String baseURI, RDFFormat format) { + Objects.requireNonNull(shapes, "shapes"); + Objects.requireNonNull(format, "rdfFormat"); + Sail shapesSail = loadShapes("string content", + connection -> connection.add(new StringReader(shapes), baseURI, format)); + return withShapes(shapesSail); + } + + /** + * Load SHACL shapes from RDF content in a string using the supplied RDF format. + */ + public BuilderWithShapes withShapes(String shapes, RDFFormat format) { + Objects.requireNonNull(shapes, "shapes"); + Objects.requireNonNull(format, "rdfFormat"); + Sail shapesSail = loadShapes("string content", + connection -> connection.add(new StringReader(shapes), format)); + return withShapes(shapesSail); + } + + /** + * Build a validator that expects shapes to be supplied at validation time. + * + * @return a validator configured with the current settings + */ + public Validator build() { + return new Validator(this); + } + + /** + * {@inheritDoc} + */ + @Override + public Builder clone() { + return (Builder) super.clone(); + } + } + + static class InternalBuilder> implements Cloneable { + private Resource[] shapeContexts = null; + private boolean parallelValidation = true; + private boolean logValidationPlans = false; + private boolean logValidationViolations = false; + private boolean validationEnabled = true; + private boolean cacheSelectNodes = true; + private boolean globalLogValidationExecution = false; + private boolean rdfsSubClassReasoning = true; + private boolean performanceLogging = false; + private boolean serializableValidation = false; + private boolean eclipseRdf4jShaclExtensions = true; + private boolean dashDataShapes = true; + private long validationResultsLimitTotal = ShaclSailConfig.VALIDATION_RESULTS_LIMIT_TOTAL_DEFAULT; + private long validationResultsLimitPerConstraint = ShaclSailConfig.VALIDATION_RESULTS_LIMIT_PER_CONSTRAINT_DEFAULT; + private long transactionalValidationLimit = ShaclSailConfig.TRANSACTIONAL_VALIDATION_LIMIT_DEFAULT; + private long validationTimeoutMillis = -1; + private boolean sparqlValidation = !"false" + .equalsIgnoreCase(System.getProperty("org.eclipse.rdf4j.sail.shacl.sparqlValidation")); + + void setAll(InternalBuilder other) { + this.shapeContexts = other.shapeContexts == null ? null : other.shapeContexts.clone(); + this.parallelValidation = other.parallelValidation; + this.logValidationPlans = other.logValidationPlans; + this.logValidationViolations = other.logValidationViolations; + this.validationEnabled = other.validationEnabled; + this.cacheSelectNodes = other.cacheSelectNodes; + this.globalLogValidationExecution = other.globalLogValidationExecution; + this.rdfsSubClassReasoning = other.rdfsSubClassReasoning; + this.performanceLogging = other.performanceLogging; + this.serializableValidation = other.serializableValidation; + this.eclipseRdf4jShaclExtensions = other.eclipseRdf4jShaclExtensions; + this.dashDataShapes = other.dashDataShapes; + this.validationResultsLimitTotal = other.validationResultsLimitTotal; + this.validationResultsLimitPerConstraint = other.validationResultsLimitPerConstraint; + this.transactionalValidationLimit = other.transactionalValidationLimit; + this.validationTimeoutMillis = other.validationTimeoutMillis; + this.sparqlValidation = other.sparqlValidation; + } + + /** + * Set the contexts to use when discovering SHACL shapes. + * + * @param shapeContexts contexts to scan, or {@code null} to scan all contexts + * @return this builder instance + */ + public T shapeContexts(Resource... shapeContexts) { + this.shapeContexts = shapeContexts == null ? null : shapeContexts.clone(); + return (T) this; + } + + /** + * Enable or disable global logging of validation execution. + * + * @param loggingEnabled whether to enable logging + * @return this builder instance + */ + public T setGlobalLogValidationExecution(boolean loggingEnabled) { + this.globalLogValidationExecution = loggingEnabled; + return (T) this; + } + + /** + * Enable or disable logging of validation violations. + * + * @param logValidationViolations whether to log violations + * @return this builder instance + */ + public T setLogValidationViolations(boolean logValidationViolations) { + this.logValidationViolations = logValidationViolations; + return (T) this; + } + + /** + * Enable or disable parallel validation. + * + * @param parallelValidation whether to run validation in parallel + * @return this builder instance + */ + public T setParallelValidation(boolean parallelValidation) { + this.parallelValidation = parallelValidation; + return (T) this; + } + + /** + * Enable or disable caching of select nodes during validation. + * + * @param cacheSelectNodes whether to cache select nodes + * @return this builder instance + */ + public T setCacheSelectNodes(boolean cacheSelectNodes) { + this.cacheSelectNodes = cacheSelectNodes; + return (T) this; + } + + /** + * Enable or disable RDFS subclass reasoning during validation. + * + * @param rdfsSubClassReasoning whether to enable subclass reasoning + * @return this builder instance + */ + public T setRdfsSubClassReasoning(boolean rdfsSubClassReasoning) { + this.rdfsSubClassReasoning = rdfsSubClassReasoning; + return (T) this; + } + + /** + * Disable SHACL validation entirely. + * + * @return this builder instance + */ + public T disableValidation() { + this.validationEnabled = false; + return (T) this; + } + + /** + * Enable SHACL validation. + * + * @return this builder instance + */ + public T enableValidation() { + this.validationEnabled = true; + return (T) this; + } + + /** + * Enable or disable logging of validation plans. + * + * @param logValidationPlans whether to log validation plans + * @return this builder instance + */ + public T setLogValidationPlans(boolean logValidationPlans) { + this.logValidationPlans = logValidationPlans; + return (T) this; + } + + /** + * Enable or disable performance logging during validation. + * + * @param performanceLogging whether to log performance details + * @return this builder instance + */ + public T setPerformanceLogging(boolean performanceLogging) { + this.performanceLogging = performanceLogging; + return (T) this; + } + + /** + * Enable or disable serializable validation mode. + * + * @param serializableValidation whether to enable serializable validation + * @return this builder instance + */ + public T setSerializableValidation(boolean serializableValidation) { + this.serializableValidation = serializableValidation; + return (T) this; + } + + /** + * Enable or disable RDF4J SHACL extensions. + * + * @param eclipseRdf4jShaclExtensions whether to enable the extensions + * @return this builder instance + */ + public T setEclipseRdf4jShaclExtensions(boolean eclipseRdf4jShaclExtensions) { + this.eclipseRdf4jShaclExtensions = eclipseRdf4jShaclExtensions; + return (T) this; + } + + /** + * Enable or disable DASH data shapes support. + * + * @param dashDataShapes whether to enable DASH data shapes + * @return this builder instance + */ + public T setDashDataShapes(boolean dashDataShapes) { + this.dashDataShapes = dashDataShapes; + return (T) this; + } + + /** + * Set the maximum number of validation results per constraint. + * + * @param validationResultsLimitPerConstraint limit per constraint, or a negative value to defer to the total + * limit + * @return this builder instance + */ + public T setValidationResultsLimitPerConstraint(long validationResultsLimitPerConstraint) { + this.validationResultsLimitPerConstraint = validationResultsLimitPerConstraint; + return (T) this; + } + + /** + * Set the total maximum number of validation results in a report. + * + * @param validationResultsLimitTotal total limit, or a negative value for no limit + * @return this builder instance + */ + public T setValidationResultsLimitTotal(long validationResultsLimitTotal) { + this.validationResultsLimitTotal = validationResultsLimitTotal; + return (T) this; + } + + /** + * Set the transactional validation limit. + * + * @param transactionalValidationLimit the transactional validation limit + * @return this builder instance + */ + public T setTransactionalValidationLimit(long transactionalValidationLimit) { + this.transactionalValidationLimit = transactionalValidationLimit; + return (T) this; + } + + /** + * Set the validation timeout in milliseconds. + * + * @param validationTimeoutMillis timeout in milliseconds, or a negative value to disable the timeout + * @return this builder instance + */ + public T setValidationTimeoutMillis(long validationTimeoutMillis) { + this.validationTimeoutMillis = validationTimeoutMillis; + return (T) this; + } + + /** + * Set the SHACL shapes graphs to use when discovering shapes. + * + *

+ * Use {@link RDF4J#NIL} or {@link SESAME#NIL} to indicate the default graph. + *

+ * + * @param shapesGraphs the shapes graphs, or {@code null} to scan all graphs + * @return this builder instance + */ + public T setShapesGraphs(Set shapesGraphs) { + if (shapesGraphs == null) { + this.shapeContexts = null; + return (T) this; + } + if (shapesGraphs.isEmpty()) { + this.shapeContexts = ALL_CONTEXTS; + return (T) this; + } + this.shapeContexts = shapesGraphs.stream() + .map(g -> { + if (RDF4J.NIL.equals(g) || SESAME.NIL.equals(g)) { + return null; + } + return g; + }) + .toArray(Resource[]::new); + return (T) this; + } + + private long getEffectiveValidationResultsLimitPerConstraint() { + if (validationResultsLimitPerConstraint < 0) { + return validationResultsLimitTotal; + } + if (validationResultsLimitTotal >= 0) { + return Math.min(validationResultsLimitTotal, validationResultsLimitPerConstraint); + } + return validationResultsLimitPerConstraint; + } + + /** + * {@inheritDoc} + */ + @Override + public InternalBuilder clone() { + try { + InternalBuilder clone = (InternalBuilder) super.clone(); + clone.shapeContexts = shapeContexts == null ? null : shapeContexts.clone(); + return clone; + } catch (CloneNotSupportedException e) { + throw new AssertionError(); + } + } + } + + public static class BuilderWithShapes extends InternalBuilder implements Cloneable { + + /** + * The shapes sail used for validation. + */ + public Sail shapes; + + private BuilderWithShapes(Builder builder, Sail shapes) { + this.shapes = shapes; + setAll(builder); + } + + /** + * Build a validator that validates data against the configured shapes. + * + * @return a validator bound to the shapes sail + */ + public ValidatorWithShapes build() { + return new ValidatorWithShapes(this); + } + + /** + * {@inheritDoc} + */ + @Override + public BuilderWithShapes clone() { + BuilderWithShapes clone = (BuilderWithShapes) super.clone(); + // TODO: copy mutable state here, so the clone can't change the internals of the original + return clone; + } + } + + public static class ValidatorWithShapes { + private final BuilderWithShapes builderWithShapes; + + /** + * Create a validator that uses the supplied builder configuration. + * + * @param builderWithShapes configured builder with shapes + */ + public ValidatorWithShapes(BuilderWithShapes builderWithShapes) { + this.builderWithShapes = builderWithShapes.clone(); + } + + private ValidationReport validateLoadedData(String description, SailLoader loader) { + Sail data = loadData(description, loader); + return validate(data); + } + + /** + * Validate the supplied data sail against the configured shapes. + * + * @param dataRepo data sail to validate + * @return validation report + */ + public ValidationReport validate(Sail dataRepo) { + return validateInternal(dataRepo, builderWithShapes.shapes, builderWithShapes); + } + + /** + * Load data from a file and validate it against the configured shapes. + * + * @param dataFile data file to parse + * @param baseURI base URI for resolving relative IRIs + * @param format RDF format to use + * @return validation report + */ + public ValidationReport validate(File dataFile, String baseURI, RDFFormat format) { + Objects.requireNonNull(dataFile, "dataFile"); + Objects.requireNonNull(format, "rdfFormat"); + return validateLoadedData("file " + dataFile, connection -> connection.add(dataFile, baseURI, format)); + } + + /** + * Load data from a path and validate it against the configured shapes. + * + * @param dataPath data path to parse + * @param baseURI base URI for resolving relative IRIs + * @param format RDF format to use + * @return validation report + */ + public ValidationReport validate(Path dataPath, String baseURI, RDFFormat format) { + Objects.requireNonNull(dataPath, "dataPath"); + return validate(dataPath.toFile(), baseURI, format); + } + + /** + * Load data from a URL and validate it against the configured shapes. + * + * @param dataUrl data URL to parse + * @param baseURI base URI for resolving relative IRIs + * @param format RDF format to use + * @return validation report + */ + public ValidationReport validate(URL dataUrl, String baseURI, RDFFormat format) { + Objects.requireNonNull(dataUrl, "dataUrl"); + Objects.requireNonNull(format, "rdfFormat"); + return validateLoadedData("URL " + dataUrl, connection -> connection.add(dataUrl, baseURI, format)); + } + + /** + * Load data from an input stream and validate it against the configured shapes. The input stream is not closed + * by this method. + * + * @param dataInputStream data input stream to parse + * @param baseURI base URI for resolving relative IRIs + * @param format RDF format to use + * @return validation report + */ + public ValidationReport validate(InputStream dataInputStream, String baseURI, RDFFormat format) { + Objects.requireNonNull(dataInputStream, "dataInputStream"); + Objects.requireNonNull(format, "rdfFormat"); + return validateLoadedData("input stream", connection -> connection.add(dataInputStream, baseURI, format)); + } + + /** + * Load data from RDF content in a string and validate it against the configured shapes. + * + * @param data RDF content to parse + * @param baseURI base URI for resolving relative IRIs + * @param format RDF format to use + * @return validation report + */ + public ValidationReport validate(String data, String baseURI, RDFFormat format) { + Objects.requireNonNull(data, "data"); + Objects.requireNonNull(format, "rdfFormat"); + return validateLoadedData("string content", + connection -> connection.add(new StringReader(data), baseURI, format)); + } + + /** + * Load data from a file using an auto-detected RDF format and validate it against the configured shapes. + * + * @param dataFile data file to parse + * @param baseURI base URI for resolving relative IRIs + * @return validation report + */ + public ValidationReport validate(File dataFile, String baseURI) { + Objects.requireNonNull(dataFile, "dataFile"); + RDFFormat format = detectRdfFormat("file " + dataFile, dataFile.getName(), baseURI); + return validate(dataFile, baseURI, format); + } + + /** + * Load data from a path using an auto-detected RDF format and validate it against the configured shapes. + * + * @param dataPath data path to parse + * @param baseURI base URI for resolving relative IRIs + * @return validation report + */ + public ValidationReport validate(Path dataPath, String baseURI) { + Objects.requireNonNull(dataPath, "dataPath"); + RDFFormat format = detectRdfFormat("path " + dataPath, dataPath.getFileName().toString(), baseURI); + return validate(dataPath, baseURI, format); + } + + /** + * Load data from a URL using an auto-detected RDF format and validate it against the configured shapes. + * + * @param dataUrl data URL to parse + * @param baseURI base URI for resolving relative IRIs + * @return validation report + */ + public ValidationReport validate(URL dataUrl, String baseURI) { + Objects.requireNonNull(dataUrl, "dataUrl"); + RDFFormat format = detectRdfFormat("URL " + dataUrl, dataUrl.getPath(), baseURI); + return validate(dataUrl, baseURI, format); + } + + /** + * Load data from an input stream using an auto-detected RDF format and validate it against the configured + * shapes. The input stream is not closed by this method. + * + * @param dataInputStream data input stream to parse + * @param baseURI base URI for resolving relative IRIs + * @return validation report + */ + public ValidationReport validate(InputStream dataInputStream, String baseURI) { + Objects.requireNonNull(dataInputStream, "dataInputStream"); + RDFFormat format = detectRdfFormat("input stream", baseURI); + return validate(dataInputStream, baseURI, format); + } + + /** + * Load data from RDF content in a string using an auto-detected RDF format and validate it against the + * configured shapes. + * + * @param data RDF content to parse + * @param baseURI base URI for resolving relative IRIs + * @return validation report + */ + public ValidationReport validate(String data, String baseURI) { + Objects.requireNonNull(data, "data"); + RDFFormat format = detectRdfFormat("string content", baseURI); + return validate(data, baseURI, format); + } + + /** + * Load data from a file using the supplied RDF format and validate it against the configured shapes. + * + * @param dataFile data file to parse + * @param format RDF format to use + * @return validation report + */ + public ValidationReport validate(File dataFile, RDFFormat format) { + Objects.requireNonNull(dataFile, "dataFile"); + Objects.requireNonNull(format, "rdfFormat"); + return validateLoadedData("file " + dataFile, connection -> connection.add(dataFile, format)); + } + + /** + * Load data from a path using the supplied RDF format and validate it against the configured shapes. + * + * @param dataPath data path to parse + * @param format RDF format to use + * @return validation report + */ + public ValidationReport validate(Path dataPath, RDFFormat format) { + Objects.requireNonNull(dataPath, "dataPath"); + return validate(dataPath.toFile(), format); + } + + /** + * Load data from a URL using the supplied RDF format and validate it against the configured shapes. + * + * @param dataUrl data URL to parse + * @param format RDF format to use + * @return validation report + */ + public ValidationReport validate(URL dataUrl, RDFFormat format) { + Objects.requireNonNull(dataUrl, "dataUrl"); + Objects.requireNonNull(format, "rdfFormat"); + return validateLoadedData("URL " + dataUrl, connection -> connection.add(dataUrl, format)); + } + + /** + * Load data from an input stream using the supplied RDF format and validate it against the configured shapes. + * The input stream is not closed by this method. + * + * @param dataInputStream data input stream to parse + * @param format RDF format to use + * @return validation report + */ + public ValidationReport validate(InputStream dataInputStream, RDFFormat format) { + Objects.requireNonNull(dataInputStream, "dataInputStream"); + Objects.requireNonNull(format, "rdfFormat"); + return validateLoadedData("input stream", connection -> connection.add(dataInputStream, format)); + } + + /** + * Load data from RDF content in a string using the supplied RDF format and validate it against the configured + * shapes. + * + * @param data RDF content to parse + * @param format RDF format to use + * @return validation report + */ + public ValidationReport validate(String data, RDFFormat format) { + Objects.requireNonNull(data, "data"); + Objects.requireNonNull(format, "rdfFormat"); + return validateLoadedData("string content", connection -> connection.add(new StringReader(data), format)); + } + + /** + * Load data from a file using an auto-detected RDF format and validate it against the configured shapes. + * + * @param dataFile data file to parse + * @return validation report + */ + public ValidationReport validate(File dataFile) { + Objects.requireNonNull(dataFile, "dataFile"); + RDFFormat format = detectRdfFormat("file " + dataFile, dataFile.getName()); + return validate(dataFile, format); + } + + /** + * Load data from a path using an auto-detected RDF format and validate it against the configured shapes. + * + * @param dataPath data path to parse + * @return validation report + */ + public ValidationReport validate(Path dataPath) { + Objects.requireNonNull(dataPath, "dataPath"); + RDFFormat format = detectRdfFormat("path " + dataPath, dataPath.getFileName().toString()); + return validate(dataPath, format); + } + + /** + * Load data from a URL using an auto-detected RDF format and validate it against the configured shapes. + * + * @param dataUrl data URL to parse + * @return validation report + */ + public ValidationReport validate(URL dataUrl) { + Objects.requireNonNull(dataUrl, "dataUrl"); + RDFFormat format = detectRdfFormat("URL " + dataUrl, dataUrl.getPath()); + return validate(dataUrl, format); + } + + } + + public static class Validator { + private final Builder builder; + + /** + * Create a validator that uses the supplied builder configuration. + * + * @param builder configured builder + */ + public Validator(Builder builder) { + this.builder = builder.clone(); + } + + private ValidationReport validateLoadedData(String description, SailLoader loader, Sail shapesRepo) { + Objects.requireNonNull(shapesRepo, "shapesRepo"); + Sail data = loadData(description, loader); + return validate(data, shapesRepo); + } + + /** + * Validate the supplied data sail against the supplied shapes sail. + * + * @param dataRepo data sail to validate + * @param shapesRepo shapes sail + * @return validation report + */ + public ValidationReport validate(Sail dataRepo, Sail shapesRepo) { + return validateInternal(dataRepo, shapesRepo, builder); + } + + /** + * Load data from a file and validate it against the supplied shapes sail. + * + * @param dataFile data file to parse + * @param baseURI base URI for resolving relative IRIs + * @param format RDF format to use + * @param shapesRepo shapes sail + * @return validation report + */ + public ValidationReport validate(File dataFile, String baseURI, RDFFormat format, Sail shapesRepo) { + Objects.requireNonNull(dataFile, "dataFile"); + Objects.requireNonNull(format, "rdfFormat"); + return validateLoadedData("file " + dataFile, connection -> connection.add(dataFile, baseURI, format), + shapesRepo); + } + + /** + * Load data from a path and validate it against the supplied shapes sail. + * + * @param dataPath data path to parse + * @param baseURI base URI for resolving relative IRIs + * @param format RDF format to use + * @param shapesRepo shapes sail + * @return validation report + */ + public ValidationReport validate(Path dataPath, String baseURI, RDFFormat format, Sail shapesRepo) { + Objects.requireNonNull(dataPath, "dataPath"); + return validate(dataPath.toFile(), baseURI, format, shapesRepo); + } + + /** + * Load data from a URL and validate it against the supplied shapes sail. + * + * @param dataUrl data URL to parse + * @param baseURI base URI for resolving relative IRIs + * @param format RDF format to use + * @param shapesRepo shapes sail + * @return validation report + */ + public ValidationReport validate(URL dataUrl, String baseURI, RDFFormat format, Sail shapesRepo) { + Objects.requireNonNull(dataUrl, "dataUrl"); + Objects.requireNonNull(format, "rdfFormat"); + return validateLoadedData("URL " + dataUrl, connection -> connection.add(dataUrl, baseURI, format), + shapesRepo); + } + + /** + * Load data from an input stream and validate it against the supplied shapes sail. The input stream is not + * closed by this method. + * + * @param dataInputStream data input stream to parse + * @param baseURI base URI for resolving relative IRIs + * @param format RDF format to use + * @param shapesRepo shapes sail + * @return validation report + */ + public ValidationReport validate(InputStream dataInputStream, String baseURI, RDFFormat format, + Sail shapesRepo) { + Objects.requireNonNull(dataInputStream, "dataInputStream"); + Objects.requireNonNull(format, "rdfFormat"); + return validateLoadedData("input stream", connection -> connection.add(dataInputStream, baseURI, format), + shapesRepo); + } + + /** + * Load data from RDF content in a string and validate it against the supplied shapes sail. + * + * @param data RDF content to parse + * @param baseURI base URI for resolving relative IRIs + * @param format RDF format to use + * @param shapesRepo shapes sail + * @return validation report + */ + public ValidationReport validate(String data, String baseURI, RDFFormat format, Sail shapesRepo) { + Objects.requireNonNull(data, "data"); + Objects.requireNonNull(format, "rdfFormat"); + return validateLoadedData("string content", + connection -> connection.add(new StringReader(data), baseURI, format), shapesRepo); + } + + /** + * Load data from a file using an auto-detected RDF format and validate it against the supplied shapes sail. + * + * @param dataFile data file to parse + * @param baseURI base URI for resolving relative IRIs + * @param shapesRepo shapes sail + * @return validation report + */ + public ValidationReport validate(File dataFile, String baseURI, Sail shapesRepo) { + Objects.requireNonNull(dataFile, "dataFile"); + RDFFormat format = detectRdfFormat("file " + dataFile, dataFile.getName(), baseURI); + return validate(dataFile, baseURI, format, shapesRepo); + } + + /** + * Load data from a path using an auto-detected RDF format and validate it against the supplied shapes sail. + * + * @param dataPath data path to parse + * @param baseURI base URI for resolving relative IRIs + * @param shapesRepo shapes sail + * @return validation report + */ + public ValidationReport validate(Path dataPath, String baseURI, Sail shapesRepo) { + Objects.requireNonNull(dataPath, "dataPath"); + RDFFormat format = detectRdfFormat("path " + dataPath, dataPath.getFileName().toString(), baseURI); + return validate(dataPath, baseURI, format, shapesRepo); + } + + /** + * Load data from a URL using an auto-detected RDF format and validate it against the supplied shapes sail. + * + * @param dataUrl data URL to parse + * @param baseURI base URI for resolving relative IRIs + * @param shapesRepo shapes sail + * @return validation report + */ + public ValidationReport validate(URL dataUrl, String baseURI, Sail shapesRepo) { + Objects.requireNonNull(dataUrl, "dataUrl"); + RDFFormat format = detectRdfFormat("URL " + dataUrl, dataUrl.getPath(), baseURI); + return validate(dataUrl, baseURI, format, shapesRepo); + } + + /** + * Load data from an input stream using an auto-detected RDF format and validate it against the supplied shapes + * sail. The input stream is not closed by this method. + * + * @param dataInputStream data input stream to parse + * @param baseURI base URI for resolving relative IRIs + * @param shapesRepo shapes sail + * @return validation report + */ + public ValidationReport validate(InputStream dataInputStream, String baseURI, Sail shapesRepo) { + Objects.requireNonNull(dataInputStream, "dataInputStream"); + RDFFormat format = detectRdfFormat("input stream", baseURI); + return validate(dataInputStream, baseURI, format, shapesRepo); + } + + /** + * Load data from RDF content in a string using an auto-detected RDF format and validate it against the supplied + * shapes sail. + * + * @param data RDF content to parse + * @param baseURI base URI for resolving relative IRIs + * @param shapesRepo shapes sail + * @return validation report + */ + public ValidationReport validate(String data, String baseURI, Sail shapesRepo) { + Objects.requireNonNull(data, "data"); + RDFFormat format = detectRdfFormat("string content", baseURI); + return validate(data, baseURI, format, shapesRepo); + } + + /** + * Load data from a file using the supplied RDF format and validate it against the supplied shapes sail. + * + * @param dataFile data file to parse + * @param format RDF format to use + * @param shapesRepo shapes sail + * @return validation report + */ + public ValidationReport validate(File dataFile, RDFFormat format, Sail shapesRepo) { + Objects.requireNonNull(dataFile, "dataFile"); + Objects.requireNonNull(format, "rdfFormat"); + return validateLoadedData("file " + dataFile, connection -> connection.add(dataFile, format), shapesRepo); + } + + /** + * Load data from a path using the supplied RDF format and validate it against the supplied shapes sail. + * + * @param dataPath data path to parse + * @param format RDF format to use + * @param shapesRepo shapes sail + * @return validation report + */ + public ValidationReport validate(Path dataPath, RDFFormat format, Sail shapesRepo) { + Objects.requireNonNull(dataPath, "dataPath"); + return validate(dataPath.toFile(), format, shapesRepo); + } + + /** + * Load data from a URL using the supplied RDF format and validate it against the supplied shapes sail. + * + * @param dataUrl data URL to parse + * @param format RDF format to use + * @param shapesRepo shapes sail + * @return validation report + */ + public ValidationReport validate(URL dataUrl, RDFFormat format, Sail shapesRepo) { + Objects.requireNonNull(dataUrl, "dataUrl"); + Objects.requireNonNull(format, "rdfFormat"); + return validateLoadedData("URL " + dataUrl, connection -> connection.add(dataUrl, format), shapesRepo); + } + + /** + * Load data from an input stream using the supplied RDF format and validate it against the supplied shapes + * sail. The input stream is not closed by this method. + * + * @param dataInputStream data input stream to parse + * @param format RDF format to use + * @param shapesRepo shapes sail + * @return validation report + */ + public ValidationReport validate(InputStream dataInputStream, RDFFormat format, Sail shapesRepo) { + Objects.requireNonNull(dataInputStream, "dataInputStream"); + Objects.requireNonNull(format, "rdfFormat"); + return validateLoadedData("input stream", connection -> connection.add(dataInputStream, format), + shapesRepo); + } + + /** + * Load data from RDF content in a string using the supplied RDF format and validate it against the supplied + * shapes sail. + * + * @param data RDF content to parse + * @param format RDF format to use + * @param shapesRepo shapes sail + * @return validation report + */ + public ValidationReport validate(String data, RDFFormat format, Sail shapesRepo) { + Objects.requireNonNull(data, "data"); + Objects.requireNonNull(format, "rdfFormat"); + return validateLoadedData("string content", connection -> connection.add(new StringReader(data), format), + shapesRepo); + } + + /** + * Load data from a file using an auto-detected RDF format and validate it against the supplied shapes sail. + * + * @param dataFile data file to parse + * @param shapesRepo shapes sail + * @return validation report + */ + public ValidationReport validate(File dataFile, Sail shapesRepo) { + Objects.requireNonNull(dataFile, "dataFile"); + RDFFormat format = detectRdfFormat("file " + dataFile, dataFile.getName()); + return validate(dataFile, format, shapesRepo); + } + + /** + * Load data from a path using an auto-detected RDF format and validate it against the supplied shapes sail. + * + * @param dataPath data path to parse + * @param shapesRepo shapes sail + * @return validation report + */ + public ValidationReport validate(Path dataPath, Sail shapesRepo) { + Objects.requireNonNull(dataPath, "dataPath"); + RDFFormat format = detectRdfFormat("path " + dataPath, dataPath.getFileName().toString()); + return validate(dataPath, format, shapesRepo); + } + + /** + * Load data from a URL using an auto-detected RDF format and validate it against the supplied shapes sail. + * + * @param dataUrl data URL to parse + * @param shapesRepo shapes sail + * @return validation report + */ + public ValidationReport validate(URL dataUrl, Sail shapesRepo) { + Objects.requireNonNull(dataUrl, "dataUrl"); + RDFFormat format = detectRdfFormat("URL " + dataUrl, dataUrl.getPath()); + return validate(dataUrl, format, shapesRepo); + } + + /** + * Load shapes from a file and validate the supplied data sail. + * + * @param dataRepo data sail to validate + * @param shapesFile shapes file to parse + * @param baseURI base URI for resolving relative IRIs + * @param format RDF format to use + * @return validation report + */ + public ValidationReport validate(Sail dataRepo, File shapesFile, String baseURI, RDFFormat format) { + return builder.withShapes(shapesFile, baseURI, format).build().validate(dataRepo); + } + + /** + * Load shapes from a path and validate the supplied data sail. + * + * @param dataRepo data sail to validate + * @param shapesPath shapes path to parse + * @param baseURI base URI for resolving relative IRIs + * @param format RDF format to use + * @return validation report + */ + public ValidationReport validate(Sail dataRepo, Path shapesPath, String baseURI, RDFFormat format) { + return builder.withShapes(shapesPath, baseURI, format).build().validate(dataRepo); + } + + /** + * Load shapes from a URL and validate the supplied data sail. + * + * @param dataRepo data sail to validate + * @param shapesUrl shapes URL to parse + * @param baseURI base URI for resolving relative IRIs + * @param format RDF format to use + * @return validation report + */ + public ValidationReport validate(Sail dataRepo, URL shapesUrl, String baseURI, RDFFormat format) { + return builder.withShapes(shapesUrl, baseURI, format).build().validate(dataRepo); + } + + /** + * Load shapes from an input stream and validate the supplied data sail. The input stream is not closed by this + * method. + * + * @param dataRepo data sail to validate + * @param shapesInputStream shapes input stream to parse + * @param baseURI base URI for resolving relative IRIs + * @param format RDF format to use + * @return validation report + */ + public ValidationReport validate(Sail dataRepo, InputStream shapesInputStream, String baseURI, + RDFFormat format) { + return builder.withShapes(shapesInputStream, baseURI, format).build().validate(dataRepo); + } + + /** + * Load shapes from RDF content in a string and validate the supplied data sail. + * + * @param dataRepo data sail to validate + * @param shapes RDF content for shapes + * @param baseURI base URI for resolving relative IRIs + * @param format RDF format to use + * @return validation report + */ + public ValidationReport validate(Sail dataRepo, String shapes, String baseURI, RDFFormat format) { + return builder.withShapes(shapes, baseURI, format).build().validate(dataRepo); + } + + /** + * Load shapes from a file using an auto-detected RDF format and validate the supplied data sail. + * + * @param dataRepo data sail to validate + * @param shapesFile shapes file to parse + * @param baseURI base URI for resolving relative IRIs + * @return validation report + */ + public ValidationReport validate(Sail dataRepo, File shapesFile, String baseURI) { + return builder.withShapes(shapesFile, baseURI).build().validate(dataRepo); + } + + /** + * Load shapes from a path using an auto-detected RDF format and validate the supplied data sail. + * + * @param dataRepo data sail to validate + * @param shapesPath shapes path to parse + * @param baseURI base URI for resolving relative IRIs + * @return validation report + */ + public ValidationReport validate(Sail dataRepo, Path shapesPath, String baseURI) { + return builder.withShapes(shapesPath, baseURI).build().validate(dataRepo); + } + + /** + * Load shapes from a URL using an auto-detected RDF format and validate the supplied data sail. + * + * @param dataRepo data sail to validate + * @param shapesUrl shapes URL to parse + * @param baseURI base URI for resolving relative IRIs + * @return validation report + */ + public ValidationReport validate(Sail dataRepo, URL shapesUrl, String baseURI) { + return builder.withShapes(shapesUrl, baseURI).build().validate(dataRepo); + } + + /** + * Load shapes from an input stream using an auto-detected RDF format and validate the supplied data sail. The + * input stream is not closed by this method. + * + * @param dataRepo data sail to validate + * @param shapesInputStream shapes input stream to parse + * @param baseURI base URI for resolving relative IRIs + * @return validation report + */ + public ValidationReport validate(Sail dataRepo, InputStream shapesInputStream, String baseURI) { + return builder.withShapes(shapesInputStream, baseURI).build().validate(dataRepo); + } + + /** + * Load shapes from RDF content in a string using an auto-detected RDF format and validate the supplied data + * sail. + * + * @param dataRepo data sail to validate + * @param shapes RDF content for shapes + * @param baseURI base URI for resolving relative IRIs + * @return validation report + */ + public ValidationReport validate(Sail dataRepo, String shapes, String baseURI) { + return builder.withShapes(shapes, baseURI).build().validate(dataRepo); + } + + /** + * Load shapes from a file using the supplied RDF format and validate the supplied data sail. + * + * @param dataRepo data sail to validate + * @param shapesFile shapes file to parse + * @param format RDF format to use + * @return validation report + */ + public ValidationReport validate(Sail dataRepo, File shapesFile, RDFFormat format) { + return builder.withShapes(shapesFile, format).build().validate(dataRepo); + } + + /** + * Load shapes from a path using the supplied RDF format and validate the supplied data sail. + * + * @param dataRepo data sail to validate + * @param shapesPath shapes path to parse + * @param format RDF format to use + * @return validation report + */ + public ValidationReport validate(Sail dataRepo, Path shapesPath, RDFFormat format) { + return builder.withShapes(shapesPath, format).build().validate(dataRepo); + } - RdfsSubClassOfReasoner reasoner; + /** + * Load shapes from a URL using the supplied RDF format and validate the supplied data sail. + * + * @param dataRepo data sail to validate + * @param shapesUrl shapes URL to parse + * @param format RDF format to use + * @return validation report + */ + public ValidationReport validate(Sail dataRepo, URL shapesUrl, RDFFormat format) { + return builder.withShapes(shapesUrl, format).build().validate(dataRepo); + } + + /** + * Load shapes from an input stream using the supplied RDF format and validate the supplied data sail. The input + * stream is not closed by this method. + * + * @param dataRepo data sail to validate + * @param shapesInputStream shapes input stream to parse + * @param format RDF format to use + * @return validation report + */ + public ValidationReport validate(Sail dataRepo, InputStream shapesInputStream, RDFFormat format) { + return builder.withShapes(shapesInputStream, format).build().validate(dataRepo); + } + + /** + * Load shapes from RDF content in a string using the supplied RDF format and validate the supplied data sail. + * + * @param dataRepo data sail to validate + * @param shapes RDF content for shapes + * @param format RDF format to use + * @return validation report + */ + public ValidationReport validate(Sail dataRepo, String shapes, RDFFormat format) { + return builder.withShapes(shapes, format).build().validate(dataRepo); + } + + /** + * Load shapes from a file using an auto-detected RDF format and validate the supplied data sail. + * + * @param dataRepo data sail to validate + * @param shapesFile shapes file to parse + * @return validation report + */ + public ValidationReport validate(Sail dataRepo, File shapesFile) { + return builder.withShapes(shapesFile).build().validate(dataRepo); + } + + /** + * Load shapes from a path using an auto-detected RDF format and validate the supplied data sail. + * + * @param dataRepo data sail to validate + * @param shapesPath shapes path to parse + * @return validation report + */ + public ValidationReport validate(Sail dataRepo, Path shapesPath) { + return builder.withShapes(shapesPath).build().validate(dataRepo); + } + + /** + * Load shapes from a URL using an auto-detected RDF format and validate the supplied data sail. + * + * @param dataRepo data sail to validate + * @param shapesUrl shapes URL to parse + * @return validation report + */ + public ValidationReport validate(Sail dataRepo, URL shapesUrl) { + return builder.withShapes(shapesUrl).build().validate(dataRepo); + } + + } + + private static ValidationReport validateInternal(Sail dataRepo, Sail shapesRepo, InternalBuilder settings) { + Objects.requireNonNull(dataRepo, "dataRepo"); + Objects.requireNonNull(shapesRepo, "shapesRepo"); + Objects.requireNonNull(settings, "settings"); + + if (!settings.validationEnabled) { + return new ValidationReport(true); + } + + if (settings.validationTimeoutMillis >= 0) { + return validateInternalWithTimeout(dataRepo, shapesRepo, settings); + } - try (SailConnection shapesConnection = shapesRepo.getConnection()) { - reasoner = RdfsSubClassOfReasoner.createReasoner( - dataRepoConnection, shapesConnection, - new ValidationSettings(ALL_CONTEXTS, false, true, false)); + return validateInternalWithoutTimeout(dataRepo, shapesRepo, settings); + } + + private static ValidationReport validateInternalWithTimeout(Sail dataRepo, Sail shapesRepo, + InternalBuilder settings) { + long validationTimeoutMillis = settings.validationTimeoutMillis; + + ExecutorService executorService = Executors.newSingleThreadExecutor(runnable -> { + Thread thread = new Thread(runnable, "ShaclValidator-timeout"); + thread.setDaemon(true); + return thread; + }); + + Future future = null; + + try { + future = executorService.submit(() -> { + ValidationReport report = validateInternalWithoutTimeout(dataRepo, shapesRepo, settings); + // Fully evaluate the report so the timeout covers the entire validation process. + report.conforms(); + return report; + }); + + return future.get(validationTimeoutMillis, TimeUnit.MILLISECONDS); + } catch (TimeoutException e) { + future.cancel(true); + throw new SailException( + "SHACL validation timed out after " + validationTimeoutMillis + " ms", e); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new InterruptedSailException(e); + } catch (ExecutionException e) { + Throwable cause = e.getCause(); + if (cause instanceof RuntimeException) { + throw (RuntimeException) cause; } + if (cause instanceof Error) { + throw (Error) cause; + } + throw new SailException(cause); + } finally { + executorService.shutdownNow(); + } + } + + private static ValidationReport validateInternalWithoutTimeout(Sail dataRepo, Sail shapesRepo, + InternalBuilder settings) { + List shapes = readShapes(shapesRepo, settings); + if (shapes.isEmpty()) { + return new ValidationReport(true); + } + + try (SailConnection dataRepoConnection = dataRepo.getConnection()) { + dataRepoConnection.begin(IsolationLevels.NONE); + try { + RdfsSubClassOfReasoner reasoner; + SailConnection baseConnection = dataRepoConnection; + ConnectionsGroup.RdfsSubClassOfReasonerProvider rdfsSubClassOfReasonerProvider = null; + + if (settings.rdfsSubClassReasoning) { + try (SailConnection shapesConnection = shapesRepo.getConnection()) { + shapesConnection.begin(IsolationLevels.NONE); + try { + reasoner = RdfsSubClassOfReasoner.createReasoner(dataRepoConnection, shapesConnection, + new ValidationSettings(ALL_CONTEXTS, settings.logValidationPlans, true, + settings.performanceLogging)); + } finally { + shapesConnection.commit(); + } + } + + RdfsSubClassOfReasoner finalReasoner = reasoner; + rdfsSubClassOfReasonerProvider = () -> finalReasoner; + baseConnection = new VerySimpleRdfsBackwardsChainingConnection(dataRepoConnection, finalReasoner); + } - VerySimpleRdfsBackwardsChainingConnection verySimpleRdfsBackwardsChainingConnection = new VerySimpleRdfsBackwardsChainingConnection( - dataRepoConnection, reasoner); + ShaclSailConnection.Settings transactionSettings = new ShaclSailConnection.Settings( + settings.cacheSelectNodes, + settings.validationEnabled, + settings.parallelValidation, + IsolationLevels.NONE); - return performValidation(shapes, new ConnectionsGroup(verySimpleRdfsBackwardsChainingConnection, null, - null, null, new Stats(), () -> reasoner, - new ShaclSailConnection.Settings(true, true, true, IsolationLevels.NONE), true)); - } catch (Throwable e) { - logger.warn("Failed to validate shapes", e); - throw e; + try (ConnectionsGroup connectionsGroup = new ConnectionsGroup( + baseConnection, + null, + null, + null, + new Stats(), + rdfsSubClassOfReasonerProvider, + transactionSettings, + settings.sparqlValidation)) { + return performValidation(shapes, connectionsGroup, settings); + } + } finally { + dataRepoConnection.commit(); + } } + } + + private static List readShapes(Sail shapesRepo, InternalBuilder settings) { + Resource[] shapeContexts = settings.shapeContexts == null ? ALL_CONTEXTS : settings.shapeContexts; + Shape.ParseSettings parseSettings = new Shape.ParseSettings(settings.eclipseRdf4jShaclExtensions, + settings.dashDataShapes); + + try (SailConnection shapesConnection = shapesRepo.getConnection()) { + shapesConnection.begin(IsolationLevels.NONE); + try (ShapeSource rootShapeSource = new CombinedShapeSource(shapesConnection, shapesConnection)) { + ShapeSource configuredShapeSource = rootShapeSource.withContext(shapeContexts); + List shapes = Shape.Factory.getShapes(configuredShapeSource, parseSettings); + if (!shapes.isEmpty()) { + return shapes; + } + + boolean hasMappings = shapesConnection.hasStatement(null, SHACL.SHAPES_GRAPH, null, false, + shapeContexts) + || shapesConnection.hasStatement(null, RDF.TYPE, RSX.DataAndShapesGraphLink, false, + shapeContexts); + if (hasMappings) { + return shapes; + } + LinkedHashSet fallbackShapesGraphs = new LinkedHashSet<>(); + if (settings.shapeContexts == null) { + fallbackShapesGraphs.add(null); + try (var contextIds = shapesConnection.getContextIDs()) { + while (contextIds.hasNext()) { + fallbackShapesGraphs.add(contextIds.next()); + } + } + } else { + fallbackShapesGraphs.addAll(Arrays.asList(shapeContexts)); + } + + Cache cache = new Cache(); + List parsed = new ArrayList<>(); + for (Resource shapesGraph : fallbackShapesGraphs) { + parsed.addAll(Shape.Factory.getShapesInContext(rootShapeSource, parseSettings, cache, ALL_CONTEXTS, + new Resource[] { shapesGraph })); + } + return Shape.Factory.getShapes(parsed); + } finally { + shapesConnection.commit(); + } + } } - private static ValidationReport performValidation(List shapes, - ConnectionsGroup connectionsGroup) { + private static ValidationReport performValidation(List shapes, ConnectionsGroup connectionsGroup, + InternalBuilder settings) { - List collect = shapes + long effectiveValidationResultsLimitPerConstraint = settings.getEffectiveValidationResultsLimitPerConstraint(); + long validationResultsLimitTotal = settings.validationResultsLimitTotal; + + List validationResultIterators = shapes .stream() .map(contextWithShape -> new ShapeValidationContainer( contextWithShape.getShape(), () -> contextWithShape.getShape() .generatePlans(connectionsGroup, - new ValidationSettings(contextWithShape.getDataGraph(), false, true, false)), - false, false, 1000, false, false, logger, + new ValidationSettings(contextWithShape.getDataGraph(), + settings.logValidationPlans, + true, settings.performanceLogging)), + settings.globalLogValidationExecution, + settings.logValidationViolations, + effectiveValidationResultsLimitPerConstraint, + settings.performanceLogging, + settings.logValidationPlans, + logger, connectionsGroup) ) .filter(ShapeValidationContainer::hasPlanNode) @@ -128,8 +1666,7 @@ private static ValidationReport performValidation(List shapes, throw new InterruptedSailException("Thread was interrupted during validation."); } - return new LazyValidationReport(collect, 10000); - + return new LazyValidationReport(validationResultIterators, validationResultsLimitTotal); } } diff --git a/core/sail/shacl/src/test/java/org/eclipse/rdf4j/sail/shacl/AbstractShaclTest.java b/core/sail/shacl/src/test/java/org/eclipse/rdf4j/sail/shacl/AbstractShaclTest.java index 5300ecc0ffd..aa14d28f687 100644 --- a/core/sail/shacl/src/test/java/org/eclipse/rdf4j/sail/shacl/AbstractShaclTest.java +++ b/core/sail/shacl/src/test/java/org/eclipse/rdf4j/sail/shacl/AbstractShaclTest.java @@ -8,9 +8,11 @@ * * SPDX-License-Identifier: BSD-3-Clause *******************************************************************************/ +// Some portions generated by Codex package org.eclipse.rdf4j.sail.shacl; +import static org.junit.jupiter.api.parallel.ExecutionMode.CONCURRENT; import static org.junit.jupiter.params.provider.Arguments.arguments; import java.io.File; @@ -36,7 +38,6 @@ import org.apache.commons.io.FileUtils; import org.apache.commons.io.IOUtils; -import org.apache.commons.lang3.reflect.FieldUtils; import org.apache.jena.query.Dataset; import org.apache.jena.query.DatasetFactory; import org.apache.jena.riot.RDFDataMgr; @@ -79,11 +80,9 @@ import org.eclipse.rdf4j.sail.shacl.ShaclSail.TransactionSettings.ValidationApproach; import org.eclipse.rdf4j.sail.shacl.ast.ContextWithShape; import org.eclipse.rdf4j.sail.shacl.results.ValidationReport; -import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.parallel.Isolated; +import org.junit.jupiter.api.parallel.Execution; import org.junit.jupiter.params.provider.Arguments; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -100,8 +99,7 @@ /** * @author Håvard Ottestad */ -@Isolated("Because we are modifying the static CONTEXTS field in the ShaclValidator class") -//@Execution(CONCURRENT) +@Execution(CONCURRENT) abstract public class AbstractShaclTest { private static final Logger logger = LoggerFactory.getLogger(AbstractShaclTest.class); @@ -120,8 +118,8 @@ abstract public class AbstractShaclTest { "test-cases/path/zeroOrMorePath", "test-cases/minCount/zeroOrMorePath", "test-cases/path/zeroOrOnePath" - ); + public static final List ISOLATION_LEVELS = List.of( IsolationLevels.NONE, IsolationLevels.SNAPSHOT, @@ -298,25 +296,6 @@ private static List getTestsToRun() { return individualTestCases; } - @BeforeAll - static void beforeAll() throws IllegalAccessException { - IRI[] shapesGraphs = SHAPE_GRAPHS.stream() - .map(g -> { - if (g.equals(RDF4J.NIL)) { - return null; - } - return g; - }) - .toArray(IRI[]::new); - - FieldUtils.writeDeclaredStaticField(ShaclValidator.class, "SHAPE_CONTEXTS", shapesGraphs, true); - } - - @AfterAll - static void afterAll() throws IllegalAccessException { - FieldUtils.writeDeclaredStaticField(ShaclValidator.class, "SHAPE_CONTEXTS", new Resource[] {}, true); - } - @AfterEach void afterEach() { fullLogging = false; @@ -460,12 +439,25 @@ void runWithShaclValidator(TestCase testCase) { printCurrentState(dataRepo); - ValidationReport validationReport1 = ShaclValidator.validate(dataRepo.getSail(), shapesRepo.getSail()); + ShaclValidator.ValidatorWithShapes validator = ShaclValidator.builder() + .setLogValidationPlans(fullLogging) + .setCacheSelectNodes(true) + .setParallelValidation(false) + .setLogValidationViolations(fullLogging) + .setGlobalLogValidationExecution(fullLogging) + .setEclipseRdf4jShaclExtensions(true) + .setDashDataShapes(true) + .setPerformanceLogging(false) + .setShapesGraphs(SHAPE_GRAPHS) + .withShapes(shapesRepo.getSail()) + .build(); + + ValidationReport validationReport1 = validator.validate(dataRepo.getSail()); Assertions.assertEquals(testCase.expectedResult == ExpectedResult.valid, validationReport1.conforms(), "Validation result does not match expected result"); - ValidationReport validationReport2 = ShaclValidator.validate(dataRepo.getSail(), shapesRepo.getSail()); + ValidationReport validationReport2 = validator.validate(dataRepo.getSail()); Assertions.assertEquals(testCase.expectedResult == ExpectedResult.valid, validationReport2.conforms(), "Validation result does not match expected result"); diff --git a/core/sail/shacl/src/test/java/org/eclipse/rdf4j/sail/shacl/ShaclTestWithoutRdfsReasonerTest.java b/core/sail/shacl/src/test/java/org/eclipse/rdf4j/sail/shacl/ShaclTestWithoutRdfsReasonerTest.java index 51815febcfb..a979fce8958 100644 --- a/core/sail/shacl/src/test/java/org/eclipse/rdf4j/sail/shacl/ShaclTestWithoutRdfsReasonerTest.java +++ b/core/sail/shacl/src/test/java/org/eclipse/rdf4j/sail/shacl/ShaclTestWithoutRdfsReasonerTest.java @@ -31,14 +31,12 @@ public class ShaclTestWithoutRdfsReasonerTest extends AbstractShaclTest { @BeforeAll - public static void beforeAll() throws IllegalAccessException { - AbstractShaclTest.beforeAll(); + public static void beforeAll() { ParentReferenceChecker.skip = true; } @AfterAll - public static void afterAll() throws IllegalAccessException { - AbstractShaclTest.afterAll(); + public static void afterAll() { ParentReferenceChecker.skip = false; } diff --git a/core/sail/shacl/src/test/java/org/eclipse/rdf4j/sail/shacl/ShaclValidatorFluentApiTest.java b/core/sail/shacl/src/test/java/org/eclipse/rdf4j/sail/shacl/ShaclValidatorFluentApiTest.java new file mode 100644 index 00000000000..1548c62c79b --- /dev/null +++ b/core/sail/shacl/src/test/java/org/eclipse/rdf4j/sail/shacl/ShaclValidatorFluentApiTest.java @@ -0,0 +1,1573 @@ +/******************************************************************************* + * 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 + *******************************************************************************/ +// Some portions generated by Codex + +package org.eclipse.rdf4j.sail.shacl; + +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.InputStream; +import java.io.StringReader; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.CountDownLatch; + +import org.eclipse.rdf4j.model.IRI; +import org.eclipse.rdf4j.model.Resource; +import org.eclipse.rdf4j.model.ValueFactory; +import org.eclipse.rdf4j.model.impl.SimpleValueFactory; +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; +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.Sail; +import org.eclipse.rdf4j.sail.SailConnection; +import org.eclipse.rdf4j.sail.SailException; +import org.eclipse.rdf4j.sail.helpers.AbstractSail; +import org.eclipse.rdf4j.sail.memory.MemoryStore; +import org.eclipse.rdf4j.sail.shacl.results.ValidationReport; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Timeout; +import org.junit.jupiter.api.io.TempDir; + +class ShaclValidatorFluentApiTest { + + @Test + void builderExposesShaclSailConfigurationMethods() { + assertAll( + () -> assertHasMethod(ShaclValidator.Builder.class, "setGlobalLogValidationExecution", boolean.class), + () -> assertHasMethod(ShaclValidator.Builder.class, "setLogValidationViolations", boolean.class), + () -> assertHasMethod(ShaclValidator.Builder.class, "setParallelValidation", boolean.class), + () -> assertHasMethod(ShaclValidator.Builder.class, "setCacheSelectNodes", boolean.class), + () -> assertHasMethod(ShaclValidator.Builder.class, "setRdfsSubClassReasoning", boolean.class), + () -> assertHasMethod(ShaclValidator.Builder.class, "disableValidation"), + () -> assertHasMethod(ShaclValidator.Builder.class, "enableValidation"), + () -> assertHasMethod(ShaclValidator.Builder.class, "setLogValidationPlans", boolean.class), + () -> assertHasMethod(ShaclValidator.Builder.class, "setPerformanceLogging", boolean.class), + () -> assertHasMethod(ShaclValidator.Builder.class, "setSerializableValidation", boolean.class), + () -> assertHasMethod(ShaclValidator.Builder.class, "setEclipseRdf4jShaclExtensions", boolean.class), + () -> assertHasMethod(ShaclValidator.Builder.class, "setDashDataShapes", boolean.class), + () -> assertHasMethod(ShaclValidator.Builder.class, "setValidationResultsLimitPerConstraint", + long.class), + () -> assertHasMethod(ShaclValidator.Builder.class, "setValidationResultsLimitTotal", long.class), + () -> assertHasMethod(ShaclValidator.Builder.class, "setTransactionalValidationLimit", long.class), + () -> assertHasMethod(ShaclValidator.Builder.class, "setValidationTimeoutMillis", long.class), + () -> assertHasMethod(ShaclValidator.Builder.class, "setShapesGraphs", Set.class), + () -> assertHasMethod(ShaclValidator.Builder.class, "withShapes", File.class, String.class, + RDFFormat.class), + () -> assertHasMethod(ShaclValidator.Builder.class, "withShapes", Path.class, String.class, + RDFFormat.class), + () -> assertHasMethod(ShaclValidator.Builder.class, "withShapes", URL.class, String.class, + RDFFormat.class), + () -> assertHasMethod(ShaclValidator.Builder.class, "withShapes", InputStream.class, String.class, + RDFFormat.class), + () -> assertHasMethod(ShaclValidator.Builder.class, "withShapes", String.class, String.class, + RDFFormat.class), + () -> assertHasMethod(ShaclValidator.Builder.class, "withShapes", File.class, String.class), + () -> assertHasMethod(ShaclValidator.Builder.class, "withShapes", Path.class, String.class), + () -> assertHasMethod(ShaclValidator.Builder.class, "withShapes", URL.class, String.class), + () -> assertHasMethod(ShaclValidator.Builder.class, "withShapes", InputStream.class, String.class), + () -> assertHasMethod(ShaclValidator.Builder.class, "withShapes", String.class, String.class), + () -> assertHasMethod(ShaclValidator.Builder.class, "withShapes", File.class, RDFFormat.class), + () -> assertHasMethod(ShaclValidator.Builder.class, "withShapes", Path.class, RDFFormat.class), + () -> assertHasMethod(ShaclValidator.Builder.class, "withShapes", URL.class, RDFFormat.class), + () -> assertHasMethod(ShaclValidator.Builder.class, "withShapes", InputStream.class, RDFFormat.class), + () -> assertHasMethod(ShaclValidator.Builder.class, "withShapes", String.class, RDFFormat.class), + () -> assertHasMethod(ShaclValidator.Builder.class, "withShapes", File.class), + () -> assertHasMethod(ShaclValidator.Builder.class, "withShapes", Path.class), + () -> assertHasMethod(ShaclValidator.Builder.class, "withShapes", URL.class)); + } + + @Test + void validatorExposesShapeSourceValidationOverloads() { + assertAll( + () -> assertHasMethod(ShaclValidator.Validator.class, "validate", Sail.class, File.class, + String.class, RDFFormat.class), + () -> assertHasMethod(ShaclValidator.Validator.class, "validate", Sail.class, Path.class, + String.class, RDFFormat.class), + () -> assertHasMethod(ShaclValidator.Validator.class, "validate", Sail.class, URL.class, + String.class, RDFFormat.class), + () -> assertHasMethod(ShaclValidator.Validator.class, "validate", Sail.class, InputStream.class, + String.class, RDFFormat.class), + () -> assertHasMethod(ShaclValidator.Validator.class, "validate", Sail.class, String.class, + String.class, RDFFormat.class), + () -> assertHasMethod(ShaclValidator.Validator.class, "validate", Sail.class, File.class, + RDFFormat.class), + () -> assertHasMethod(ShaclValidator.Validator.class, "validate", Sail.class, Path.class, + RDFFormat.class), + () -> assertHasMethod(ShaclValidator.Validator.class, "validate", Sail.class, URL.class, + RDFFormat.class), + () -> assertHasMethod(ShaclValidator.Validator.class, "validate", Sail.class, InputStream.class, + RDFFormat.class), + () -> assertHasMethod(ShaclValidator.Validator.class, "validate", Sail.class, String.class, + RDFFormat.class), + () -> assertHasMethod(ShaclValidator.Validator.class, "validate", Sail.class, File.class), + () -> assertHasMethod(ShaclValidator.Validator.class, "validate", Sail.class, Path.class), + () -> assertHasMethod(ShaclValidator.Validator.class, "validate", Sail.class, URL.class)); + } + + @Test + void validatorExposesDataSourceValidationOverloads() { + assertAll( + () -> assertHasMethod(ShaclValidator.Validator.class, "validate", File.class, String.class, + RDFFormat.class, Sail.class), + () -> assertHasMethod(ShaclValidator.Validator.class, "validate", Path.class, String.class, + RDFFormat.class, Sail.class), + () -> assertHasMethod(ShaclValidator.Validator.class, "validate", URL.class, String.class, + RDFFormat.class, Sail.class), + () -> assertHasMethod(ShaclValidator.Validator.class, "validate", InputStream.class, String.class, + RDFFormat.class, Sail.class), + () -> assertHasMethod(ShaclValidator.Validator.class, "validate", String.class, String.class, + RDFFormat.class, Sail.class), + () -> assertHasMethod(ShaclValidator.Validator.class, "validate", File.class, String.class, + Sail.class), + () -> assertHasMethod(ShaclValidator.Validator.class, "validate", Path.class, String.class, + Sail.class), + () -> assertHasMethod(ShaclValidator.Validator.class, "validate", URL.class, String.class, + Sail.class), + () -> assertHasMethod(ShaclValidator.Validator.class, "validate", InputStream.class, String.class, + Sail.class), + () -> assertHasMethod(ShaclValidator.Validator.class, "validate", String.class, String.class, + Sail.class), + () -> assertHasMethod(ShaclValidator.Validator.class, "validate", File.class, RDFFormat.class, + Sail.class), + () -> assertHasMethod(ShaclValidator.Validator.class, "validate", Path.class, RDFFormat.class, + Sail.class), + () -> assertHasMethod(ShaclValidator.Validator.class, "validate", URL.class, RDFFormat.class, + Sail.class), + () -> assertHasMethod(ShaclValidator.Validator.class, "validate", InputStream.class, RDFFormat.class, + Sail.class), + () -> assertHasMethod(ShaclValidator.Validator.class, "validate", String.class, RDFFormat.class, + Sail.class), + () -> assertHasMethod(ShaclValidator.Validator.class, "validate", File.class, Sail.class), + () -> assertHasMethod(ShaclValidator.Validator.class, "validate", Path.class, Sail.class), + () -> assertHasMethod(ShaclValidator.Validator.class, "validate", URL.class, Sail.class)); + } + + @Test + void validatorWithShapesExposesDataSourceValidationOverloads() { + assertAll( + () -> assertHasMethod(ShaclValidator.ValidatorWithShapes.class, "validate", File.class, String.class, + RDFFormat.class), + () -> assertHasMethod(ShaclValidator.ValidatorWithShapes.class, "validate", Path.class, String.class, + RDFFormat.class), + () -> assertHasMethod(ShaclValidator.ValidatorWithShapes.class, "validate", URL.class, String.class, + RDFFormat.class), + () -> assertHasMethod(ShaclValidator.ValidatorWithShapes.class, "validate", InputStream.class, + String.class, RDFFormat.class), + () -> assertHasMethod(ShaclValidator.ValidatorWithShapes.class, "validate", String.class, + String.class, RDFFormat.class), + () -> assertHasMethod(ShaclValidator.ValidatorWithShapes.class, "validate", File.class, String.class), + () -> assertHasMethod(ShaclValidator.ValidatorWithShapes.class, "validate", Path.class, String.class), + () -> assertHasMethod(ShaclValidator.ValidatorWithShapes.class, "validate", URL.class, String.class), + () -> assertHasMethod(ShaclValidator.ValidatorWithShapes.class, "validate", InputStream.class, + String.class), + () -> assertHasMethod(ShaclValidator.ValidatorWithShapes.class, "validate", String.class, + String.class), + () -> assertHasMethod(ShaclValidator.ValidatorWithShapes.class, "validate", File.class, + RDFFormat.class), + () -> assertHasMethod(ShaclValidator.ValidatorWithShapes.class, "validate", Path.class, + RDFFormat.class), + () -> assertHasMethod(ShaclValidator.ValidatorWithShapes.class, "validate", URL.class, + RDFFormat.class), + () -> assertHasMethod(ShaclValidator.ValidatorWithShapes.class, "validate", InputStream.class, + RDFFormat.class), + () -> assertHasMethod(ShaclValidator.ValidatorWithShapes.class, "validate", String.class, + RDFFormat.class), + () -> assertHasMethod(ShaclValidator.ValidatorWithShapes.class, "validate", File.class), + () -> assertHasMethod(ShaclValidator.ValidatorWithShapes.class, "validate", Path.class), + () -> assertHasMethod(ShaclValidator.ValidatorWithShapes.class, "validate", URL.class)); + } + + @Test + void builderWithShapesFromFileLoadsShapesUsingBaseUriAndFormat(@TempDir Path tempDir) throws Exception { + String baseUri = "http://example.com/ns"; + String shapesTtl = relativeShapesTtl(); + Path shapesPath = writeShapesFile(tempDir, shapesTtl); + + SailRepository dataRepo = createDataRepoForBaseUri(baseUri); + try { + ValidationReport report = validateWithShapes(shapesPath.toFile(), baseUri, RDFFormat.TURTLE, dataRepo); + assertFalse(report.conforms(), "expected validation to run when loading shapes from File"); + } finally { + dataRepo.shutDown(); + } + } + + @Test + void builderWithShapesFromPathLoadsShapesUsingBaseUriAndFormat(@TempDir Path tempDir) throws Exception { + String baseUri = "http://example.com/ns"; + String shapesTtl = relativeShapesTtl(); + Path shapesPath = writeShapesFile(tempDir, shapesTtl); + + SailRepository dataRepo = createDataRepoForBaseUri(baseUri); + try { + ValidationReport report = validateWithShapes(shapesPath, baseUri, RDFFormat.TURTLE, dataRepo); + assertFalse(report.conforms(), "expected validation to run when loading shapes from Path"); + } finally { + dataRepo.shutDown(); + } + } + + @Test + void builderWithShapesFromUrlLoadsShapesUsingBaseUriAndFormat(@TempDir Path tempDir) throws Exception { + String baseUri = "http://example.com/ns"; + String shapesTtl = relativeShapesTtl(); + Path shapesPath = writeShapesFile(tempDir, shapesTtl); + + SailRepository dataRepo = createDataRepoForBaseUri(baseUri); + try { + ValidationReport report = validateWithShapes(shapesPath.toUri().toURL(), baseUri, RDFFormat.TURTLE, + dataRepo); + assertFalse(report.conforms(), "expected validation to run when loading shapes from URL"); + } finally { + dataRepo.shutDown(); + } + } + + @Test + void builderWithShapesFromInputStreamLoadsShapesUsingBaseUriAndFormat() throws Exception { + String baseUri = "http://example.com/ns"; + String shapesTtl = relativeShapesTtl(); + + SailRepository dataRepo = createDataRepoForBaseUri(baseUri); + try (InputStream inputStream = new ByteArrayInputStream(shapesTtl.getBytes(StandardCharsets.UTF_8))) { + ValidationReport report = validateWithShapes(inputStream, baseUri, RDFFormat.TURTLE, dataRepo); + assertFalse(report.conforms(), "expected validation to run when loading shapes from InputStream"); + } finally { + dataRepo.shutDown(); + } + } + + @Test + void builderWithShapesFromStringLoadsShapesUsingBaseUriAndFormat() throws Exception { + String baseUri = "http://example.com/ns"; + String shapesTtl = relativeShapesTtl(); + + SailRepository dataRepo = createDataRepoForBaseUri(baseUri); + try { + ValidationReport report = validateWithShapes(shapesTtl, baseUri, RDFFormat.TURTLE, dataRepo); + assertFalse(report.conforms(), "expected validation to run when loading shapes from String content"); + } finally { + dataRepo.shutDown(); + } + } + + @Test + void builderWithShapesFromFileAutoDetectsFormat(@TempDir Path tempDir) throws Exception { + String baseUri = "http://example.com/ns"; + String shapesTtl = relativeShapesTtl(); + Path shapesPath = writeShapesFile(tempDir, shapesTtl); + + SailRepository dataRepo = createDataRepoForBaseUri(baseUri); + try { + ValidationReport report = validateWithShapesAuto(shapesPath.toFile(), baseUri, dataRepo); + assertFalse(report.conforms(), "expected validation to run when auto-detecting format from File"); + } finally { + dataRepo.shutDown(); + } + } + + @Test + void builderWithShapesFromPathAutoDetectsFormat(@TempDir Path tempDir) throws Exception { + String baseUri = "http://example.com/ns"; + String shapesTtl = relativeShapesTtl(); + Path shapesPath = writeShapesFile(tempDir, shapesTtl); + + SailRepository dataRepo = createDataRepoForBaseUri(baseUri); + try { + ValidationReport report = validateWithShapesAuto(shapesPath, baseUri, dataRepo); + assertFalse(report.conforms(), "expected validation to run when auto-detecting format from Path"); + } finally { + dataRepo.shutDown(); + } + } + + @Test + void builderWithShapesFromUrlAutoDetectsFormat(@TempDir Path tempDir) throws Exception { + String baseUri = "http://example.com/ns"; + String shapesTtl = relativeShapesTtl(); + Path shapesPath = writeShapesFile(tempDir, shapesTtl); + + SailRepository dataRepo = createDataRepoForBaseUri(baseUri); + try { + ValidationReport report = validateWithShapesAuto(shapesPath.toUri().toURL(), baseUri, dataRepo); + assertFalse(report.conforms(), "expected validation to run when auto-detecting format from URL"); + } finally { + dataRepo.shutDown(); + } + } + + @Test + void builderWithShapesFromInputStreamAutoDetectsFormat() throws Exception { + String baseUri = "http://example.com/shapes.ttl"; + String shapesTtl = relativeShapesTtl(); + + SailRepository dataRepo = createDataRepoForBaseUri(baseUri); + try (InputStream inputStream = new ByteArrayInputStream(shapesTtl.getBytes(StandardCharsets.UTF_8))) { + ValidationReport report = validateWithShapesAuto(inputStream, baseUri, dataRepo); + assertFalse(report.conforms(), "expected validation to run when auto-detecting format from InputStream"); + } finally { + dataRepo.shutDown(); + } + } + + @Test + void builderWithShapesFromStringAutoDetectsFormat() throws Exception { + String baseUri = "http://example.com/shapes.ttl"; + String shapesTtl = relativeShapesTtl(); + + SailRepository dataRepo = createDataRepoForBaseUri(baseUri); + try { + ValidationReport report = validateWithShapesAuto(shapesTtl, baseUri, dataRepo); + assertFalse(report.conforms(), "expected validation to run when auto-detecting format from String"); + } finally { + dataRepo.shutDown(); + } + } + + @Test + void builderWithShapesFromFileWithoutBaseUriLoadsShapes(@TempDir Path tempDir) throws Exception { + String shapesTtl = absoluteShapesTtl(); + Path shapesPath = writeShapesFile(tempDir, shapesTtl); + + SailRepository dataRepo = createDataRepoForAbsoluteShapes(); + try { + ValidationReport report = validateWithShapesNoBaseUri(shapesPath.toFile(), RDFFormat.TURTLE, dataRepo); + assertFalse(report.conforms(), "expected validation to run when loading shapes from File without base URI"); + } finally { + dataRepo.shutDown(); + } + } + + @Test + void builderWithShapesFromPathWithoutBaseUriLoadsShapes(@TempDir Path tempDir) throws Exception { + String shapesTtl = absoluteShapesTtl(); + Path shapesPath = writeShapesFile(tempDir, shapesTtl); + + SailRepository dataRepo = createDataRepoForAbsoluteShapes(); + try { + ValidationReport report = validateWithShapesNoBaseUri(shapesPath, RDFFormat.TURTLE, dataRepo); + assertFalse(report.conforms(), "expected validation to run when loading shapes from Path without base URI"); + } finally { + dataRepo.shutDown(); + } + } + + @Test + void builderWithShapesFromUrlWithoutBaseUriLoadsShapes(@TempDir Path tempDir) throws Exception { + String shapesTtl = absoluteShapesTtl(); + Path shapesPath = writeShapesFile(tempDir, shapesTtl); + + SailRepository dataRepo = createDataRepoForAbsoluteShapes(); + try { + ValidationReport report = validateWithShapesNoBaseUri(shapesPath.toUri().toURL(), RDFFormat.TURTLE, + dataRepo); + assertFalse(report.conforms(), "expected validation to run when loading shapes from URL without base URI"); + } finally { + dataRepo.shutDown(); + } + } + + @Test + void builderWithShapesFromInputStreamWithoutBaseUriLoadsShapes() throws Exception { + String shapesTtl = absoluteShapesTtl(); + + SailRepository dataRepo = createDataRepoForAbsoluteShapes(); + try (InputStream inputStream = new ByteArrayInputStream(shapesTtl.getBytes(StandardCharsets.UTF_8))) { + ValidationReport report = validateWithShapesNoBaseUri(inputStream, RDFFormat.TURTLE, dataRepo); + assertFalse(report.conforms(), + "expected validation to run when loading shapes from InputStream without base URI"); + } finally { + dataRepo.shutDown(); + } + } + + @Test + void builderWithShapesFromStringWithoutBaseUriLoadsShapes() throws Exception { + String shapesTtl = absoluteShapesTtl(); + + SailRepository dataRepo = createDataRepoForAbsoluteShapes(); + try { + ValidationReport report = validateWithShapesNoBaseUri(shapesTtl, RDFFormat.TURTLE, dataRepo); + assertFalse(report.conforms(), + "expected validation to run when loading shapes from String without base URI"); + } finally { + dataRepo.shutDown(); + } + } + + @Test + void builderWithShapesFromFileAutoDetectsFormatWithoutBaseUri(@TempDir Path tempDir) throws Exception { + String shapesTtl = absoluteShapesTtl(); + Path shapesPath = writeShapesFile(tempDir, shapesTtl); + + SailRepository dataRepo = createDataRepoForAbsoluteShapes(); + try { + ValidationReport report = validateWithShapesAutoNoBaseUri(shapesPath.toFile(), dataRepo); + assertFalse(report.conforms(), + "expected validation to run when auto-detecting format from File without base URI"); + } finally { + dataRepo.shutDown(); + } + } + + @Test + void builderWithShapesFromPathAutoDetectsFormatWithoutBaseUri(@TempDir Path tempDir) throws Exception { + String shapesTtl = absoluteShapesTtl(); + Path shapesPath = writeShapesFile(tempDir, shapesTtl); + + SailRepository dataRepo = createDataRepoForAbsoluteShapes(); + try { + ValidationReport report = validateWithShapesAutoNoBaseUri(shapesPath, dataRepo); + assertFalse(report.conforms(), + "expected validation to run when auto-detecting format from Path without base URI"); + } finally { + dataRepo.shutDown(); + } + } + + @Test + void builderWithShapesFromUrlAutoDetectsFormatWithoutBaseUri(@TempDir Path tempDir) throws Exception { + String shapesTtl = absoluteShapesTtl(); + Path shapesPath = writeShapesFile(tempDir, shapesTtl); + + SailRepository dataRepo = createDataRepoForAbsoluteShapes(); + try { + ValidationReport report = validateWithShapesAutoNoBaseUri(shapesPath.toUri().toURL(), dataRepo); + assertFalse(report.conforms(), + "expected validation to run when auto-detecting format from URL without base URI"); + } finally { + dataRepo.shutDown(); + } + } + + @Test + void validatorLoadsDataFromFileUsingBaseUriAndFormat(@TempDir Path tempDir) throws Exception { + String baseUri = "http://example.com/ns"; + Path dataPath = writeDataFile(tempDir, relativeDataTtl()); + + SailRepository shapesRepo = createShapesRepoForBaseUri(baseUri); + try { + ValidationReport report = validateWithValidatorData(dataPath.toFile(), baseUri, RDFFormat.TURTLE, + shapesRepo.getSail()); + assertFalse(report.conforms(), "expected validator to load data from File"); + } finally { + shapesRepo.shutDown(); + } + } + + @Test + void validatorAutoDetectsDataFormatWithBaseUri(@TempDir Path tempDir) throws Exception { + String baseUri = "http://example.com/ns/data.ttl"; + Path dataPath = writeDataFile(tempDir, relativeDataTtl()); + + SailRepository shapesRepo = createShapesRepoForBaseUri(baseUri); + try { + ValidationReport report = validateWithValidatorDataAuto(dataPath.toUri().toURL(), baseUri, + shapesRepo.getSail()); + assertFalse(report.conforms(), "expected validator to auto-detect data format with base URI"); + } finally { + shapesRepo.shutDown(); + } + } + + @Test + void validatorLoadsDataWithoutBaseUriUsingFormat(@TempDir Path tempDir) throws Exception { + Path dataPath = writeDataFile(tempDir, absoluteDataTtl()); + + SailRepository shapesRepo = createShapesRepoForAbsoluteShapes(); + try { + ValidationReport report = validateWithValidatorDataNoBaseUri(dataPath, RDFFormat.TURTLE, + shapesRepo.getSail()); + assertFalse(report.conforms(), "expected validator to load data without base URI"); + } finally { + shapesRepo.shutDown(); + } + } + + @Test + void validatorAutoDetectsDataFormatWithoutBaseUri(@TempDir Path tempDir) throws Exception { + Path dataPath = writeDataFile(tempDir, absoluteDataTtl()); + + SailRepository shapesRepo = createShapesRepoForAbsoluteShapes(); + try { + ValidationReport report = validateWithValidatorDataAutoNoBaseUri(dataPath.toFile(), + shapesRepo.getSail()); + assertFalse(report.conforms(), "expected validator to auto-detect data format without base URI"); + } finally { + shapesRepo.shutDown(); + } + } + + @Test + void validatorWithShapesLoadsDataFromFileUsingBaseUriAndFormat(@TempDir Path tempDir) throws Exception { + String baseUri = "http://example.com/ns"; + String shapesTtl = relativeShapesTtl(); + String dataTtl = relativeDataTtl(); + Path dataPath = writeDataFile(tempDir, dataTtl); + + ValidationReport report = validateWithData(dataPath.toFile(), baseUri, RDFFormat.TURTLE, shapesTtl); + assertFalse(report.conforms(), "expected validation to run when loading data from File"); + } + + @Test + void validatorWithShapesLoadsDataFromPathUsingBaseUriAndFormat(@TempDir Path tempDir) throws Exception { + String baseUri = "http://example.com/ns"; + String shapesTtl = relativeShapesTtl(); + String dataTtl = relativeDataTtl(); + Path dataPath = writeDataFile(tempDir, dataTtl); + + ValidationReport report = validateWithData(dataPath, baseUri, RDFFormat.TURTLE, shapesTtl); + assertFalse(report.conforms(), "expected validation to run when loading data from Path"); + } + + @Test + void validatorWithShapesLoadsDataFromUrlUsingBaseUriAndFormat(@TempDir Path tempDir) throws Exception { + String baseUri = "http://example.com/ns"; + String shapesTtl = relativeShapesTtl(); + String dataTtl = relativeDataTtl(); + Path dataPath = writeDataFile(tempDir, dataTtl); + + ValidationReport report = validateWithData(dataPath.toUri().toURL(), baseUri, RDFFormat.TURTLE, shapesTtl); + assertFalse(report.conforms(), "expected validation to run when loading data from URL"); + } + + @Test + void validatorWithShapesLoadsDataFromInputStreamUsingBaseUriAndFormat() throws Exception { + String baseUri = "http://example.com/ns"; + String shapesTtl = relativeShapesTtl(); + String dataTtl = relativeDataTtl(); + + try (InputStream inputStream = new ByteArrayInputStream(dataTtl.getBytes(StandardCharsets.UTF_8))) { + ValidationReport report = validateWithData(inputStream, baseUri, RDFFormat.TURTLE, shapesTtl); + assertFalse(report.conforms(), "expected validation to run when loading data from InputStream"); + } + } + + @Test + void validatorWithShapesLoadsDataFromStringUsingBaseUriAndFormat() throws Exception { + String baseUri = "http://example.com/ns"; + String shapesTtl = relativeShapesTtl(); + String dataTtl = relativeDataTtl(); + + ValidationReport report = validateWithData(dataTtl, baseUri, RDFFormat.TURTLE, shapesTtl); + assertFalse(report.conforms(), "expected validation to run when loading data from String"); + } + + @Test + void validatorWithShapesAutoDetectsDataFormatFromFileWithBaseUri(@TempDir Path tempDir) throws Exception { + String baseUri = "http://example.com/ns"; + String shapesTtl = relativeShapesTtl(); + String dataTtl = relativeDataTtl(); + Path dataPath = writeDataFile(tempDir, dataTtl); + + ValidationReport report = validateWithDataAuto(dataPath.toFile(), baseUri, shapesTtl); + assertFalse(report.conforms(), "expected validation to auto-detect data format from File"); + } + + @Test + void validatorWithShapesAutoDetectsDataFormatFromPathWithBaseUri(@TempDir Path tempDir) throws Exception { + String baseUri = "http://example.com/ns"; + String shapesTtl = relativeShapesTtl(); + String dataTtl = relativeDataTtl(); + Path dataPath = writeDataFile(tempDir, dataTtl); + + ValidationReport report = validateWithDataAuto(dataPath, baseUri, shapesTtl); + assertFalse(report.conforms(), "expected validation to auto-detect data format from Path"); + } + + @Test + void validatorWithShapesAutoDetectsDataFormatFromUrlWithBaseUri(@TempDir Path tempDir) throws Exception { + String baseUri = "http://example.com/ns"; + String shapesTtl = relativeShapesTtl(); + String dataTtl = relativeDataTtl(); + Path dataPath = writeDataFile(tempDir, dataTtl); + + ValidationReport report = validateWithDataAuto(dataPath.toUri().toURL(), baseUri, shapesTtl); + assertFalse(report.conforms(), "expected validation to auto-detect data format from URL"); + } + + @Test + void validatorWithShapesAutoDetectsDataFormatFromInputStreamWithBaseUri() throws Exception { + String baseUri = "http://example.com/data.ttl"; + String shapesTtl = relativeShapesTtl(); + String dataTtl = relativeDataTtl(); + + try (InputStream inputStream = new ByteArrayInputStream(dataTtl.getBytes(StandardCharsets.UTF_8))) { + ValidationReport report = validateWithDataAuto(inputStream, baseUri, shapesTtl); + assertFalse(report.conforms(), "expected validation to auto-detect data format from InputStream"); + } + } + + @Test + void validatorWithShapesAutoDetectsDataFormatFromStringWithBaseUri() throws Exception { + String baseUri = "http://example.com/data.ttl"; + String shapesTtl = relativeShapesTtl(); + String dataTtl = relativeDataTtl(); + + ValidationReport report = validateWithDataAuto(dataTtl, baseUri, shapesTtl); + assertFalse(report.conforms(), "expected validation to auto-detect data format from String"); + } + + @Test + void validatorWithShapesLoadsDataFromFileWithoutBaseUriUsingFormat(@TempDir Path tempDir) throws Exception { + String shapesTtl = absoluteShapesTtl(); + String dataTtl = absoluteDataTtl(); + Path dataPath = writeDataFile(tempDir, dataTtl); + + ValidationReport report = validateWithDataNoBaseUri(dataPath.toFile(), RDFFormat.TURTLE, shapesTtl); + assertFalse(report.conforms(), "expected validation to run when loading data from File without base URI"); + } + + @Test + void validatorWithShapesLoadsDataFromPathWithoutBaseUriUsingFormat(@TempDir Path tempDir) throws Exception { + String shapesTtl = absoluteShapesTtl(); + String dataTtl = absoluteDataTtl(); + Path dataPath = writeDataFile(tempDir, dataTtl); + + ValidationReport report = validateWithDataNoBaseUri(dataPath, RDFFormat.TURTLE, shapesTtl); + assertFalse(report.conforms(), "expected validation to run when loading data from Path without base URI"); + } + + @Test + void validatorWithShapesLoadsDataFromUrlWithoutBaseUriUsingFormat(@TempDir Path tempDir) throws Exception { + String shapesTtl = absoluteShapesTtl(); + String dataTtl = absoluteDataTtl(); + Path dataPath = writeDataFile(tempDir, dataTtl); + + ValidationReport report = validateWithDataNoBaseUri(dataPath.toUri().toURL(), RDFFormat.TURTLE, shapesTtl); + assertFalse(report.conforms(), "expected validation to run when loading data from URL without base URI"); + } + + @Test + void validatorWithShapesLoadsDataFromInputStreamWithoutBaseUriUsingFormat() throws Exception { + String shapesTtl = absoluteShapesTtl(); + String dataTtl = absoluteDataTtl(); + + try (InputStream inputStream = new ByteArrayInputStream(dataTtl.getBytes(StandardCharsets.UTF_8))) { + ValidationReport report = validateWithDataNoBaseUri(inputStream, RDFFormat.TURTLE, shapesTtl); + assertFalse(report.conforms(), + "expected validation to run when loading data from InputStream without base URI"); + } + } + + @Test + void validatorWithShapesLoadsDataFromStringWithoutBaseUriUsingFormat() throws Exception { + String shapesTtl = absoluteShapesTtl(); + String dataTtl = absoluteDataTtl(); + + ValidationReport report = validateWithDataNoBaseUri(dataTtl, RDFFormat.TURTLE, shapesTtl); + assertFalse(report.conforms(), "expected validation to run when loading data from String without base URI"); + } + + @Test + void validatorWithShapesAutoDetectsDataFormatFromFileWithoutBaseUri(@TempDir Path tempDir) throws Exception { + String shapesTtl = absoluteShapesTtl(); + String dataTtl = absoluteDataTtl(); + Path dataPath = writeDataFile(tempDir, dataTtl); + + ValidationReport report = validateWithDataAutoNoBaseUri(dataPath.toFile(), shapesTtl); + assertFalse(report.conforms(), "expected validation to auto-detect data format from File without base URI"); + } + + @Test + void validatorWithShapesAutoDetectsDataFormatFromPathWithoutBaseUri(@TempDir Path tempDir) throws Exception { + String shapesTtl = absoluteShapesTtl(); + String dataTtl = absoluteDataTtl(); + Path dataPath = writeDataFile(tempDir, dataTtl); + + ValidationReport report = validateWithDataAutoNoBaseUri(dataPath, shapesTtl); + assertFalse(report.conforms(), "expected validation to auto-detect data format from Path without base URI"); + } + + @Test + void validatorWithShapesAutoDetectsDataFormatFromUrlWithoutBaseUri(@TempDir Path tempDir) throws Exception { + String shapesTtl = absoluteShapesTtl(); + String dataTtl = absoluteDataTtl(); + Path dataPath = writeDataFile(tempDir, dataTtl); + + ValidationReport report = validateWithDataAutoNoBaseUri(dataPath.toUri().toURL(), shapesTtl); + assertFalse(report.conforms(), "expected validation to auto-detect data format from URL without base URI"); + } + + @Test + void validatorUsesDefensiveCopyOfShapesGraphsSet() throws Exception { + + SailRepository shapesRepo = new SailRepository(new MemoryStore()); + IRI graph1 = iri("http://example.com/graph1"); + IRI graph2 = iri("http://example.com/graph2"); + addShape(shapesRepo, graph1, "http://example.com/ns#p1"); + addShape(shapesRepo, graph2, "http://example.com/ns#p2"); + + SailRepository dataRepo = new SailRepository(new MemoryStore()); + addData(dataRepo, "http://example.com/ns#a", "http://example.com/ns#p1"); + + Set shapesGraphs = new HashSet<>(); + shapesGraphs.add(graph1); + + ShaclValidator.Builder builder = ShaclValidator.builder(); + invoke(builder, "setShapesGraphs", shapesGraphs); + ShaclValidator.Validator validator = builder.build(); + + shapesGraphs.clear(); + shapesGraphs.add(graph2); + + ValidationReport report = validator.validate(dataRepo.getSail(), shapesRepo.getSail()); + assertTrue(report.conforms(), "validator should not be affected by mutations to the original Set"); + } + + @Test + void setShapesGraphsTreatsRdf4jNilAsDefaultGraphForMappings() throws Exception { + SailRepository shapesRepo = new SailRepository(new MemoryStore()); + IRI shapesGraph = iri("http://example.com/ns#shapesGraph"); + + String shapesTtl = "@prefix sh: .\n" + + "@prefix ex: .\n" + + "\n" + + "ex:PersonShape a sh:NodeShape ;\n" + + " sh:targetClass ex:Person ;\n" + + " sh:property [\n" + + " sh:path ex:required ;\n" + + " sh:minCount 1 ;\n" + + " ] .\n"; + + try (SailRepositoryConnection conn = shapesRepo.getConnection()) { + conn.add(RDF4J.NIL, SHACL.SHAPES_GRAPH, shapesGraph); + conn.add(new StringReader(shapesTtl), "", RDFFormat.TURTLE, shapesGraph); + } + + SailRepository dataRepo = new SailRepository(new MemoryStore()); + try (SailRepositoryConnection conn = dataRepo.getConnection()) { + conn.add(iri("http://example.com/ns#alice"), RDF.TYPE, iri("http://example.com/ns#Person")); + } + + ShaclValidator.Builder builder = ShaclValidator.builder(); + invoke(builder, "setShapesGraphs", Set.of(RDF4J.NIL)); + ValidationReport report = builder.build().validate(dataRepo.getSail(), shapesRepo.getSail()); + assertFalse(report.conforms(), "expected default-graph mapping statements to be honored via rdf4j:nil"); + } + + @Test + void setShapesGraphToNullShouldDiscoverAllShapes() throws Exception { + + IRI[] contexts = { null, RDF4J.SHACL_SHAPE_GRAPH, RDF4J.NIL, iri("http://example.com/otherGraph") }; + + for (IRI context : contexts) { + SailRepository shapesRepo = new SailRepository(new MemoryStore()); + + String shapesTtl = "@prefix sh: .\n" + + "@prefix ex: .\n" + + "\n" + + "ex:PersonShape a sh:NodeShape ;\n" + + " sh:targetClass ex:Person ;\n" + + " sh:property [\n" + + " sh:path ex:required ;\n" + + " sh:minCount 1 ;\n" + + " ] .\n"; + + try (SailRepositoryConnection conn = shapesRepo.getConnection()) { + if (context == null) { + conn.add(new StringReader(shapesTtl), "", RDFFormat.TURTLE); + } else { + conn.add(new StringReader(shapesTtl), "", RDFFormat.TURTLE, context); + } + } + + SailRepository dataRepo = new SailRepository(new MemoryStore()); + try (SailRepositoryConnection conn = dataRepo.getConnection()) { + conn.add(iri("http://example.com/ns#alice"), RDF.TYPE, iri("http://example.com/ns#Person")); + } + + ShaclValidator.Builder builder = ShaclValidator.builder(); + builder.setShapesGraphs(null); + ValidationReport report = builder.build().validate(dataRepo.getSail(), shapesRepo.getSail()); + assertFalse(report.conforms(), "expected all shapes to be discovered when null is provided"); + shapesRepo.shutDown(); + } + } + + @Test + void validatorUsesDefensiveCopyOfShapeContextsArray() throws Exception { + SailRepository shapesRepo = new SailRepository(new MemoryStore()); + IRI graph1 = iri("http://example.com/graph1"); + IRI graph2 = iri("http://example.com/graph2"); + addShape(shapesRepo, graph1, "http://example.com/ns#p1"); + addShape(shapesRepo, graph2, "http://example.com/ns#p2"); + + SailRepository dataRepo = new SailRepository(new MemoryStore()); + addData(dataRepo, "http://example.com/ns#a", "http://example.com/ns#p1"); + + IRI[] contexts = new IRI[] { graph1 }; + + ShaclValidator.Builder builder = ShaclValidator.builder().shapeContexts(contexts); + ShaclValidator.Validator validator = builder.build(); + + contexts[0] = graph2; + + ValidationReport report = validator.validate(dataRepo.getSail(), shapesRepo.getSail()); + assertTrue(report.conforms(), "validator should not be affected by mutations to the original array"); + } + + @Test + void validatorDefaultsToReadingAllShapeContexts() throws Exception { + SailRepository shapesRepo = new SailRepository(new MemoryStore()); + addManyViolationsShape(shapesRepo, RDF4J.SHACL_SHAPE_GRAPH); + + SailRepository dataRepo = new SailRepository(new MemoryStore()); + addManyViolationsData(dataRepo, 1); + + ValidationReport report = ShaclValidator.builder() + .build() + .validate(dataRepo.getSail(), shapesRepo.getSail()); + assertFalse(report.conforms(), "expected validation to run against all shapes by default"); + } + + @Test + void builderDefaultsToIncludingDefaultGraphShapes() throws Exception { + + IRI[] contexts = { null, RDF4J.SHACL_SHAPE_GRAPH, RDF4J.NIL, iri("http://example.com/otherGraph") }; + + String ttl = "@prefix sh: .\n" + + "@prefix ex: .\n" + + "\n" + + "ex:Shape a sh:NodeShape ;\n" + + " sh:targetClass ex:Person ;\n" + + " sh:property [\n" + + " sh:path ex:required ;\n" + + " sh:minCount 1 ;\n" + + " ] .\n"; + + for (IRI context : contexts) { + SailRepository shapesRepo = new SailRepository(new MemoryStore()); + + try (SailRepositoryConnection conn = shapesRepo.getConnection()) { + if (context == null) { + conn.add(new StringReader(ttl), "", RDFFormat.TURTLE); + } else { + conn.add(new StringReader(ttl), "", RDFFormat.TURTLE, context); + } + } + + SailRepository dataRepo = new SailRepository(new MemoryStore()); + addManyViolationsData(dataRepo, 1); + + ValidationReport report = ShaclValidator.builder() + .build() + .validate(dataRepo.getSail(), shapesRepo.getSail()); + assertFalse(report.conforms(), + "builder validation should include all graphs by default, failed for context: " + context); + + shapesRepo.shutDown(); + } + + } + + @Test + void settingsAreCopiedFromBuilderToBuilderWithShapesToValidatorWithShapes() throws Exception { + SailRepository shapesRepo = new SailRepository(new MemoryStore()); + IRI graph1 = iri("http://example.com/graph1"); + IRI graph2 = iri("http://example.com/graph2"); + addShape(shapesRepo, graph1, "http://example.com/ns#p1"); + addShape(shapesRepo, graph2, "http://example.com/ns#p2"); + + SailRepository dataRepo = new SailRepository(new MemoryStore()); + addData(dataRepo, "http://example.com/ns#a", "http://example.com/ns#p1"); + + ShaclValidator.Builder builder = ShaclValidator.builder(); + invoke(builder, "setShapesGraphs", Set.of(graph1)); + ShaclValidator.BuilderWithShapes builderWithShapes = builder.withShapes(shapesRepo.getSail()); + + invoke(builder, "setShapesGraphs", Set.of(graph2)); + + ShaclValidator.ValidatorWithShapes validatorWithShapes = builderWithShapes.build(); + ValidationReport report = validatorWithShapes.validate(dataRepo.getSail()); + assertTrue(report.conforms(), "builder changes should not affect an already created BuilderWithShapes"); + } + + @Test + void settingsFromShaclSailAreCopied() throws Exception { + ShaclSail shaclSail = new ShaclSail(new MemoryStore()); + + IRI graph = iri("http://example.com/graph1"); + shaclSail.setShapesGraphs(Set.of(graph)); + shaclSail.setValidationResultsLimitTotal(1); + + ShaclValidator.Builder builder = ShaclValidator.Builder.settingsFrom(shaclSail); + SailRepository shapesRepo = new SailRepository(new MemoryStore()); + addManyViolationsShape(shapesRepo, graph); + + SailRepository dataRepo = new SailRepository(new MemoryStore()); + addManyViolationsData(dataRepo, 3); + + ValidationReport report = builder.build().validate(dataRepo.getSail(), shapesRepo.getSail()); + assertFalse(report.conforms()); + assertTrue(report.isTruncated()); + assertEquals(1, report.getValidationResult().size()); + } + + @Test + void rdfsSubClassReasoningSettingAffectsTargetClass() throws Exception { + SailRepository shapesRepo = new SailRepository(new MemoryStore()); + IRI shapesGraph = RDF4J.SHACL_SHAPE_GRAPH; + addTargetClassShape(shapesRepo, shapesGraph); + + SailRepository dataRepo = new SailRepository(new MemoryStore()); + addSubClassData(dataRepo); + + ShaclValidator.Builder builder = ShaclValidator.builder(); + invoke(builder, "setShapesGraphs", Set.of(shapesGraph)); + invoke(builder, "setRdfsSubClassReasoning", false); + + ValidationReport reportWithoutReasoning = builder.build().validate(dataRepo.getSail(), shapesRepo.getSail()); + assertTrue(reportWithoutReasoning.conforms(), "without RDFS reasoning, the targetClass should not match"); + + invoke(builder, "setRdfsSubClassReasoning", true); + ValidationReport reportWithReasoning = builder.build().validate(dataRepo.getSail(), shapesRepo.getSail()); + assertFalse(reportWithReasoning.conforms(), "with RDFS reasoning, the targetClass should match via subclass"); + } + + @Test + void validationResultsLimitsAreApplied() throws Exception { + SailRepository shapesRepo = new SailRepository(new MemoryStore()); + IRI shapesGraph = RDF4J.SHACL_SHAPE_GRAPH; + addManyViolationsShape(shapesRepo, shapesGraph); + + SailRepository dataRepo = new SailRepository(new MemoryStore()); + addManyViolationsData(dataRepo, 3); + + ShaclValidator.Builder builder = ShaclValidator.builder(); + invoke(builder, "setShapesGraphs", Set.of(shapesGraph)); + invoke(builder, "setValidationResultsLimitTotal", 100L); + invoke(builder, "setValidationResultsLimitPerConstraint", 1L); + + ValidationReport report = builder.build().validate(dataRepo.getSail(), shapesRepo.getSail()); + assertFalse(report.conforms()); + assertTrue(report.isTruncated()); + assertEquals(1, report.getValidationResult().size()); + } + + @Test + void fromShaclSailUsesItsShapesAndCopiedSettings() throws Exception { + ShaclSail shaclSail = new ShaclSail(new MemoryStore()); + shaclSail.setValidationResultsLimitTotal(1); + + SailRepository shaclRepository = new SailRepository(shaclSail); + try { + addManyViolationsShape(shaclRepository, RDF4J.SHACL_SHAPE_GRAPH); + + SailRepository dataRepo = new SailRepository(new MemoryStore()); + addManyViolationsData(dataRepo, 3); + + ValidationReport report = ShaclValidator.from(shaclSail) + .build() + .validate(dataRepo.getSail()); + + assertFalse(report.conforms()); + assertTrue(report.isTruncated()); + assertEquals(1, report.getValidationResult().size()); + } finally { + shaclRepository.shutDown(); + } + } + + @Test + @Timeout(5) + void validationTimeoutMillisAbortsValidation() throws Exception { + ShaclValidator.Builder builder = ShaclValidator.builder(); + invoke(builder, "setValidationTimeoutMillis", 50L); + + Sail dataRepo = new MemoryStore(); + Sail shapesRepo = new BlockingSail(); + + SailException exception = assertThrows(SailException.class, + () -> builder.build().validate(dataRepo, shapesRepo)); + assertTrue(exception.getMessage().contains("timed out"), "Expected a validation-timeout error"); + } + + @Test + void allSettingsAreCopiedFromBuilderToValidatorAndUnaffectedByLaterMutations() throws Exception { + IRI graph1 = iri("http://example.com/graph1"); + IRI graph2 = iri("http://example.com/graph2"); + + ShaclValidator.Builder builder = ShaclValidator.builder() + .shapeContexts(graph1, graph2) + .setParallelValidation(false) + .setLogValidationPlans(true) + .setLogValidationViolations(true) + .setGlobalLogValidationExecution(true) + .setCacheSelectNodes(false) + .setRdfsSubClassReasoning(false) + .setPerformanceLogging(true) + .setSerializableValidation(false) + .setEclipseRdf4jShaclExtensions(true) + .setDashDataShapes(true) + .setValidationResultsLimitTotal(123L) + .setValidationResultsLimitPerConstraint(45L) + .setTransactionalValidationLimit(67L) + .setValidationTimeoutMillis(89L) + .disableValidation(); + + ShaclValidator.Validator validator = builder.build(); + + // mutate builder after creating the validator + builder.shapeContexts(RDF4J.SHACL_SHAPE_GRAPH) + .setParallelValidation(true) + .setLogValidationPlans(false) + .setLogValidationViolations(false) + .setGlobalLogValidationExecution(false) + .setCacheSelectNodes(true) + .setRdfsSubClassReasoning(true) + .setPerformanceLogging(false) + .setSerializableValidation(true) + .setEclipseRdf4jShaclExtensions(false) + .setDashDataShapes(false) + .setValidationResultsLimitTotal(999L) + .setValidationResultsLimitPerConstraint(999L) + .setTransactionalValidationLimit(999L) + .setValidationTimeoutMillis(999L) + .enableValidation(); + + Object validatorBuilder = getFieldValue(validator, "builder"); + + assertArrayEquals(new Resource[] { graph1, graph2 }, + (Resource[]) getFieldValue(validatorBuilder, "shapeContexts")); + assertEquals(false, getFieldValue(validatorBuilder, "parallelValidation")); + assertEquals(true, getFieldValue(validatorBuilder, "logValidationPlans")); + assertEquals(true, getFieldValue(validatorBuilder, "logValidationViolations")); + assertEquals(false, getFieldValue(validatorBuilder, "validationEnabled")); + assertEquals(false, getFieldValue(validatorBuilder, "cacheSelectNodes")); + assertEquals(true, getFieldValue(validatorBuilder, "globalLogValidationExecution")); + assertEquals(false, getFieldValue(validatorBuilder, "rdfsSubClassReasoning")); + assertEquals(true, getFieldValue(validatorBuilder, "performanceLogging")); + assertEquals(false, getFieldValue(validatorBuilder, "serializableValidation")); + assertEquals(true, getFieldValue(validatorBuilder, "eclipseRdf4jShaclExtensions")); + assertEquals(true, getFieldValue(validatorBuilder, "dashDataShapes")); + assertEquals(123L, getFieldValue(validatorBuilder, "validationResultsLimitTotal")); + assertEquals(45L, getFieldValue(validatorBuilder, "validationResultsLimitPerConstraint")); + assertEquals(67L, getFieldValue(validatorBuilder, "transactionalValidationLimit")); + assertEquals(89L, getFieldValue(validatorBuilder, "validationTimeoutMillis")); + } + + @Test + void allSettingsAreCopiedFromBuilderToBuilderWithShapesToValidatorWithShapesAndUnaffectedByLaterMutations() + throws Exception { + SailRepository shapesRepo = new SailRepository(new MemoryStore()); + Sail shapesSail = shapesRepo.getSail(); + + IRI graph1 = iri("http://example.com/graph1"); + IRI graph2 = iri("http://example.com/graph2"); + + ShaclValidator.Builder builder = ShaclValidator.builder() + .shapeContexts(graph1, graph2) + .setParallelValidation(false) + .setLogValidationPlans(true) + .setLogValidationViolations(true) + .setGlobalLogValidationExecution(true) + .setCacheSelectNodes(false) + .setRdfsSubClassReasoning(false) + .setPerformanceLogging(true) + .setSerializableValidation(false) + .setEclipseRdf4jShaclExtensions(true) + .setDashDataShapes(true) + .setValidationResultsLimitTotal(123L) + .setValidationResultsLimitPerConstraint(45L) + .setTransactionalValidationLimit(67L) + .setValidationTimeoutMillis(89L) + .disableValidation(); + + ShaclValidator.BuilderWithShapes builderWithShapes = builder.withShapes(shapesSail); + + // mutate builder after creating BuilderWithShapes + builder.shapeContexts(RDF4J.SHACL_SHAPE_GRAPH) + .setParallelValidation(true) + .setLogValidationPlans(false) + .setLogValidationViolations(false) + .setGlobalLogValidationExecution(false) + .setCacheSelectNodes(true) + .setRdfsSubClassReasoning(true) + .setPerformanceLogging(false) + .setSerializableValidation(true) + .setEclipseRdf4jShaclExtensions(false) + .setDashDataShapes(false) + .setValidationResultsLimitTotal(999L) + .setValidationResultsLimitPerConstraint(999L) + .setTransactionalValidationLimit(999L) + .setValidationTimeoutMillis(999L) + .enableValidation(); + + ShaclValidator.ValidatorWithShapes validatorWithShapes = builderWithShapes.build(); + + // mutate BuilderWithShapes after creating ValidatorWithShapes + builderWithShapes.shapeContexts(RDF4J.SHACL_SHAPE_GRAPH) + .setParallelValidation(true) + .setLogValidationPlans(false) + .setLogValidationViolations(false) + .setGlobalLogValidationExecution(false) + .setCacheSelectNodes(true) + .setRdfsSubClassReasoning(true) + .setPerformanceLogging(false) + .setSerializableValidation(true) + .setEclipseRdf4jShaclExtensions(false) + .setDashDataShapes(false) + .setValidationResultsLimitTotal(999L) + .setValidationResultsLimitPerConstraint(999L) + .setTransactionalValidationLimit(999L) + .setValidationTimeoutMillis(999L) + .enableValidation(); + builderWithShapes.shapes = new MemoryStore(); + + Object capturedBuilderWithShapes = getFieldValue(validatorWithShapes, "builderWithShapes"); + + assertSame(shapesSail, getFieldValue(capturedBuilderWithShapes, "shapes")); + assertArrayEquals(new Resource[] { graph1, graph2 }, + (Resource[]) getFieldValue(capturedBuilderWithShapes, "shapeContexts")); + assertEquals(false, getFieldValue(capturedBuilderWithShapes, "parallelValidation")); + assertEquals(true, getFieldValue(capturedBuilderWithShapes, "logValidationPlans")); + assertEquals(true, getFieldValue(capturedBuilderWithShapes, "logValidationViolations")); + assertEquals(false, getFieldValue(capturedBuilderWithShapes, "validationEnabled")); + assertEquals(false, getFieldValue(capturedBuilderWithShapes, "cacheSelectNodes")); + assertEquals(true, getFieldValue(capturedBuilderWithShapes, "globalLogValidationExecution")); + assertEquals(false, getFieldValue(capturedBuilderWithShapes, "rdfsSubClassReasoning")); + assertEquals(true, getFieldValue(capturedBuilderWithShapes, "performanceLogging")); + assertEquals(false, getFieldValue(capturedBuilderWithShapes, "serializableValidation")); + assertEquals(true, getFieldValue(capturedBuilderWithShapes, "eclipseRdf4jShaclExtensions")); + assertEquals(true, getFieldValue(capturedBuilderWithShapes, "dashDataShapes")); + assertEquals(123L, getFieldValue(capturedBuilderWithShapes, "validationResultsLimitTotal")); + assertEquals(45L, getFieldValue(capturedBuilderWithShapes, "validationResultsLimitPerConstraint")); + assertEquals(67L, getFieldValue(capturedBuilderWithShapes, "transactionalValidationLimit")); + assertEquals(89L, getFieldValue(capturedBuilderWithShapes, "validationTimeoutMillis")); + } + + @Test + void settingsFromShaclSailCopiesAllConfigurationSettings() throws Exception { + ShaclSail shaclSail = new ShaclSail(new MemoryStore()); + IRI graph = iri("http://example.com/graph1"); + + shaclSail.setShapesGraphs(Set.of(graph)); + shaclSail.setParallelValidation(false); + shaclSail.setLogValidationPlans(true); + shaclSail.setLogValidationViolations(true); + shaclSail.setGlobalLogValidationExecution(true); + shaclSail.setCacheSelectNodes(false); + shaclSail.setRdfsSubClassReasoning(false); + shaclSail.setSerializableValidation(false); + shaclSail.setPerformanceLogging(true); + shaclSail.setEclipseRdf4jShaclExtensions(true); + shaclSail.setDashDataShapes(true); + shaclSail.setValidationResultsLimitTotal(123L); + shaclSail.setValidationResultsLimitPerConstraint(45L); + shaclSail.setTransactionalValidationLimit(67L); + shaclSail.disableValidation(); + + ShaclValidator.Builder builder = ShaclValidator.Builder.settingsFrom(shaclSail); + + assertArrayEquals(new Resource[] { graph }, (Resource[]) getFieldValue(builder, "shapeContexts")); + assertEquals(false, getFieldValue(builder, "parallelValidation")); + assertEquals(true, getFieldValue(builder, "logValidationPlans")); + assertEquals(true, getFieldValue(builder, "logValidationViolations")); + assertEquals(false, getFieldValue(builder, "validationEnabled")); + assertEquals(false, getFieldValue(builder, "cacheSelectNodes")); + assertEquals(true, getFieldValue(builder, "globalLogValidationExecution")); + assertEquals(false, getFieldValue(builder, "rdfsSubClassReasoning")); + assertEquals(true, getFieldValue(builder, "performanceLogging")); + assertEquals(false, getFieldValue(builder, "serializableValidation")); + assertEquals(true, getFieldValue(builder, "eclipseRdf4jShaclExtensions")); + assertEquals(true, getFieldValue(builder, "dashDataShapes")); + assertEquals(123L, getFieldValue(builder, "validationResultsLimitTotal")); + assertEquals(45L, getFieldValue(builder, "validationResultsLimitPerConstraint")); + assertEquals(67L, getFieldValue(builder, "transactionalValidationLimit")); + } + + private static String relativeShapesTtl() { + return "@prefix sh: .\n" + + "\n" + + "<#PersonShape> a sh:NodeShape ;\n" + + " sh:targetClass <#Person> ;\n" + + " sh:property [\n" + + " sh:path <#required> ;\n" + + " sh:minCount 1 ;\n" + + " ] .\n"; + } + + private static String absoluteShapesTtl() { + return "@prefix sh: .\n" + + "\n" + + " a sh:NodeShape ;\n" + + " sh:targetClass ;\n" + + " sh:property [\n" + + " sh:path ;\n" + + " sh:minCount 1 ;\n" + + " ] .\n"; + } + + private static String relativeDataTtl() { + return "<#alice> a <#Person> .\n"; + } + + private static String absoluteDataTtl() { + return " a .\n"; + } + + private static Path writeShapesFile(Path tempDir, String shapesTtl) throws Exception { + Path shapesFile = tempDir.resolve("shapes.ttl"); + Files.writeString(shapesFile, shapesTtl, StandardCharsets.UTF_8); + return shapesFile; + } + + private static Path writeDataFile(Path tempDir, String dataTtl) throws Exception { + Path dataFile = tempDir.resolve("data.ttl"); + Files.writeString(dataFile, dataTtl, StandardCharsets.UTF_8); + return dataFile; + } + + private static SailRepository createDataRepoForBaseUri(String baseUri) throws Exception { + SailRepository dataRepo = new SailRepository(new MemoryStore()); + try (SailRepositoryConnection conn = dataRepo.getConnection()) { + conn.add(iri(baseUri + "#alice"), RDF.TYPE, iri(baseUri + "#Person")); + } + return dataRepo; + } + + private static SailRepository createDataRepoForAbsoluteShapes() throws Exception { + return createDataRepoForBaseUri("http://example.com/ns"); + } + + private static SailRepository createShapesRepoForBaseUri(String baseUri) throws Exception { + SailRepository shapesRepo = new SailRepository(new MemoryStore()); + try (SailRepositoryConnection conn = shapesRepo.getConnection()) { + conn.add(new StringReader(relativeShapesTtl()), baseUri, RDFFormat.TURTLE); + } + return shapesRepo; + } + + private static SailRepository createShapesRepoForAbsoluteShapes() throws Exception { + SailRepository shapesRepo = new SailRepository(new MemoryStore()); + try (SailRepositoryConnection conn = shapesRepo.getConnection()) { + conn.add(new StringReader(absoluteShapesTtl()), "", RDFFormat.TURTLE); + } + return shapesRepo; + } + + private static ValidationReport validateWithShapes(Object shapesSource, String baseUri, RDFFormat format, + SailRepository dataRepo) throws Exception { + ShaclValidator.Builder builder = ShaclValidator.builder(); + ShaclValidator.BuilderWithShapes builderWithShapes = (ShaclValidator.BuilderWithShapes) invoke(builder, + "withShapes", shapesSource, baseUri, format); + return builderWithShapes.build().validate(dataRepo.getSail()); + } + + private static ValidationReport validateWithShapesNoBaseUri(Object shapesSource, RDFFormat format, + SailRepository dataRepo) throws Exception { + ShaclValidator.Builder builder = ShaclValidator.builder(); + ShaclValidator.BuilderWithShapes builderWithShapes = (ShaclValidator.BuilderWithShapes) invoke(builder, + "withShapes", shapesSource, format); + return builderWithShapes.build().validate(dataRepo.getSail()); + } + + private static ValidationReport validateWithShapesAuto(Object shapesSource, String baseUri, + SailRepository dataRepo) throws Exception { + ShaclValidator.Builder builder = ShaclValidator.builder(); + ShaclValidator.BuilderWithShapes builderWithShapes = (ShaclValidator.BuilderWithShapes) invoke(builder, + "withShapes", shapesSource, baseUri); + return builderWithShapes.build().validate(dataRepo.getSail()); + } + + private static ValidationReport validateWithShapesAutoNoBaseUri(Object shapesSource, + SailRepository dataRepo) throws Exception { + ShaclValidator.Builder builder = ShaclValidator.builder(); + ShaclValidator.BuilderWithShapes builderWithShapes = (ShaclValidator.BuilderWithShapes) invoke(builder, + "withShapes", shapesSource); + return builderWithShapes.build().validate(dataRepo.getSail()); + } + + private static ValidationReport validateWithData(Object dataSource, String baseUri, RDFFormat format, + String shapesTtl) throws Exception { + ShaclValidator.ValidatorWithShapes validator = ShaclValidator.builder() + .withShapes(shapesTtl, baseUri, RDFFormat.TURTLE) + .build(); + return (ValidationReport) invoke(validator, "validate", dataSource, baseUri, format); + } + + private static ValidationReport validateWithDataAuto(Object dataSource, String baseUri, String shapesTtl) + throws Exception { + ShaclValidator.ValidatorWithShapes validator = ShaclValidator.builder() + .withShapes(shapesTtl, baseUri, RDFFormat.TURTLE) + .build(); + return (ValidationReport) invoke(validator, "validate", dataSource, baseUri); + } + + private static ValidationReport validateWithDataNoBaseUri(Object dataSource, RDFFormat format, + String shapesTtl) throws Exception { + ShaclValidator.ValidatorWithShapes validator = ShaclValidator.builder() + .withShapes(shapesTtl, RDFFormat.TURTLE) + .build(); + return (ValidationReport) invoke(validator, "validate", dataSource, format); + } + + private static ValidationReport validateWithDataAutoNoBaseUri(Object dataSource, String shapesTtl) + throws Exception { + ShaclValidator.ValidatorWithShapes validator = ShaclValidator.builder() + .withShapes(shapesTtl, RDFFormat.TURTLE) + .build(); + return (ValidationReport) invoke(validator, "validate", dataSource); + } + + private static ValidationReport validateWithValidatorData(Object dataSource, String baseUri, RDFFormat format, + Sail shapesSail) throws Exception { + ShaclValidator.Validator validator = ShaclValidator.builder().build(); + return (ValidationReport) invoke(validator, "validate", dataSource, baseUri, format, shapesSail); + } + + private static ValidationReport validateWithValidatorDataAuto(Object dataSource, String baseUri, Sail shapesSail) + throws Exception { + ShaclValidator.Validator validator = ShaclValidator.builder().build(); + return (ValidationReport) invoke(validator, "validate", dataSource, baseUri, shapesSail); + } + + private static ValidationReport validateWithValidatorDataNoBaseUri(Object dataSource, RDFFormat format, + Sail shapesSail) throws Exception { + ShaclValidator.Validator validator = ShaclValidator.builder().build(); + return (ValidationReport) invoke(validator, "validate", dataSource, format, shapesSail); + } + + private static ValidationReport validateWithValidatorDataAutoNoBaseUri(Object dataSource, Sail shapesSail) + throws Exception { + ShaclValidator.Validator validator = ShaclValidator.builder().build(); + return (ValidationReport) invoke(validator, "validate", dataSource, shapesSail); + } + + private static final class BlockingSail extends AbstractSail { + + private final CountDownLatch latch = new CountDownLatch(1); + + @Override + protected SailConnection getConnectionInternal() throws SailException { + try { + latch.await(); + throw new AssertionError("Unexpected latch release"); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new SailException("Interrupted while waiting for a connection", e); + } + } + + @Override + protected void shutDownInternal() throws SailException { + latch.countDown(); + } + + @Override + public boolean isWritable() throws SailException { + return false; + } + + @Override + public ValueFactory getValueFactory() { + return SimpleValueFactory.getInstance(); + } + } + + private static void assertHasMethod(Class clazz, String name, Class... parameterTypes) { + try { + clazz.getMethod(name, parameterTypes); + } catch (NoSuchMethodException e) { + fail("Expected method " + clazz.getName() + "#" + name, e); + } + } + + private static Object invoke(Object target, String name, Object... args) throws Exception { + Method match = null; + for (Method candidate : target.getClass().getMethods()) { + if (!candidate.getName().equals(name)) { + continue; + } + Class[] parameterTypes = candidate.getParameterTypes(); + if (parameterTypes.length != args.length) { + continue; + } + boolean compatible = true; + for (int i = 0; i < parameterTypes.length; i++) { + Class parameterType = parameterTypes[i]; + Object arg = args[i]; + Class argType = (arg == null) ? null : arg.getClass(); + if (parameterType.isPrimitive()) { + parameterType = wrapPrimitive(parameterType); + } + if (argType != null && !parameterType.isAssignableFrom(argType)) { + compatible = false; + break; + } + } + if (compatible) { + match = candidate; + break; + } + } + + if (match == null) { + fail("Expected compatible method " + target.getClass().getName() + "#" + name); + } + + return match.invoke(target, args); + } + + private static Class wrapPrimitive(Class type) { + if (type == boolean.class) { + return Boolean.class; + } + if (type == long.class) { + return Long.class; + } + if (type == int.class) { + return Integer.class; + } + if (type == double.class) { + return Double.class; + } + if (type == float.class) { + return Float.class; + } + if (type == short.class) { + return Short.class; + } + if (type == byte.class) { + return Byte.class; + } + if (type == char.class) { + return Character.class; + } + return type; + } + + private static Object getFieldValue(Object target, String fieldName) { + Class clazz = target.getClass(); + while (clazz != null) { + try { + Field field = clazz.getDeclaredField(fieldName); + field.setAccessible(true); + return field.get(target); + } catch (NoSuchFieldException e) { + clazz = clazz.getSuperclass(); + } catch (IllegalAccessException e) { + throw new AssertionError(e); + } + } + throw new AssertionError("Field not found: " + target.getClass().getName() + "#" + fieldName); + } + + private static void addShape(SailRepository repo, IRI graph, String requiredPropertyIri) throws Exception { + boolean isRdf4jShapeGraph = RDF4J.SHACL_SHAPE_GRAPH.equals(graph); + + String mapping = isRdf4jShapeGraph + ? "" + : "@prefix rdf4j: .\n" + + "\n" + + "rdf4j:nil sh:shapesGraph <" + graph.stringValue() + "> .\n" + + "\n"; + + String ttl = "@prefix sh: .\n" + + "@prefix ex: .\n" + + mapping + + "ex:Shape a sh:NodeShape ;\n" + + " sh:targetNode ex:a ;\n" + + " sh:property [\n" + + " sh:path <" + requiredPropertyIri + "> ;\n" + + " sh:minCount 1 ;\n" + + " ] .\n"; + + try (SailRepositoryConnection conn = repo.getConnection()) { + conn.add(new StringReader(ttl), "", RDFFormat.TURTLE, graph); + } + } + + private static void addData(SailRepository repo, String nodeIri, String propertyIri) throws Exception { + try (SailRepositoryConnection conn = repo.getConnection()) { + conn.add(iri(nodeIri), iri(propertyIri), iri("http://example.com/ns#value")); + } + } + + private static void addManyViolationsShape(SailRepository repo, IRI graph) throws Exception { + boolean isRdf4jShapeGraph = RDF4J.SHACL_SHAPE_GRAPH.equals(graph); + + String mapping = isRdf4jShapeGraph + ? "" + : "@prefix rdf4j: .\n" + + "\n" + + "rdf4j:nil sh:shapesGraph <" + graph.stringValue() + "> .\n" + + "\n"; + + String ttl = "@prefix sh: .\n" + + "@prefix ex: .\n" + + mapping + + "ex:Shape a sh:NodeShape ;\n" + + " sh:targetClass ex:Person ;\n" + + " sh:property [\n" + + " sh:path ex:required ;\n" + + " sh:minCount 1 ;\n" + + " ] .\n"; + try (SailRepositoryConnection conn = repo.getConnection()) { + conn.add(new StringReader(ttl), "", RDFFormat.TURTLE, graph); + } + } + + private static void addManyViolationsData(SailRepository repo, int count) throws Exception { + try (SailRepositoryConnection conn = repo.getConnection()) { + for (int i = 0; i < count; i++) { + conn.add(iri("http://example.com/ns#n" + i), RDF.TYPE, iri("http://example.com/ns#Person")); + } + } + } + + private static void addTargetClassShape(SailRepository repo, IRI graph) throws Exception { + String ttl = "@prefix sh: .\n" + + "@prefix ex: .\n" + + "\n" + + "ex:Shape a sh:NodeShape ;\n" + + " sh:targetClass ex:Parent ;\n" + + " sh:property [\n" + + " sh:path ex:required ;\n" + + " sh:minCount 1 ;\n" + + " ] .\n"; + try (SailRepositoryConnection conn = repo.getConnection()) { + conn.add(new StringReader(ttl), "", RDFFormat.TURTLE, graph); + } + } + + private static void addSubClassData(SailRepository repo) throws Exception { + try (SailRepositoryConnection conn = repo.getConnection()) { + conn.add(iri("http://example.com/ns#Child"), RDFS.SUBCLASSOF, iri("http://example.com/ns#Parent")); + conn.add(iri("http://example.com/ns#inst"), RDF.TYPE, iri("http://example.com/ns#Child")); + } + } + + private static IRI iri(String iri) { + return org.eclipse.rdf4j.model.util.Values.iri(iri); + } +} diff --git a/core/sail/shacl/src/test/java/org/eclipse/rdf4j/sail/shacl/W3cComplianceTest.java b/core/sail/shacl/src/test/java/org/eclipse/rdf4j/sail/shacl/W3cComplianceTest.java index ecaf7e3c8f1..10ec9ddf2a4 100644 --- a/core/sail/shacl/src/test/java/org/eclipse/rdf4j/sail/shacl/W3cComplianceTest.java +++ b/core/sail/shacl/src/test/java/org/eclipse/rdf4j/sail/shacl/W3cComplianceTest.java @@ -8,6 +8,7 @@ * * SPDX-License-Identifier: BSD-3-Clause *******************************************************************************/ +// Some portions generated by Codex package org.eclipse.rdf4j.sail.shacl; @@ -224,11 +225,14 @@ private void runTest(URL resourceName) throws IOException { try (RepositoryConnection conn = shapes.getConnection()) { conn.begin(IsolationLevels.NONE, ShaclSail.TransactionSettings.ValidationApproach.Disabled); - conn.add(resourceName, resourceName.toString(), RDFFormat.TURTLE); + conn.add(resourceName, resourceName.toString(), RDFFormat.TURTLE, RDF4J.SHACL_SHAPE_GRAPH); conn.commit(); } - ValidationReport validate = ShaclValidator.validate(data.getSail(), shapes.getSail()); + ValidationReport validate = ShaclValidator.builder() + .withShapes(shapes.getSail()) + .build() + .validate(data.getSail()); assertEquals(expected.conforms, validate.conforms()); } diff --git a/core/sail/shacl/src/test/java/org/eclipse/rdf4j/sail/shacl/results/ShaclValidatorNamedGraphTest.java b/core/sail/shacl/src/test/java/org/eclipse/rdf4j/sail/shacl/results/ShaclValidatorNamedGraphTest.java index e1d5ca11dc3..f30f4be0779 100644 --- a/core/sail/shacl/src/test/java/org/eclipse/rdf4j/sail/shacl/results/ShaclValidatorNamedGraphTest.java +++ b/core/sail/shacl/src/test/java/org/eclipse/rdf4j/sail/shacl/results/ShaclValidatorNamedGraphTest.java @@ -8,6 +8,7 @@ * * SPDX-License-Identifier: BSD-3-Clause ******************************************************************************/ +// Some portions generated by Codex package org.eclipse.rdf4j.sail.shacl.results; @@ -17,6 +18,7 @@ import java.io.IOException; import java.io.StringReader; +import java.util.Set; import org.eclipse.rdf4j.model.IRI; import org.eclipse.rdf4j.model.util.Values; @@ -38,6 +40,7 @@ public class ShaclValidatorNamedGraphTest { private static final String EX = "http://example.com/ns#"; private static final IRI dataGraph = Values.iri(EX, "dataGraph"); + private static final IRI shapesGraph = Values.iri(EX, "shapesGraph"); @Test public void testNamedGraph() throws Exception { @@ -50,7 +53,7 @@ public void testNamedGraph() throws Exception { connection.add(Values.iri(EX, "person1"), RDF.TYPE, RDFS.RESOURCE, dataGraph); } - ValidationReport validate = ShaclValidator.validate(data.getSail(), shapes.getSail()); + ValidationReport validate = validate(data, shapes, Set.of(shapesGraph)); assertFalse(validate.conforms()); } @@ -66,7 +69,7 @@ public void testRdf4jShaclShapesGraph() throws Exception { connection.add(Values.iri(EX, "person1"), RDF.TYPE, RDFS.RESOURCE, dataGraph); } - ValidationReport validate = ShaclValidator.validate(data.getSail(), shapes.getSail()); + ValidationReport validate = validate(data, shapes); assertFalse(validate.conforms()); } @@ -75,7 +78,7 @@ public void testRdf4jShaclShapesGraph() throws Exception { connection.add(Values.iri(EX, "person1"), RDFS.LABEL, Values.literal("label1")); } - ValidationReport validate = ShaclValidator.validate(data.getSail(), shapes.getSail()); + ValidationReport validate = validate(data, shapes); assertTrue(validate.conforms()); } @@ -85,7 +88,7 @@ public void testRdf4jShaclShapesGraph() throws Exception { connection.add(Values.iri(EX, "person1"), RDFS.LABEL, Values.literal("label3"), dataGraph); } - ValidationReport validate = ShaclValidator.validate(data.getSail(), shapes.getSail()); + ValidationReport validate = validate(data, shapes); assertFalse(validate.conforms()); } @@ -103,7 +106,7 @@ public void testRdf4jShaclShapesGraph2() throws Exception { connection.add(Values.iri(EX, "person1"), RDF.TYPE, RDFS.RESOURCE, dataGraph); } - ValidationReport validate = ShaclValidator.validate(data.getSail(), shapes.getSail()); + ValidationReport validate = validate(data, shapes); assertFalse(validate.conforms()); assertEquals(2, validate.getValidationResult().size()); } @@ -230,4 +233,21 @@ private static SailRepository getShapesRdf4jShaclShapesGraph2() throws IOExcepti return shapes; } + private static ValidationReport validate(SailRepository data, SailRepository shapes) { + return validate(data, shapes, Set.of(RDF4J.SHACL_SHAPE_GRAPH, shapesGraph)); + } + + private static ValidationReport validate(SailRepository data, SailRepository shapes, Set shapesGraphs) { + ShaclValidator.Builder builder = ShaclValidator.builder() + .setRdfsSubClassReasoning(false); + if (shapesGraphs != null) { + builder.setShapesGraphs(shapesGraphs); + } + + return builder + .withShapes(shapes.getSail()) + .build() + .validate(data.getSail()); + } + } 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 index 0c96b634220..1079411a68c 100644 --- 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 @@ -8,6 +8,7 @@ * * SPDX-License-Identifier: BSD-3-Clause ******************************************************************************/ +// Some portions generated by Codex package org.eclipse.rdf4j.sail.shacl.results; @@ -26,6 +27,7 @@ import org.eclipse.rdf4j.model.Model; import org.eclipse.rdf4j.model.Resource; import org.eclipse.rdf4j.model.Value; +import org.eclipse.rdf4j.model.vocabulary.RDF4J; import org.eclipse.rdf4j.model.vocabulary.SHACL; import org.eclipse.rdf4j.repository.sail.SailRepository; import org.eclipse.rdf4j.repository.sail.SailRepositoryConnection; @@ -78,7 +80,7 @@ public void multipleSparqlConstraintsDifferentMessages() throws Exception { SailRepository shapes = new SailRepository(new MemoryStore()); try (SailRepositoryConnection connection = shapes.getConnection()) { - connection.add(new StringReader(shapesTtl), RDFFormat.TURTLE); + connection.add(new StringReader(shapesTtl), "", RDFFormat.TURTLE, RDF4J.SHACL_SHAPE_GRAPH); } SailRepository data = new SailRepository(new MemoryStore()); @@ -86,7 +88,7 @@ public void multipleSparqlConstraintsDifferentMessages() throws Exception { connection.add(new StringReader(dataTtl), RDFFormat.TURTLE); } - ValidationReport report = ShaclValidator.validate(data.getSail(), shapes.getSail()); + ValidationReport report = validate(data, shapes); assertFalse(report.conforms()); assertEquals(2, report.getValidationResult().size()); @@ -133,7 +135,7 @@ public void multipleMessagesPerConstraintAreAllReported() throws Exception { SailRepository shapes = new SailRepository(new MemoryStore()); try (SailRepositoryConnection connection = shapes.getConnection()) { - connection.add(new StringReader(shapesTtl), RDFFormat.TURTLE); + connection.add(new StringReader(shapesTtl), "", RDFFormat.TURTLE, RDF4J.SHACL_SHAPE_GRAPH); } SailRepository data = new SailRepository(new MemoryStore()); @@ -141,7 +143,7 @@ public void multipleMessagesPerConstraintAreAllReported() throws Exception { connection.add(new StringReader(dataTtl), RDFFormat.TURTLE); } - ValidationReport report = ShaclValidator.validate(data.getSail(), shapes.getSail()); + ValidationReport report = validate(data, shapes); assertFalse(report.conforms()); assertResultMessagesMatchConstraintMessages(report); @@ -189,7 +191,7 @@ public void propertyShapeWithMultipleSparqlConstraintsReportsAllMessages() throw SailRepository shapes = new SailRepository(new MemoryStore()); try (SailRepositoryConnection connection = shapes.getConnection()) { - connection.add(new StringReader(shapesTtl), RDFFormat.TURTLE); + connection.add(new StringReader(shapesTtl), "", RDFFormat.TURTLE, RDF4J.SHACL_SHAPE_GRAPH); } SailRepository data = new SailRepository(new MemoryStore()); @@ -197,7 +199,7 @@ public void propertyShapeWithMultipleSparqlConstraintsReportsAllMessages() throw connection.add(new StringReader(dataTtl), RDFFormat.TURTLE); } - ValidationReport report = ShaclValidator.validate(data.getSail(), shapes.getSail()); + ValidationReport report = validate(data, shapes); System.out.println(report); assertFalse(report.conforms()); @@ -246,7 +248,7 @@ public void personShapePositiveAgeAndNoSelfManageMessagesPerViolation() throws E SailRepository shapes = new SailRepository(new MemoryStore()); try (SailRepositoryConnection connection = shapes.getConnection()) { - connection.add(new StringReader(shapesTtl), RDFFormat.TURTLE); + connection.add(new StringReader(shapesTtl), "", RDFFormat.TURTLE, RDF4J.SHACL_SHAPE_GRAPH); } SailRepository data = new SailRepository(new MemoryStore()); @@ -254,7 +256,7 @@ public void personShapePositiveAgeAndNoSelfManageMessagesPerViolation() throws E connection.add(new StringReader(dataTtl), RDFFormat.TURTLE); } - ValidationReport report = ShaclValidator.validate(data.getSail(), shapes.getSail()); + ValidationReport report = validate(data, shapes); ShaclSailValidationReportHelper.printValidationReport(report, System.out); assertFalse(report.conforms()); assertEquals(3, report.getValidationResult().size()); @@ -288,7 +290,7 @@ public void messageBindingOverridesConstraintMessages() throws Exception { SailRepository shapes = new SailRepository(new MemoryStore()); try (SailRepositoryConnection connection = shapes.getConnection()) { - connection.add(new StringReader(shapesTtl), RDFFormat.TURTLE); + connection.add(new StringReader(shapesTtl), "", RDFFormat.TURTLE, RDF4J.SHACL_SHAPE_GRAPH); } SailRepository data = new SailRepository(new MemoryStore()); @@ -296,7 +298,7 @@ public void messageBindingOverridesConstraintMessages() throws Exception { connection.add(new StringReader(dataTtl), RDFFormat.TURTLE); } - ValidationReport report = ShaclValidator.validate(data.getSail(), shapes.getSail()); + ValidationReport report = validate(data, shapes); assertFalse(report.conforms()); assertEquals(1, report.getValidationResult().size()); @@ -328,7 +330,7 @@ public void messageBindingUsedWhenNoConstraintMessages() throws Exception { SailRepository shapes = new SailRepository(new MemoryStore()); try (SailRepositoryConnection connection = shapes.getConnection()) { - connection.add(new StringReader(shapesTtl), RDFFormat.TURTLE); + connection.add(new StringReader(shapesTtl), "", RDFFormat.TURTLE, RDF4J.SHACL_SHAPE_GRAPH); } SailRepository data = new SailRepository(new MemoryStore()); @@ -336,7 +338,7 @@ public void messageBindingUsedWhenNoConstraintMessages() throws Exception { connection.add(new StringReader(dataTtl), RDFFormat.TURTLE); } - ValidationReport report = ShaclValidator.validate(data.getSail(), shapes.getSail()); + ValidationReport report = validate(data, shapes); assertFalse(report.conforms()); assertEquals(1, report.getValidationResult().size()); @@ -373,7 +375,7 @@ public void messageTemplatesSubstituteSelectVariables() throws Exception { SailRepository shapes = new SailRepository(new MemoryStore()); try (SailRepositoryConnection connection = shapes.getConnection()) { - connection.add(new StringReader(shapesTtl), RDFFormat.TURTLE); + connection.add(new StringReader(shapesTtl), "", RDFFormat.TURTLE, RDF4J.SHACL_SHAPE_GRAPH); } SailRepository data = new SailRepository(new MemoryStore()); @@ -381,7 +383,7 @@ public void messageTemplatesSubstituteSelectVariables() throws Exception { connection.add(new StringReader(dataTtl), RDFFormat.TURTLE); } - ValidationReport report = ShaclValidator.validate(data.getSail(), shapes.getSail()); + ValidationReport report = validate(data, shapes); assertFalse(report.conforms()); assertEquals(1, report.getValidationResult().size()); @@ -412,7 +414,7 @@ public void noMessagesProducesNoResultMessage() throws Exception { SailRepository shapes = new SailRepository(new MemoryStore()); try (SailRepositoryConnection connection = shapes.getConnection()) { - connection.add(new StringReader(shapesTtl), RDFFormat.TURTLE); + connection.add(new StringReader(shapesTtl), "", RDFFormat.TURTLE, RDF4J.SHACL_SHAPE_GRAPH); } SailRepository data = new SailRepository(new MemoryStore()); @@ -420,7 +422,7 @@ public void noMessagesProducesNoResultMessage() throws Exception { connection.add(new StringReader(dataTtl), RDFFormat.TURTLE); } - ValidationReport report = ShaclValidator.validate(data.getSail(), shapes.getSail()); + ValidationReport report = validate(data, shapes); assertFalse(report.conforms()); assertEquals(1, report.getValidationResult().size()); @@ -463,7 +465,7 @@ public void multipleTemplateMessagesAreAllSubstitutedAndReported() throws Except SailRepository shapes = new SailRepository(new MemoryStore()); try (SailRepositoryConnection connection = shapes.getConnection()) { - connection.add(new StringReader(shapesTtl), RDFFormat.TURTLE); + connection.add(new StringReader(shapesTtl), "", RDFFormat.TURTLE, RDF4J.SHACL_SHAPE_GRAPH); } SailRepository data = new SailRepository(new MemoryStore()); @@ -471,7 +473,7 @@ public void multipleTemplateMessagesAreAllSubstitutedAndReported() throws Except connection.add(new StringReader(dataTtl), RDFFormat.TURTLE); } - ValidationReport report = ShaclValidator.validate(data.getSail(), shapes.getSail()); + ValidationReport report = validate(data, shapes); assertFalse(report.conforms()); assertEquals(2, report.getValidationResult().size()); @@ -518,7 +520,7 @@ public void unboundTemplateVariablesRemainUnchanged() throws Exception { SailRepository shapes = new SailRepository(new MemoryStore()); try (SailRepositoryConnection connection = shapes.getConnection()) { - connection.add(new StringReader(shapesTtl), RDFFormat.TURTLE); + connection.add(new StringReader(shapesTtl), "", RDFFormat.TURTLE, RDF4J.SHACL_SHAPE_GRAPH); } SailRepository data = new SailRepository(new MemoryStore()); @@ -526,7 +528,7 @@ public void unboundTemplateVariablesRemainUnchanged() throws Exception { connection.add(new StringReader(dataTtl), RDFFormat.TURTLE); } - ValidationReport report = ShaclValidator.validate(data.getSail(), shapes.getSail()); + ValidationReport report = validate(data, shapes); assertFalse(report.conforms()); assertEquals(1, report.getValidationResult().size()); @@ -563,7 +565,7 @@ public void repeatedTemplatePlaceholdersAreAllReplaced() throws Exception { SailRepository shapes = new SailRepository(new MemoryStore()); try (SailRepositoryConnection connection = shapes.getConnection()) { - connection.add(new StringReader(shapesTtl), RDFFormat.TURTLE); + connection.add(new StringReader(shapesTtl), "", RDFFormat.TURTLE, RDF4J.SHACL_SHAPE_GRAPH); } SailRepository data = new SailRepository(new MemoryStore()); @@ -571,7 +573,7 @@ public void repeatedTemplatePlaceholdersAreAllReplaced() throws Exception { connection.add(new StringReader(dataTtl), RDFFormat.TURTLE); } - ValidationReport report = ShaclValidator.validate(data.getSail(), shapes.getSail()); + ValidationReport report = validate(data, shapes); assertFalse(report.conforms()); assertEquals(1, report.getValidationResult().size()); @@ -604,7 +606,7 @@ public void messageBindingNonLiteralConvertedToStringLiteral() throws Exception SailRepository shapes = new SailRepository(new MemoryStore()); try (SailRepositoryConnection connection = shapes.getConnection()) { - connection.add(new StringReader(shapesTtl), RDFFormat.TURTLE); + connection.add(new StringReader(shapesTtl), "", RDFFormat.TURTLE, RDF4J.SHACL_SHAPE_GRAPH); } SailRepository data = new SailRepository(new MemoryStore()); @@ -612,7 +614,7 @@ public void messageBindingNonLiteralConvertedToStringLiteral() throws Exception connection.add(new StringReader(dataTtl), RDFFormat.TURTLE); } - ValidationReport report = ShaclValidator.validate(data.getSail(), shapes.getSail()); + ValidationReport report = validate(data, shapes); assertFalse(report.conforms()); assertEquals(1, report.getValidationResult().size()); @@ -657,7 +659,7 @@ public void preboundShapesGraphAvailableInSparqlConstraints() throws Exception { connection.add(new StringReader(dataTtl), RDFFormat.TURTLE); } - ValidationReport report = ShaclValidator.validate(data.getSail(), shapes.getSail()); + ValidationReport report = validate(data, shapes); assertFalse(report.conforms()); assertEquals(1, report.getValidationResult().size()); @@ -689,7 +691,7 @@ public void preboundCurrentShapeAvailableInSparqlConstraints() throws Exception SailRepository shapes = new SailRepository(new MemoryStore()); try (SailRepositoryConnection connection = shapes.getConnection()) { - connection.add(new StringReader(shapesTtl), RDFFormat.TURTLE); + connection.add(new StringReader(shapesTtl), "", RDFFormat.TURTLE, RDF4J.SHACL_SHAPE_GRAPH); } SailRepository data = new SailRepository(new MemoryStore()); @@ -697,7 +699,7 @@ public void preboundCurrentShapeAvailableInSparqlConstraints() throws Exception connection.add(new StringReader(dataTtl), RDFFormat.TURTLE); } - ValidationReport report = ShaclValidator.validate(data.getSail(), shapes.getSail()); + ValidationReport report = validate(data, shapes); assertFalse(report.conforms()); assertEquals(1, report.getValidationResult().size()); @@ -742,7 +744,7 @@ public void messageTemplatesSubstitutePreboundVariablesForNodeShapes() throws Ex connection.add(new StringReader(dataTtl), RDFFormat.TURTLE); } - ValidationReport report = ShaclValidator.validate(data.getSail(), shapes.getSail()); + ValidationReport report = validate(data, shapes); assertFalse(report.conforms()); assertEquals(1, report.getValidationResult().size()); @@ -796,7 +798,7 @@ public void messageTemplatesSubstitutePreboundVariablesForPropertyShapes() throw connection.add(new StringReader(dataTtl), RDFFormat.TURTLE); } - ValidationReport report = ShaclValidator.validate(data.getSail(), shapes.getSail()); + ValidationReport report = validate(data, shapes); assertFalse(report.conforms()); assertEquals(1, report.getValidationResult().size()); @@ -843,7 +845,7 @@ public void messageTemplatesSubstitutePathPlaceholderForPropertyShapes() throws SailRepository shapes = new SailRepository(new MemoryStore()); try (SailRepositoryConnection connection = shapes.getConnection()) { - connection.add(new StringReader(shapesTrig), RDFFormat.TURTLE); + connection.add(new StringReader(shapesTrig), "", RDFFormat.TURTLE, RDF4J.SHACL_SHAPE_GRAPH); } SailRepository data = new SailRepository(new MemoryStore()); @@ -851,7 +853,7 @@ public void messageTemplatesSubstitutePathPlaceholderForPropertyShapes() throws connection.add(new StringReader(dataTtl), RDFFormat.TURTLE); } - ValidationReport report = ShaclValidator.validate(data.getSail(), shapes.getSail()); + ValidationReport report = validate(data, shapes); assertFalse(report.conforms()); assertEquals(1, report.getValidationResult().size()); @@ -889,7 +891,7 @@ public void pathPlaceholderWorksForInversePaths() throws Exception { SailRepository shapes = new SailRepository(new MemoryStore()); try (SailRepositoryConnection connection = shapes.getConnection()) { - connection.add(new StringReader(shapesTtl), RDFFormat.TURTLE); + connection.add(new StringReader(shapesTtl), "", RDFFormat.TURTLE, RDF4J.SHACL_SHAPE_GRAPH); } SailRepository data = new SailRepository(new MemoryStore()); @@ -897,7 +899,7 @@ public void pathPlaceholderWorksForInversePaths() throws Exception { connection.add(new StringReader(dataTtl), RDFFormat.TURTLE); } - ValidationReport report = ShaclValidator.validate(data.getSail(), shapes.getSail()); + ValidationReport report = validate(data, shapes); assertFalse(report.conforms()); assertEquals(1, report.getValidationResult().size()); @@ -932,7 +934,7 @@ public void illegalPathPlaceholderUseCausesFailure() throws Exception { SailRepository shapes = new SailRepository(new MemoryStore()); try (SailRepositoryConnection connection = shapes.getConnection()) { - connection.add(new StringReader(shapesTtl), RDFFormat.TURTLE); + connection.add(new StringReader(shapesTtl), "", RDFFormat.TURTLE, RDF4J.SHACL_SHAPE_GRAPH); } SailRepository data = new SailRepository(new MemoryStore()); @@ -941,7 +943,7 @@ public void illegalPathPlaceholderUseCausesFailure() throws Exception { } SailException ex = assertThrows(SailException.class, - () -> ShaclValidator.validate(data.getSail(), shapes.getSail())); + () -> validate(data, shapes)); Throwable root = ex; while (root.getCause() != null) { root = root.getCause(); @@ -979,7 +981,7 @@ public void nonIriPathBindingIgnoredForPropertyShapes() throws Exception { SailRepository shapes = new SailRepository(new MemoryStore()); try (SailRepositoryConnection connection = shapes.getConnection()) { - connection.add(new StringReader(shapesTtl), RDFFormat.TURTLE); + connection.add(new StringReader(shapesTtl), "", RDFFormat.TURTLE, RDF4J.SHACL_SHAPE_GRAPH); } SailRepository data = new SailRepository(new MemoryStore()); @@ -987,7 +989,7 @@ public void nonIriPathBindingIgnoredForPropertyShapes() throws Exception { connection.add(new StringReader(dataTtl), RDFFormat.TURTLE); } - ValidationReport report = ShaclValidator.validate(data.getSail(), shapes.getSail()); + ValidationReport report = validate(data, shapes); assertFalse(report.conforms()); assertEquals(1, report.getValidationResult().size()); @@ -1001,6 +1003,15 @@ public void nonIriPathBindingIgnoredForPropertyShapes() throws Exception { } } + private static ValidationReport validate(SailRepository data, SailRepository shapes) { + return ShaclValidator.builder() + .setRdfsSubClassReasoning(false) + .shapeContexts(RDF4J.SHACL_SHAPE_GRAPH, null) + .withShapes(shapes.getSail()) + .build() + .validate(data.getSail()); + } + private static void assertResultMessagesMatchConstraintMessages(ValidationReport report) { Model model = report.asModel(); for (ValidationResult result : report.getValidationResult()) { diff --git a/core/sail/shacl/src/test/java/org/eclipse/rdf4j/sail/shacl/results/ShaclValidatorTest.java b/core/sail/shacl/src/test/java/org/eclipse/rdf4j/sail/shacl/results/ShaclValidatorTest.java index 962e85b3a66..a70d20d942a 100644 --- a/core/sail/shacl/src/test/java/org/eclipse/rdf4j/sail/shacl/results/ShaclValidatorTest.java +++ b/core/sail/shacl/src/test/java/org/eclipse/rdf4j/sail/shacl/results/ShaclValidatorTest.java @@ -8,6 +8,7 @@ * * SPDX-License-Identifier: BSD-3-Clause ******************************************************************************/ +// Some portions generated by Codex package org.eclipse.rdf4j.sail.shacl.results; @@ -20,6 +21,7 @@ import org.eclipse.rdf4j.model.util.Values; 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; import org.eclipse.rdf4j.repository.sail.SailRepository; @@ -47,7 +49,7 @@ public void testDefaultGraphIsUnionValid() throws Exception { Values.iri("http://example.org/g1")); } - ValidationReport validate = ShaclValidator.validate(data.getSail(), shapes.getSail()); + ValidationReport validate = validate(data, shapes); assertTrue(validate.conforms()); } @@ -62,7 +64,7 @@ public void testDefaultGraphIsUnionFailureDefaultGraph() throws Exception { connection.add(Values.iri("http://example.org", "person1"), RDF.TYPE, RDFS.RESOURCE); } - ValidationReport validate = ShaclValidator.validate(data.getSail(), shapes.getSail()); + ValidationReport validate = validate(data, shapes); assertFalse(validate.conforms()); } @@ -77,7 +79,7 @@ public void testDefaultGraphIsUnionWarningDefaultGraph() throws Exception { connection.add(Values.iri("http://example.org", "person1"), RDF.TYPE, RDFS.RESOURCE); } - ValidationReport validate = ShaclValidator.validate(data.getSail(), shapes.getSail()); + ValidationReport validate = validate(data, shapes); assertFalse(validate.conforms()); assertEquals(1, validate.getValidationResult().size()); assertEquals(SHACL.WARNING, validate.getValidationResult().get(0).getSeverity().getIri()); @@ -95,7 +97,7 @@ public void testDefaultGraphIsUnionInfoDefaultGraph() throws Exception { connection.add(Values.iri("http://example.org", "person1"), RDF.TYPE, RDFS.RESOURCE); } - ValidationReport validate = ShaclValidator.validate(data.getSail(), shapes.getSail()); + ValidationReport validate = validate(data, shapes); assertFalse(validate.conforms()); assertEquals(1, validate.getValidationResult().size()); assertEquals(SHACL.INFO, validate.getValidationResult().get(0).getSeverity().getIri()); @@ -113,7 +115,7 @@ public void testDefaultGraphIsUnionFailureNamedGraph() throws Exception { Values.iri("http://example.org/g1")); } - ValidationReport validate = ShaclValidator.validate(data.getSail(), shapes.getSail()); + ValidationReport validate = validate(data, shapes); assertFalse(validate.conforms()); } @@ -143,9 +145,17 @@ private static SailRepository getShapes(String severity) throws IOException { SailRepository shapes = new SailRepository(new MemoryStore()); try (SailRepositoryConnection connection = shapes.getConnection()) { - connection.add(new StringReader(turtleString), RDFFormat.TURTLE); + connection.add(new StringReader(turtleString), "", RDFFormat.TURTLE, RDF4J.SHACL_SHAPE_GRAPH); } return shapes; } + private static ValidationReport validate(SailRepository data, SailRepository shapes) { + return ShaclValidator.builder() + .setRdfsSubClassReasoning(false) + .withShapes(shapes.getSail()) + .build() + .validate(data.getSail()); + } + } diff --git a/core/sail/shacl/src/test/resources/junit-platform.properties b/core/sail/shacl/src/test/resources/junit-platform.properties index 5f3e14bbeb4..4f4d53ce03f 100644 --- a/core/sail/shacl/src/test/resources/junit-platform.properties +++ b/core/sail/shacl/src/test/resources/junit-platform.properties @@ -2,7 +2,7 @@ junit.jupiter.execution.parallel.mode.default = same_thread junit.jupiter.execution.parallel.mode.classes.default = concurrent junit.jupiter.execution.parallel.enabled = true junit.jupiter.execution.parallel.config.strategy = fixed -junit.jupiter.execution.parallel.config.fixed.parallelism = 4 +junit.jupiter.execution.parallel.config.fixed.parallelism = 8 junit.jupiter.execution.timeout.default = 10m junit.jupiter.execution.timeout.mode = disabled_on_debug diff --git a/dist/debug-surefire.skill b/dist/debug-surefire.skill new file mode 100644 index 0000000000000000000000000000000000000000..16c5a78e99586476394f32afc5d6f2af9a19f4f0 GIT binary patch literal 3395 zcmai%c{o&U8^8xMkzJN-4Uw`&B5O)xNt!T-2{C5KWX%$??_1d=vNMz|j9nNzS+cy0 zY3wQn2_acO>b<`A>#eTuIoJJM&-vr|-S;`yb?)ltJ&eb%RAykC(0gM5O&g%zm?~# z>lx5;oFmh!QqJ+QuEBYK9?T#PSaL1zxZR@@5cTU;Ux~iN3Xf0An;z?n0+g(*=;8rO z+99zHDs;8R#^C(IiUX5fK>BqO!~x))o>q%srK3hOTA=#(Y15uP;Iv1N+R59~N$i!EMR8Cu;g6^eRg}LiOznbV=k>iRwi*5M==kb0D zbE>w4hsA{D^oz1q%aj{^vdx&1jUe80MLAi!xLQ8EZ?OELK#BpDDb-z;r~rL1e1&FvAs7&-$DdSPFAS>?PiSgQcY<=PI&K`Y+N?^IBv=d1WI8+_s$V z&Am0ne0CBxS3PSXX-nKDVxXXU{A zBPH#fR8=i>%*rz*D?{LwMS|^WaLYR`I*?Hh(p2&lww8&4=kUf1WP!g!gv*L`uM0vD zXu-zQ&C!%!-w=Xm?Gt56OQ3w_KnuE)RtxOumTIZzI>n&RM#ak1Xm?GZuGUHG)waS} ze~QZ9z_v>tX7-vOoYHB0CQ#{!VoOlO7B3 z@b%l}_xc z9REmfT-q5?nsS*z3*4&ii^PxUHh6BWWGZ8iuh6-b6foW`M6`6Fq)5-#rrZjrHOhlO z545@@^}Q2nq`P+inPxq0Dp$f2jRp6(fza4-I4@=juiQtS-v=OeK{Et4iPmXN%Vf9l zE?V(!v{w|bdNaFviH8rMV!xu-xsilSjY{aQQHf%Q%=$?Nryw2Ldh9_egVod*6BfHB z29N92@Grw9`p(Eqp$CDp!EZiOI@2p{PoQMMCTWq8^h};jrAg{7{&SL29g{di0*ygk zqKb->NT-SUi#)x$5k8-StQ`qYLv@?1P2>YXR^a^ZbyyKsqoOm{XGDJ8aKCr!s|EkWX zBoOwj!?OCK3t=U`(BxuSXT3##QOLFozZ1S0@R~<6ke@$m^Ra3G0@m+bl8hyiH=cKk- zO$tT|1+weRYI-v+A=9T;tYJG|H8!9YYIT!ykqc?eX)~NV%$cMzRbyHZelMMBNip0& z$nQzYLZ)Tfb&b3%+r3DuoR}N^UfH=KWC{Tz=^**gzs?K4;2KPh#jm|DPkJy!T4>3A z9rN<@^)U~rJ3`t0SNB+x=r`DQUbdZBt(9%W`YqR7bxp6?>DKMEl`6yP@hqa8&lN&3Nr!fXHA1i(j0|eGqC|Y$9A2*~q3)ezR~t1=UEq zp3XwC8#>3R_s%BWy*NlaM24S#BkQ2}>-gEg$*p0cV)EUkRU@!8>!XPSvLnU#VFW#g zMi8n0pb|?C0PKMPfZr&FuBOJ#o9As&-vuD!@aKON{}O;AJ*YiVg0_CgfU!4|PMbg| zA&2-HkK{mT$`{v4PX%YfY~^nAYZjlW>Tcvg%h@t7Ri@GEtb4rYPiIGlF;#FwPJbp! ze<_N?I7$lTyiL9DUuH?GTN_}#dWKGPo7Ug(Z%%}_sWbVtAZo()umv>J=7;^zk{+%NJbyCXri;By5XkottV#v>YU)J$iOXQdxW)7XV5ow-JF>;PT7;4Iye?M5cPZRBy2W8Z#O!gjVF(aU6aa5=aYhZ!kFu)>OusL!P=2Vt zM#j@Xd6`@Nqu(o=f{2$UgKDR~w0N`9(YN*LzjO@6Zkz6V&(vTP$C`T#`0zWmqJrhP zZo#*8jI~+2>$uREdW*o>%VlJ3Y)^Bd!uxNGW$dRtMTA`p)f+EOgl*|!K*&g~)M5|25$ZQwHz zymBa(lCxmF<9i@!RgeOy$d0tS2xig}#GWHnrkeA<@dZPGyLd>z(BoIyb7Q*EBzZ4a z3dCdZ>AVF{vKQ8wFEI{gwe$q~va@@w9hT+uhf(S4j2C`}wD59KwIoFSIp5#|~=*01q#(!g&*Mgds!_Wat)op(!Du@@nT`(t)ZPIADF0sF z{N1068p1TRv_#zOi;M=`abm#cUxEz1`Cc@d>f@=&YvT|FDphv>WV=4~fw%Wm%bxZp z-nPOt{ZSBwu@hT$iGb$;GG7cdSNvjpHl-J18A{`7b*0Z9Ai$3hZck@AofzjjIHahs@<=8n{PUmF*4e+$P{~lI4aglLNgAg+ z#sNoM1n&j&xP_FfHSDR+@nRqTx+>Le))Fj6UvvQO^nD@h70)uf4(Rm(lG%X{dX`$rh> z)}C8ZMr^K;gY%^0A}U$nm25<%Dh-^Im%~2ULKoS_wfyit6%45@!w3Kwwf#uG<}uQK zanyjp`Xxp$4{sl>_hgnWd2!LW*si1Djb7Ud;(^(T_JbeZXtKHMQ1kaP&`OiyX4?!! zB~~I!GY1Q9DzCT$8c7UBtSFRNLVA`;}?A^T|-f7g9`9q_Oz9wO`5Gbspc>Z**hy}!RTGg`c zXTDJKU_N?gnP&SP3wzL`1DSS5*}bZ2KyaO?O&-1*?GIrtwTADsjpHPh=_Zt!w2O|u za67x-Rldf?rKI19nG?LGK0I73jgd?CfKbG}A!tpBbH)Rf_~cn$gr@J=elI)b}?)AzDA$_-DPjifJI4J(e+8{0jrvU zfoiMci&uO5O&>8gzg*!pLm508V*#N9sZr@T%L#v>PdgY`Q*~*#D`p9sZ8=9)@$Og6 z56gyoBdF1~F5~WlLFMoUm+S=c5QHfAQhBLB9u4D=%*Il%h;D#WfN^@}#Tp%(;}^4G z@(jdIb|T-bH1$dwEj+!T3251rII}8;WapD43OA?HoD{NFyizlq;)tWCbtggl(g>n6 zRz%@b6i`)knEq5yHGfl~8(M^p^<_>HpbHTG`L_C5HX8Osop%S9F|D!{37p!cB;_5m zvnKe)nZk5dEM!_zl>y;8BrkZogqKLdDx*e!XWu!-AkelBF|FQ!`kVsKm^#R zISkENShtbldkc(wUk)KGsQ+0|>K@4S$q_Dx(!xljm8TW*0Q~{vm%vQUz5xc6)%4}z z5T}eRjt?lRG)yzFVM_9Iku|Wwo-B4gCwiT5x6!5cXrUH=B9fwHl?~_2B1mpM4$kMK z7^-=K`$XA$8Wh%fsjIm~N>|*(TIr+R{v|#=SQF+GToqV6kHhfPukBj-A}%K^XQMk} zFqPgCPLuYV{b`|^9)Uw*wM@*?euii7~{`N7>cmkj7<;t4MM*G)Nn^R~e zKlk*8E`$9&?NIVs>7*Cj2*$y*yfZNeAsOd(1?J~Dz=-v;tB~XKOs6(3-N@JF7Qgi& z6_F3XfVawljOEyr?Qw1#@4?Dlo6poWIg&yCk%YyxaY{Xd#Emf($H%=5IZ>n7Q#d~{ zxLrVf38Fh~D#2i8`>QG8;NhRjn(cF4H(U*$GP$0=KD;~7(`-}ln%iKr?6*-lwNr^> zOjK?CjIpN!2dQ7&vZ)$c`n!BNswvFG>qPuq-^`+$sh6>tO#Ml$6;d`$m3{D&ky$!f zvh^0A(z~SSf!xapDNn7{Xoc|MMb!#{5KpYRHEkK5LrXF#ycjkoSl2=IQT`G(!+BHdoe*CS$$b>ac_%2+FM3V#9paUf7I$ z6C#i{kMDAzkWn2692`HY#T>toL>ylHHmI2e6`9^7FBZZAk04Hud(JrD$}?Ux;>a&u z3sO(gh;TIn6=!+DJlU)U`n)cspHk(yrE4Ip42;>*(#Af_0)gtRQKQp5zRpaCqd7;9 z5D?cLeOtAIq%6+slBA>6Zage#D*OpmSRGQZ>lfi3fF-iXd71Z`Q4vd7*w;f>(d4*_ zx=3+uu_;0Us}JcMTx z8pI9VaNz=fS||4Owjb*-!)Lt<3`*v!Rr` zLVm>16U+!l57&J#^<^uk46#3*o)LXXrK}Bg#+;qC=%IRzKBu_cl@UJs$JePHbSQFU ztMs6pZsYZB2{=*~>CNIiLKLy$H$j!2j1EaS(;#yY>K)9Dd5jKzUtZc&Q_cbOrDl!D zlH(X1r^R@JQo_iU43KxbrQFpWC%{~d6Q>=1G47^^mi8zORBjo|<%h3^O-v&sn>C=_ivdZ|UC%cdWgtCnU z2_x?`WDTE*6}G!nXuZ1Ta=lD)NHTxjd(p}0_Dr&wSo+0HJk1|AJ4ch})8$R8<-+$( zSdm~k8bt{JeD0RfV|pL)$1=|(vUwDzL(|#XEmb8YUSfJGaU??%JR_kw%I;ieIOXK9 zojJ{Ac;Ta&SD{eT!5>LXetx#yfrp^zLE;eY>eI(%vs!-lki1V6DW~xc%?X`F^$11m zn_Hfgm8F{H*2$?WaHLC3pKNNgkPRdphYAx+J`?GO^tt;?TK1B?@iI-nC%xdXFodG3 zHBz|uA(O**BpK`Iv&gDuS_xpZA76&_C$ z`|47<6U0F&+-+X0`%SIsahfPD(Z6n620zu0H3lf^6*0%a`RJqKsyu$=lFWVbL1;>~ zu;4wwHR{o|yVhN5@9GbcCv>I&CD9X+dw?(hh_R7$&D`52%B zQtD1hdlsDqZ7ldnsQ6lstOm)`P z-i)?*u|1~M;vMhVSy$MZTh)m}b1U%|EI7fj_t2Uw!;76tAk6c(oxbaalL4!e`7mH{ z7(c|YE*7yyX=g8W4(l;#PVA zxZnIC2YQjg=s=E55{owc@n@CJv}M9J_JcgZd(5Q?Ib4b2p_-LPqRPhXmpE3=n~t0M z@(}QNm-zwD_s))drOUWwzxjogDB2b})brVv5R~ob384(b+Rs@m&{?{S9rG~tK>J(IydjzN2cbTDI+{O~-TfXh4V-;~+J-*xX*EygPpZY+akXP8q$I5RdU-n=Q={v94T2zD{eFoX1k#pDpo(8hS8C^rA-MNzD&f#=Tk!k{A606V$Q>(#J52dobvlnx*CwT zbKWYa&!G*Kql7M!UT{dYXzhtHoZ&gJKa(!&R4;LR$Pgh;OMu)D`)2a}QRxBfBl}Y8 zBqDqTp(a8ct`u|TN zjUWJH@TMkmaN5HOCC7E05O*f*CDsPR5pgK*JMs|^dUD-8RizN>l*fb z$n6DNnC$IK^4{9^VlkPE)3HC$XDrv(n-gPZ)*H%xI5oSr^^74irLFhhD(?wE*If~O z{m$+rB3@vQJE2d33}@c7P*XFER$3kNrA_x@BJZn8ESlMfUD>xp;qa0FbK+}b&g`Ta z(oV*{^1~bU(l+baB#9mWLx*addSLPH5f+1S-B_7g7s5&UeJcV2DHrTzsDP2-q>XKa#hKEl_@ZZ_@FFyQh u3M2lJetrue true true - true true true @@ -359,8 +358,7 @@ eclipse-settings/** **/target/** **/site/themes/** - **/.*/** - + **/.*/** **/dependency-reduced-pom.xml @@ -411,14 +409,6 @@ - - au.com.acegi - xml-format-maven-plugin - [0,) - - xml-format - - false @@ -746,15 +736,6 @@ maven-gpg-plugin 1.6 - - au.com.acegi - xml-format-maven-plugin - 3.3.1 - - true - eclipse-settings/**,**/target/**,**/site/themes/**,**/.*/** - - org.apache.maven.plugins maven-eclipse-plugin diff --git a/site/content/documentation/programming/shacl.md b/site/content/documentation/programming/shacl.md index e28c893016c..1de664f2294 100644 --- a/site/content/documentation/programming/shacl.md +++ b/site/content/documentation/programming/shacl.md @@ -94,6 +94,59 @@ data will not violate the shapes, or otherwise need to skip validation then you Do not use SPARQL to update your shapes! +## Standalone validation with ShaclValidator + +For non-transactional or one-off validation you can use the standalone `ShaclValidator` API. It lets you load shapes +and data from common inputs without creating a `ShaclSail`, while still giving access to the same validation options +(parallel validation, logging, RDFS reasoning, extensions, and limits). + +Provide shapes up front and validate multiple datasets: + +```java +import java.nio.file.Paths; +import org.eclipse.rdf4j.rio.RDFFormat; +import org.eclipse.rdf4j.sail.shacl.ShaclValidator; +import org.eclipse.rdf4j.sail.shacl.results.ValidationReport; + +ShaclValidator.ValidatorWithShapes validator = ShaclValidator.builder() + .setRdfsSubClassReasoning(false) + .withShapes(Paths.get("shapes.ttl"), "http://example.com/ns", RDFFormat.TURTLE) + .build(); + +ValidationReport report = validator.validate(Paths.get("data.ttl"), "http://example.com/ns", RDFFormat.TURTLE); +boolean conforms = report.conforms(); +``` + +Auto-detect formats when the file name or base URI has a known extension: + +```java +import java.io.File; + +ValidationReport report = ShaclValidator.builder() + .withShapes(new File("shapes.ttl")) + .build() + .validate(new File("data.ttl")); +``` + +You can also provide shapes per validation run: + +```java +import java.io.File; +import org.eclipse.rdf4j.sail.Sail; + +Sail dataSail = ...; // preloaded data sail +ShaclValidator.Validator validator = ShaclValidator.builder().build(); +ValidationReport report = validator.validate(dataSail, new File("shapes.ttl")); +``` + +Supported inputs for shapes and data are: + +- `File`, `Path`, `URL` (auto-detect or explicit `RDFFormat`) +- `InputStream` (explicit `RDFFormat`, or auto-detect using `baseURI`) +- `String` content (explicit `RDFFormat`, or auto-detect using `baseURI`) + +Note: Input streams are not closed by the validator; callers are responsible for closing them. + ## Supported SHACL features The SHACL W3C Recommendation defines the SHACL features that should be supported and RDF4J is working hard to @@ -666,4 +719,3 @@ Here are some useful links to learn more about SHACL: - [W3C SHACL specification](http://www.w3.org/TR/shacl/) - [Validating RDF Data](http://book.validatingrdf.com/) (various authors) - From 13f80a42f4acc4cb6f015b70811796a8b6172237 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ha=CC=8Avard=20Ottestad?= Date: Thu, 25 Dec 2025 21:21:55 +0100 Subject: [PATCH 2/2] wip --- .codex/skills/debug-surefire/SKILL.md | 240 +++++++++++++++++++++++++- 1 file changed, 238 insertions(+), 2 deletions(-) diff --git a/.codex/skills/debug-surefire/SKILL.md b/.codex/skills/debug-surefire/SKILL.md index 4c25c72e373..1b5f579852f 100644 --- a/.codex/skills/debug-surefire/SKILL.md +++ b/.codex/skills/debug-surefire/SKILL.md @@ -1,14 +1,28 @@ --- name: debug-surefire -description: Debug Maven Surefire unit tests by running them in JDWP "wait for debugger" mode (`-Dmaven.surefire.debug`) and attaching IntelliJ/VS Code/jdb. Use when asked to debug/step through a failing JUnit test, attach a debugger to a Maven test run, or run `mvn test -Dtest=Class[#method]` suspended on a port (including multi-module `-pl` runs). +description: Debug Maven Surefire unit tests by running them in JDWP "wait for debugger" mode (`-Dmaven.surefire.debug`) and attaching to the forked test JVM using **jdb** (preferred for CLI/agent debugging), IntelliJ, or VS Code. Use when asked to debug/step through a failing JUnit test, attach a debugger to a Maven test run, or run `mvn test -Dtest=Class[#method]` suspended on a port (including multi-module `-pl` runs). The JVM will block at startup until a debugger attaches; the agent should attach with `jdb -attach :` and drive the session from the terminal. --- # debug-surefire -Run Maven Surefire tests suspended in JDWP so you can attach a debugger and step through the code. +Run Maven Surefire tests **suspended in JDWP** so you can attach a debugger and step through the code. +In headless/agent environments, **attach with `jdb`** (the JDK’s command-line debugger). + +## What this does + +- Runs `mvn test` with Surefire in debug mode via `-Dmaven.surefire.debug`. +- Surefire launches the **forked test JVM** with a JDWP socket and `suspend=y`, meaning: + - the forked JVM starts, + - prints “Listening for transport dt_socket at address: ”, + - then **waits** until a debugger attaches, + - only then does it begin executing tests. + +This is important: you do **not** attach to the `mvn` process itself; you attach to the **forked JVM** running the tests. ## Quick start +1) Start a suspended test run + - Debug a test class: - `.codex/skills/debug-surefire/scripts/debug-surefire.sh --test-class MyTest` - Debug a single test method (quote the `#`): @@ -17,6 +31,38 @@ Run Maven Surefire tests suspended in JDWP so you can attach a debugger and step - `.codex/skills/debug-surefire/scripts/debug-surefire.sh --module core/sail/shacl --test-class ShaclSailTest` - `.codex/skills/debug-surefire/scripts/debug-surefire.sh --module rdf4j-sail-shacl --test 'ShaclSailTest#testSomething'` +When the forked JVM is ready, you should see something like: + +- `Listening for transport dt_socket at address: 55005` + +2) Attach with `jdb` (preferred) + +In a second terminal, attach to the printed port: + +- Local attach (port only implies localhost): + - `jdb -attach 55005` +- Explicit host+port: + - `jdb -attach localhost:55005` + +If you need source listing inside jdb, provide a sourcepath up front: + +- `jdb -sourcepath module/src/main/java:module/src/test/java -attach 55005` + +3) Set breakpoints / catches, then resume + +Once you’re at the `jdb` prompt, set a breakpoint (or exception catch) and continue: + +- `stop in com.example.MyTest.shouldDoThing` +- `catch uncaught java.lang.AssertionError` +- `cont` + +Tip: right after attaching to a just-started suspended JVM, `jdb` may show: +- `No frames on the current call stack` + +That’s normal. The JVM hasn’t executed into any Java frames yet. Set breakpoints first, then `cont`. + +--- + ## Notes - The script runs a fast pre-test install (`-Pquick` into the repo-local `.m2_repo`) and then runs `mvn test` with Surefire in debug mode. @@ -24,3 +70,193 @@ Run Maven Surefire tests suspended in JDWP so you can attach a debugger and step - Use `--no-offline` / `--online` if offline (`-o`) resolution fails. - Everything after `--` is passed to Maven, e.g.: - `.codex/skills/debug-surefire/scripts/debug-surefire.sh --test-class MyTest -- -DtrimStackTrace=false -DfailIfNoTests=false -DforkCount=1 -DreuseForks=false` + +--- + +## jdb interaction guide + +This section is optimized for quickly getting signal from a failing unit test without drowning in framework/JDK internals. + +### Command cheat sheet (the ones you’ll actually use) + +**Start / resume** +- `cont` — continue execution from current stop/breakpoint +- `run` — start execution *when jdb launched the VM* (less relevant when you used `-attach`) + +**Breakpoints** +- `stop in .` — break on method entry +- `stop at :` — break at a specific line +- `stop` — list all breakpoints +- `clear .` / `clear :` — remove a breakpoint +- `clear` — list breakpoints (same idea as `stop` listing) + +**Stepping** +- `next` — step over calls (line-level) +- `step` — step into calls (line-level) +- `step up` — run until current method returns +- `stepi` — step one bytecode instruction (rarely needed) + +**Threads / stacks** +- `threads` — list threads +- `thread ` — select default thread +- `where` — stack trace for current thread +- `where all` — stack traces for all threads +- `up` / `down` — move the current frame up/down the stack + +**Inspect state** +- `locals` — print locals in current frame +- `print ` — evaluate/print an expression (same as `eval`) +- `dump ` — more complete object dump +- `set = ` — mutate a variable/field (use sparingly) + +**Source** +- `list` — show source around current line +- `list ` or `list ` — show a specific region +- `use :` (aka `sourcepath`) — set where jdb looks for sources + +**Exceptions** +- `catch uncaught ` — break when an uncaught exception occurs +- `catch caught ` — break when a caught exception occurs +- `catch all ` — break for both caught+uncaught +- `ignore ...` — undo a catch + +**Reduce noise** +- `exclude ,,...` — don’t report step/method events for matching classes +- `exclude none` — clear exclusions + +**Automation** +- `monitor ` — run a command every time the program stops (e.g., `monitor where`) +- `read ` — execute commands from a file +- `!!` — repeat last command +- ` ` — repeat command n times (e.g., `10 next`) + +### Efficient workflow for failing JUnit tests + +#### 1) Add exclusions immediately (so stepping doesn’t become a swamp) + +Right after connecting, set exclusions to avoid stepping into JDK/framework code: + +- `exclude java.*,javax.*,jdk.*,sun.*,com.sun.*,org.junit.*,org.junit.jupiter.*,org.assertj.*,org.hamcrest.*,org.mockito.*,org.apache.maven.*` + +You can always clear this later: + +- `exclude none` + +#### 2) Break where it matters + +Common breakpoint patterns: + +- Break at the failing test method: + - `stop in com.example.MyTest.shouldDoThing` + +- Break inside the code under test: + - `stop in com.example.service.FooService.doWork` + +- Break at a specific suspicious line: + - `stop at com.example.service.FooService:123` + +If the method is overloaded, specify argument types: + +- `stop in com.example.FooService.doWork(int,java.lang.String)` + +Note: `jdb` supports **deferred breakpoints**. If the class isn’t loaded yet, it will still accept the breakpoint and activate it when the class loads. + +#### 3) Break on the *failure*, not just your guess + +For unit tests, breaking on the thrown assertion/error is often faster than guessing a line. + +Useful catches: + +- Classic Java assertions / many test failures: + - `catch uncaught java.lang.AssertionError` + +- Common in JUnit 5 assertions: + - `catch uncaught org.opentest4j.AssertionFailedError` + +- NPE hunting: + - `catch uncaught java.lang.NullPointerException` + +You can remove a catch with `ignore`: + +- `ignore uncaught java.lang.AssertionError` + +Tip: `catch all java.lang.Throwable` is the nuclear option. It works, but it can get loud. + +#### 4) Resume and drive + +Once breakpoints/catches are set: + +- `cont` + +When you hit a breakpoint: + +- `where` to see the call stack +- `list` to see nearby source +- `locals` to see local variables +- `print someVar` or `print this.someField` to inspect +- `next` to step over, `step` to step into + +#### 5) Find the “real” test thread quickly + +Surefire + JUnit can spin up multiple threads (and Maven itself has its own). When in doubt: + +- `threads` +- `where all` + +Then pick the thread that’s in your test/code-under-test: + +- `thread ` +- `where` + +#### 6) Keep the JVM count predictable + +Surefire forking and parallelism can make debugging confusing. If you see multiple JVMs / inconsistent behavior, pass Maven flags to keep it single and fresh: + +- `-DforkCount=1 -DreuseForks=false` + +Example: + +- `.codex/skills/debug-surefire/scripts/debug-surefire.sh --test 'MyTest#shouldDoThing' -- -DforkCount=1 -DreuseForks=false` + +#### 7) Use “thread-only” breakpoints when concurrency matters + +By default, `jdb` breakpoints suspend all threads, which can create deadlocks in concurrent tests. You can tell jdb to suspend only the thread that hits the breakpoint: + +- `stop thread in com.example.concurrent.Worker.run` + +(You can also target a specific thread id with `stop thread in ...` if needed.) + +--- + +## jdb startup customization (optional but powerful) + +jdb will execute startup commands from `jdb.ini` or `.jdbrc` in either `user.home` or `user.dir`. + +This is handy for keeping your default exclusions/catches consistent, e.g.: + +- `exclude java.*,javax.*,jdk.*,sun.*,com.sun.*,org.junit.*,org.assertj.*` +- `catch uncaught java.lang.AssertionError` + +You can also keep a project-local command file and load it on demand: + +- `read .codex/skills/debug-surefire/jdb.cmds` + +--- + +## Troubleshooting + +- **jdb can’t connect** + - Confirm the port printed by the suspended JVM matches what you used in `jdb -attach`. + - Confirm you’re attaching to the forked JVM port (the one that prints “Listening for transport…”), not Maven’s PID. + - If running remotely (CI box / container / VM), ensure the port is reachable or forwarded. + +- **Breakpoints don’t hit** + - Use fully qualified class names. + - If overloaded, include argument types. + - Use `classes` to see what’s loaded. + - Try `stop at :` to avoid signature mismatch. + +- **`list` can’t find source** + - Set sourcepath: + - `use module/src/main/java:module/src/test/java` + - Or launch jdb with `-sourcepath ...` from the start.