diff --git a/CHANGELOG.md b/CHANGELOG.md index 2c27ef29b2..299c6d3ad1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,99 @@ +## [6.0.0] +Bootstrap 2 is gone, and with it the class vocabulary application stylesheets were written against. An app's `ac:stylesheet` points at its own XSLT, which imports the platform's `xsl/bootstrap/2.3.2/layout.xsl` — that import path is unchanged, and the `bs2:` modes it defines are all still there. An override that emits Bootstrap 2 markup therefore still compiles and still runs; it just emits classes no stylesheet backs any more, and renders unstyled with no diagnostic. Hence the major version: the failure gives no signal of its own. + +### Migration +- Legacy Bootstrap buttons (`btn`, `btn-primary`, `btn-large`, …) become the `ldhc-btn` intent/appearance/size vocabulary — `ldhc-btn in-primary ap-solid sz-md` +- `$ac:langs` and `$ac:lang` stylesheet parameters become the `ac:langs()` and `ac:langs()[1]` functions; an `xsl:with-param` left behind is silently ignored rather than reported +- `pull-left` and `pull-right` have no replacement — the design system lays out with flex and grid +- Overrides depending on `bootstrap.js`, jQuery, WYMEditor or the sprite icon layer need rewriting: dropdowns and modals are driven by IXSL templates, icons by the `msi` font, and `rdf:XMLLiteral` editing by the RDFa editor +- Stylesheets extending the removed vocabulary can be diffed against `ldh-bridge.css`, which carries the primitives LDH still emits + +### Added +- Design system port: the app shell (header, sidebar, footer), content blocks, action bar, breadcrumbs, mode lists, type badges, property lists, pager, modals and forms all render the design system's class vocabulary +- Vendored design system stylesheets and typefaces, with latin-ext subsets so Latin Extended-A no longer falls back mid-word +- `ldh-bridge.css` carries the primitives LDH still emits, replacing the Web-Client Bootstrap stylesheet link +- `ac:langs()` returns the reader's accepted languages, normalised to deduped primary subtags with an `en` floor — one function, an SSR body reading the writer-supplied parameter and a SaxonJS body reading `navigator.languages` +- `ac:lang-rank()` ranks a value's language against that list, so property values sort by the reader's preference +- `Content-Language` on HTML responses, negotiated against the languages the UI translation bundle actually ships +- `Vary: Accept-Language` and language-sensitive entity tags on language-negotiated representations, so a shared cache cannot serve one reader's rendering to another +- Every rendered literal declares its own language: lang-tagged values carry their tag, untagged values carry `lang=""`, typed non-string values inherit +- Predicate labels, object links and table cells declare the language their label was chosen in, which may differ from the document's +- `http-tests/language/`: six scripts covering `Content-Language` by reader and by media type, `Vary`, per-language entity tags, `` agreement and per-literal marking +- `ResponseHeadersFilterTest` covers the response filter's hypermedia — snapshot read-only modes, proxy suppression, the RFC 7089 relations and `Link` folding — alongside the language labelling +- `BundleLanguagesTest` covers deriving the supported languages from the translation bundle and the RFC 4647 matching behind it +- Spanish translations completed, so the bundle covers every UI string it claims to +- View table columns are sortable, keyed off each column's XSD datatype through `ldh:sort-key()` +- Facet toolbar, three-zone pager, chart controls grid, design-system list and grid modes +- Block drag-and-drop is payload-typed with an app MIME marker, with a single moving drop marker +- Copy-URI button reaches every block type through a context-free `ldh:CopyUriButton` mode +- Empty blocks and empty chart result sets say so through the design system's block-state component +- `ldh:date-time()`, `ldh:datatype-family()`, `ldh:css-token()` and `ldh:view-cache()` replace logic that had been written out at every call site +- Package ontologies are declared as `owl:imports` instead of grafted, with an imports characterization test +- `ldh packages list`, `ldh packages add` and `ldh packages remove` give the CLI the package management the settings modal has: the catalog read through the Linked Data proxy with the imported packages marked, and the `ldh:import` declaration written through `PATCH /settings` +- `ldh get` reaches a versioned document's RFC 7089 Memento roles through mutually exclusive `--timemap`, `--version ` and `--timegate` options, the last taking a `--datetime` in RFC 1123 or ISO 8601 and printing the negotiated version's URI as its only output + +### Changed +- **BREAKING**: `ldh` commands regroup by verb, dropping the `bin/` directory mirror: the hyphenated leaves split into `create`, `add` and `remove` groups (`ldh create item`, `ldh add view`, `ldh remove block`), the `imports`, `content`, `admin acl` and `admin ontologies` groups dissolve into them (`ldh import rdf`, `ldh add object-block`, `ldh admin create group`, `ldh admin add class`), `import` carries the composite workflows — `rdf`, `csv` and `admin import ontology` — apart from the atomic `add rdf-import`/`add csv-import`, and `admin acl add-agent-to-group` shortens to `ldh admin add agent`; the script → command table in `cli/README.md` maps every name +- `` is taken from the `Content-Language` the response carries, so the header and the document agree by construction rather than by two computations staying in step +- The language a page reports is the first accepted language the UI bundle has, not the reader's top preference — asking for German no longer puts `lang="de"` on a page written entirely in English +- Published language tags are the shortest the bundle justifies (`en`, not `en-US`), keeping a region subtag only where the bundle distinguishes two variants of one language +- Supported UI languages are derived from `translations.rdf` rather than declared beside it in `web.xml` +- Property lists and table cells order their values by the reader's language instead of hiding the ones that do not match, so a reader whose language the data lacks still sees every value +- Language negotiation moved out of `Application` into `LanguageNegotiator` +- Object metadata merges on the RDF term rather than the lexical form, so a tagged literal is no longer collapsed into an untagged twin +- Document responses pass the accepted languages into entity tag computation, so tags stop colliding across languages +- Legacy Bootstrap buttons move to the `ldhc-btn` intent/appearance/size vocabulary +- Block link columns route through the `ldh-drawer`; backlinks and copy-URI split into two placements chosen by what a click does +- Charts draw with resolved design tokens; the HTML Table chart is skinned in CSS and fills the block width +- Client-side HTTP moves to promise chains, retiring the legacy `ixsl:schedule-action` `http-request` form +- The busy cursor becomes a promise concern rather than 53 copies of the same `ixsl:set-style` +- Dropdown dismissal moves into IXSL, and the `DOMContentLoaded` block retires into an `ldh:CloseDropdown` mode +- Subject-control change handlers move from JavaScript into `ixsl:onchange` templates +- `ac:uuid()` generates through the platform, and the seven call sites that went around it stop doing so +- One navigation leaves one history entry +- CSR failures render through the design system's block states instead of `div.alert.alert-block` +- The RDFa editor's XMLLiteral canonicalization runs as a single `cm:canonical` pass in `ldh:FormPreSubmit` +- RDFa editor template names and modes move into their module namespaces +- Document-level map and graph modes fill the space between action bar and footer +- The map marker info window is the card itself, fitted into the map viewport and panning the least it can to reveal itself +- Editing forms already open on the page reconcile with the constructor after a constructor save +- The edit pencil on a resource description appears only under `acl:Write` +- `http-tests` assertions read captured responses from a here-string instead of piping `curl` into `grep -q` +- Web-Client dependency bumped to 5.0.4 + +### Removed +- Bootstrap 2: the framework stylesheets, `bootstrap.js`, jQuery, WYMEditor, the sprite icon layer and the `pull-left`/`pull-right` tokens +- The Bootstrap 2 class vocabulary that no stylesheet backed +- `$ac:langs` and `$ac:lang` parameters, replaced by `ac:langs()` and `ac:langs()[1]` at 36 call sites +- `ldt:lang`, which nothing declared, read or wrote +- `DEBUG:` output from `http-tests`, with the diagnostic-only `gh api` and `curl` calls and the helper that fed them + +### Fixed +- `curl ... | grep -q` was a race under `pipefail`: `grep -q` closes the pipe on its first match and `curl` dies with 141, failing the pipeline even though the assertion passed +- View sort's numeric keys yield an empty sequence rather than `NaN` for non-numeric values +- `DataTransfer.types` marshals to an XDM array under SaxonJS, so payload guards compare via `array:flatten` +- The content-body drag-and-drop guards spell the child step `./div`, since after `and` a bare `div` parses as the division operator +- Reading an `ixsl:call` result of two numbers as an XDM array hung the renderer with no diagnostic; it arrives as a sequence of doubles +- Nested cards no longer draw doubled borders, and `dl` column placement is corrected +- The DataTable converters recognise the derived numeric and dateTime types +- The violation renderer fails on an absent form again, rather than falling back to the rejected rendering +- Concurrent writes to a versioned application silently lost commits: the commit chain was keyed by file path, while the GitHub Contents API takes its optimistic lock on the branch head — two documents committing at once conflicted and the loser was dropped with only a log line. Commits to one branch now share a single chain +- A conflicting write is retried with a re-read blob SHA (`MAX_CONFLICT_RETRIES`) instead of being abandoned, so a writer outside this JVM no longer costs a version +- A rate-limited commit retried with an empty request body: the builder had already been spent by the first attempt, and `JsonObjectBuilder` yields its object only once +- `release.sh` published to Maven Central before its `git checkout master`, so a failed checkout left 5.10.0 on Central with no tag: the switches are now proven possible first, and once published the trap prints recovery steps instead of deleting the tag that records what was published +- `release.sh`'s clean check could not see `skip-worktree`/`assume-unchanged` files — git reports them as matching the index whatever is on disk, while a branch switch refuses to overwrite them; they are now warned about at startup and checked precisely before publishing +- `release.sh` derived the release and snapshot commits positionally (`git log -2`), which breaks as soon as anything else commits in between; they are derived from an anchor taken before `release:prepare` +- A drop-down flips to whichever side of its trigger has room, on both axes, so the action bar's left-zone menu no longer opens off the viewport edge +- The language tag renders as a pill wherever a value is laid out rather than only inside a property list, so a table cell's tag stops running into the value it annotates +- A view block derived from the ontology keeps its URI across reloads: the fragment is hashed from the host resource and the view instead of drawn from `ac:uuid()`, and a fragment scroll waits for the injected blocks to hydrate +- An injected view block's subject keeps the hash that makes it a fragment +- A `pre` in an `ldh:XHTML` block takes the recessed code surface the inline chip and error listing already share, instead of running on the bare page background +- The property-list grid keys on the wrapped `dt`/`dd` shape, leaving prose `dl`s to their own layout +- An error page renders without the action bar, which had nothing left to act on + +### Known limitations +- The in-place editor edits tab-group content but cannot create tab groups: while a region is edited every tab panel is shown stacked and editable, and the markup round-trips intact, but there is no gesture that inserts a new tab group — that markup is authored via the HTTP API (e.g. `ldh put`) for now + ## [5.10.0] - 2026-08-30 ### Added - `ldh` command line interface (`cli/`): a standalone picocli/Jena port of the `bin/` HTTP API scripts — one command per script with the same option names, `bin/` subdirectories as nested subcommand groups, PKCS12 WebID keystore authentication, env-var defaults and a shaded executable jar diff --git a/CLAUDE.md b/CLAUDE.md index 7b4ec903ca..327d505e3c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -148,15 +148,16 @@ The SPARQL endpoint forwarding chain ensures ContentMode blocks (charts, maps) q ## CLI Tools `ldh` (in `cli/`) is the command line interface for the HTTP API — one command per `bin/` script, -same option names, `bin/` subdirectories as nested subcommand groups. Built with Maven on Java 21 +same option names, grouped by verb (`create`, `add`, `remove`, `import`) plus the `packages` +family and the `admin` scope. Built with Maven on Java 21 into a shaded `cli/target/ldh.jar` that `cli/bin/ldh` launches. See `cli/README.md` for the full script → command table and the behavioral differences from the scripts. ```bash cd cli && mvn package && export PATH="$PWD/bin:$PATH" -ldh create-container --parent "$LDH_BASE" --title "Some" --slug some -ldh admin acl add-agent-to-group --agent "$AGENT_URI" "${ADMIN_BASE}acl/groups/writers/" +ldh create container --parent "$LDH_BASE" --title "Some" --slug some +ldh admin add agent --agent "$AGENT_URI" "${ADMIN_BASE}acl/groups/writers/" ``` `cli/` is not a module of the platform reactor (the root pom is the webapp artifact, so it cannot @@ -165,11 +166,18 @@ around both release bumps, and `make cli-version` re-aligns it if it drifts. `LDH_CERT_FILE`, `LDH_CERT_PASSWORD`, `LDH_BASE` and `LDH_PROXY` supply defaults for `-f`, `-p`, `-b` and `--proxy`. Commands that create or append to a document print its URL as the only line on -stdout (diagnostics go to stderr), so `item=$(ldh create-item ...)` works; exit codes are `0` +stdout (diagnostics go to stderr), so `item=$(ldh create item ...)` works; exit codes are `0` success, `1` HTTP or runtime failure, `2` usage error. -Packages have no command — an application imports one with a single ` ldh:import ` -triple, so `ldh patch` on the application's `settings` document is the whole interface. +Packages are declarative: an application imports one with a single ` ldh:import ` +triple in its settings, and `ldh packages list`, `ldh packages add` and +`ldh packages remove` write that triple through `PATCH /settings`. `packages list` reads the +registry catalog through the Linked Data proxy, marking the imported ones. + +`ldh get` also addresses the RFC 7089 Memento roles of a versioned document, as mutually exclusive +options: `--timemap` (version history), `--version ` (a historical version) and `--timegate` +with an optional `--datetime` (RFC 1123 or ISO 8601), which prints the selected version's URI as the +only line on stdout so it pipes into another `ldh get`. The `bin/` HTTP API scripts are **deprecated** — `ldh` replaces them, and http-tests build their fixtures with it. Authentication moves from the `.pem` the scripts feed `curl -E` to the PKCS12 diff --git a/README.md b/README.md index fec89f62bb..22489f6d9a 100644 --- a/README.md +++ b/README.md @@ -297,11 +297,11 @@ export LDH_CERT_FILE=./ssl/owner/keystore.p12 export LDH_CERT_PASSWORD=$(cat secrets/owner_cert_password.txt) export LDH_BASE=https://localhost:4443/ -ldh create-container --parent "$LDH_BASE" --title "Concepts" --slug concepts -ldh create-item --container "${LDH_BASE}concepts/" --title "Example" --slug example +ldh create container --parent "$LDH_BASE" --title "Concepts" --slug concepts +ldh create item --container "${LDH_BASE}concepts/" --title "Example" --slug example ``` -Commands that create or append to a document print its URL as the only line on stdout, so they compose in shell pipelines: `item=$(ldh create-item ...)`. The `bin/` subdirectories became nested subcommand groups — `ldh admin acl create-group`, `ldh content add-xhtml-block`, `ldh imports import-csv`. See [`cli/README.md`](https://github.com/AtomGraph/LinkedDataHub/blob/master/cli/README.md) for the full command table and the differences from the scripts. +Commands that create or append to a document print its URL as the only line on stdout, so they compose in shell pipelines: `item=$(ldh create item ...)`. Commands group by verb — `ldh create container`, `ldh add xhtml-block`, `ldh import csv`, `ldh admin create group` — with `import` holding the workflows that compose the atomic commands. See [`cli/README.md`](https://github.com/AtomGraph/LinkedDataHub/blob/master/cli/README.md) for the full command table and the differences from the scripts. _:warning: The `bin/` HTTP API scripts that `ldh` replaces are **deprecated**. The certificate and WebID tooling (`webid-keygen.sh`, `webid-keygen-pem.sh`, `webid-uri.sh`, `webid-modulus.sh`, `server-cert-gen.sh`) talks to no API and stays in `bin/`._ diff --git a/cli/README.md b/cli/README.md index fa41b32a39..b3f7c5289a 100644 --- a/cli/README.md +++ b/cli/README.md @@ -1,11 +1,24 @@ # LinkedDataHub CLI `ldh` is a command line interface for the [LinkedDataHub](https://github.com/AtomGraph/LinkedDataHub) HTTP API. -It mirrors the shell scripts in [`bin/`](../bin) one command per script, with the same option names, +It covers the shell scripts in [`bin/`](../bin) one command per script, with the same option names, implemented in Java on top of AtomGraph [Core](https://github.com/AtomGraph/Core)'s `GraphStoreClient` (picocli + Apache Jena). It replaces the scripts' external dependencies (`curl`, `turtle`, `python`, `uuidgen`, `shasum`) with a single executable jar. +Commands group by verb: `create` `PUT`s a new document, `add` `POST`s an append onto an existing +one, `remove` likewise names the single request it makes, and `import` holds the workflows that +compose the atomic commands. `admin` scopes the same verbs to the admin application, and +`packages` manages the application's package imports: + +```bash +ldh create container --parent "$LDH_BASE" --title "Some" --slug some +ldh add view ... +ldh import csv ... # workflow: add construct + add file + add csv-import +ldh admin create ontology ... +ldh packages list +``` + The `bin/` HTTP API scripts it replaces are deprecated. The [http-tests](../http-tests) suite builds all of its fixtures with `ldh`, so the commands are exercised against a live instance on every CI run; `run.sh` aborts if `ldh` is not on `PATH`. @@ -81,14 +94,14 @@ Repeated options can be set once via environment variables: ```bash export LDH_CERT_FILE=ssl/owner/keystore.p12 LDH_CERT_PASSWORD=... LDH_BASE=https://localhost:4443/ -ldh create-container --parent "$LDH_BASE" --title "Some" --slug some -ldh create-item --container https://localhost:4443/some/ --title "My item" --slug my-item +ldh create container --parent "$LDH_BASE" --title "Some" --slug some +ldh create item --container https://localhost:4443/some/ --title "My item" --slug my-item ``` ## Conventions - Commands that create or append to a document print its URL as the only line on stdout, so shell - pipelines keep working: `item=$(ldh create-item ...)`. `add-file` prints the content-addressed + pipelines keep working: `item=$(ldh create item ...)`. `add file` prints the content-addressed upload URI (`{base}uploads/{sha1}`). All diagnostics go to stderr. - Exit codes: `0` success, `1` HTTP error status or runtime failure (message on stderr, stack trace with `--verbose`), `2` usage error. @@ -100,6 +113,67 @@ ldh create-item --container https://localhost:4443/some/ --title "My item" --slu Shell completion: `source <(ldh generate-completion)` (bash/zsh). +## Memento + +`get` also addresses the three [RFC 7089](https://datatracker.ietf.org/doc/html/rfc7089) roles a +document serves when its application is versioned — the version history, a historical version, and +the TimeGate that negotiates between them on datetime. They are mutually exclusive: + +```bash +doc=https://localhost:4443/some/ + +ldh get --accept text/turtle --timemap "$doc" # version history, described with PROV-O +ldh get --accept application/link-format --timemap "$doc" +ldh get --accept text/turtle --version "$doc" # that version, with a Memento-Datetime +``` + +`--timegate` asks the server which version was current at a given time and prints the selected +version's URI as the only line on stdout, so it composes: + +```bash +memento=$(ldh get --timegate --datetime "2026-08-20T10:00:00Z" "$doc") +ldh get --accept text/turtle "$memento" +``` + +`--datetime` takes either an RFC 1123 (`Wed, 20 Aug 2026 10:00:00 GMT`) or an ISO 8601 +(`2026-08-20T10:00:00Z`) datetime, so a `prov:generatedAtTime` read out of a TimeMap can be handed +straight back. Without it the TimeGate selects the most recent version. `--accept` is required for +every other request but not for `--timegate`, whose `302` has no representation of its own. + +## Packages + +A package bundles an ontology with an optional stylesheet, and an application imports one by +declaring a single `ldh:import` triple in its settings — the declaration *is* the installation. +The commands write that triple; they do not install anything of their own. + +```bash +ldh packages list +# available https://packages.linkeddatahub.com/skos/#this SKOS + +ldh packages add --package https://packages.linkeddatahub.com/skos/#this +ldh packages remove --package https://packages.linkeddatahub.com/skos/#this +``` + +Both take effect on the next request, with no restart: the package ontology joins the +application's `owl:imports` closure and its stylesheet is composed into the application +stylesheet. `packages remove` is idempotent — removing an import the application does not +have succeeds and changes nothing. + +`packages list` prints one tab-separated line per package — state, URI, title — so the listing +greps and cuts: + +```bash +ldh packages list | grep ^available | cut -f2 +``` + +The registry defaults to `https://packages.linkeddatahub.com/` and `--registry` overrides it. It is +read through the application's Linked Data proxy rather than fetched directly, the same way the +application settings modal reads it, so `packages list` needs `--base` as much as the other two do. + +The commands go through `PATCH /settings`, which is the live path: the change is in effect +immediately but lives in the running application's context dataset. Declaring the same +`ldh:import` triple in `config/dataspaces.trig` is the permanent one, applied on restart. + ## Script → command migration | Script | Command | @@ -109,53 +183,54 @@ Shell completion: `source <(ldh generate-completion)` (bash/zsh). | `put.sh` | `ldh put` | | `patch.sh` | `ldh patch` | | `delete.sh` | `ldh delete` | -| `create-item.sh` | `ldh create-item` | -| `create-container.sh` | `ldh create-container` | -| `add-view.sh` | `ldh add-view` | -| `add-construct.sh` | `ldh add-construct` | -| `add-select.sh` | `ldh add-select` | -| `add-result-set-chart.sh` | `ldh add-result-set-chart` | -| `add-file.sh` | `ldh add-file` | -| `add-generic-service.sh` | `ldh add-generic-service` | -| `admin/clear-ontology.sh` | `ldh admin clear-ontology` | -| `admin/add-ontology-import.sh` | `ldh admin add-ontology-import` | -| `admin/ontologies/create-ontology.sh` | `ldh admin ontologies create-ontology` | -| `admin/ontologies/import-ontology.sh` | `ldh admin ontologies import-ontology` | -| `admin/ontologies/add-class.sh` | `ldh admin ontologies add-class` | -| `admin/ontologies/add-constructor.sh` | `ldh admin ontologies add-constructor` | -| `admin/ontologies/add-select.sh` | `ldh admin ontologies add-select` | -| `admin/ontologies/add-property-constraint.sh` | `ldh admin ontologies add-property-constraint` | -| `admin/ontologies/add-restriction.sh` | `ldh admin ontologies add-restriction` | -| `admin/acl/create-group.sh` | `ldh admin acl create-group` | -| `admin/acl/create-authorization.sh` | `ldh admin acl create-authorization` | -| `admin/acl/add-agent-to-group.sh` | `ldh admin acl add-agent-to-group` | -| `admin/acl/make-public.sh` | `ldh admin acl make-public` | -| `content/add-object-block.sh` | `ldh content add-object-block` | -| `content/add-xhtml-block.sh` | `ldh content add-xhtml-block` | -| `content/remove-block.sh` | `ldh content remove-block` | -| `imports/add-csv-import.sh` | `ldh imports add-csv-import` | -| `imports/add-rdf-import.sh` | `ldh imports add-rdf-import` | -| `imports/import-csv.sh` | `ldh imports import-csv` | -| `imports/import-rdf.sh` | `ldh imports import-rdf` | +| `create-item.sh` | `ldh create item` | +| `create-container.sh` | `ldh create container` | +| `add-view.sh` | `ldh add view` | +| `add-construct.sh` | `ldh add construct` | +| `add-select.sh` | `ldh add select` | +| `add-result-set-chart.sh` | `ldh add result-set-chart` | +| `add-file.sh` | `ldh add file` | +| `add-generic-service.sh` | `ldh add generic-service` | +| `admin/clear-ontology.sh` | `ldh admin clear ontology` | +| `admin/add-ontology-import.sh` | `ldh admin add ontology-import` | +| `admin/ontologies/create-ontology.sh` | `ldh admin create ontology` | +| `admin/ontologies/import-ontology.sh` | `ldh admin import ontology` | +| `admin/ontologies/add-class.sh` | `ldh admin add class` | +| `admin/ontologies/add-constructor.sh` | `ldh admin add constructor` | +| `admin/ontologies/add-select.sh` | `ldh admin add select` | +| `admin/ontologies/add-property-constraint.sh` | `ldh admin add property-constraint` | +| `admin/ontologies/add-restriction.sh` | `ldh admin add restriction` | +| `admin/acl/create-group.sh` | `ldh admin create group` | +| `admin/acl/create-authorization.sh` | `ldh admin create authorization` | +| `admin/acl/add-agent-to-group.sh` | `ldh admin add agent` | +| `admin/acl/make-public.sh` | `ldh admin make-public` | +| `content/add-object-block.sh` | `ldh add object-block` | +| `content/add-xhtml-block.sh` | `ldh add xhtml-block` | +| `content/remove-block.sh` | `ldh remove block` | +| `imports/add-csv-import.sh` | `ldh add csv-import` | +| `imports/add-rdf-import.sh` | `ldh add rdf-import` | +| `imports/import-csv.sh` | `ldh import csv` | +| `imports/import-rdf.sh` | `ldh import rdf` | Local certificate tooling (`webid-keygen.sh`, `webid-keygen-pem.sh`, `webid-uri.sh`, `webid-modulus.sh`, `server-cert-gen.sh`) and the experimental `sitemap/` generator remain shell scripts. -Packages have no command: an application imports one with a single ` ldh:import ` -triple, so `ldh patch` on the application's `/settings` document is the whole interface. +`bin/admin/packages/` has no counterpart under that name: a package is imported by declaring an +`ldh:import` triple rather than by installing files, and `ldh packages list`, +`ldh packages add` and `ldh packages remove` write that declaration. ### Differences from the scripts - `-f/--cert-pem-file` is now `-f/--cert-file` and takes the `.p12` keystore directly — no PEM conversion needed. -- `create-group` writes the `--name` value into `foaf:name`/`dct:title` (the script wrote an +- `admin create group` writes the `--name` value into `foaf:name`/`dct:title` (the script wrote an unset variable, producing empty literals). -- `add-generic-service` drops the documented-but-unparsed `--slug` option. -- `add-csv-import`/`import-csv` default `--delimiter` to `,` (the script required it despite +- `add generic-service` drops the documented-but-unparsed `--slug` option. +- `add csv-import`/`import csv` default `--delimiter` to `,` (the script required it despite documenting a default). -- `import-csv`/`import-rdf` run their steps in-process instead of spawning subscripts, and pass +- `import csv`/`import rdf` run their steps in-process instead of spawning subscripts, and pass `--description` through to the import metadata. -- `import-ontology` reads the `construct-constructors` query text by dereferencing its document - instead of going through a `SELECT` on `/sparql`; the CONSTRUCT it then runs over the scratch - graph is unchanged. +- `admin import ontology` reads the `construct-constructors` query text by dereferencing its + document instead of going through a `SELECT` on `/sparql`; the CONSTRUCT it then runs over the + scratch graph is unchanged. diff --git a/cli/pom.xml b/cli/pom.xml index a3564f7c0c..13acddc2d9 100644 --- a/cli/pom.xml +++ b/cli/pom.xml @@ -4,7 +4,7 @@ com.atomgraph linkeddatahub-cli - 5.10.0 + 5.10.1-SNAPSHOT jar LinkedDataHub CLI diff --git a/cli/src/main/java/com/atomgraph/linkeddatahub/cli/CommandGroup.java b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/CommandGroup.java index 97a01bb2a1..fe2dd2542e 100644 --- a/cli/src/main/java/com/atomgraph/linkeddatahub/cli/CommandGroup.java +++ b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/CommandGroup.java @@ -24,7 +24,8 @@ /** * Base class for commands that only group subcommands and do nothing on their own, - * mirroring the subdirectories of the deprecated bin/ scripts. + * grouping them by verb (create, add, remove, list, import) + * or scope (admin). * * @author Martynas Jusevičius {@literal } */ diff --git a/cli/src/main/java/com/atomgraph/linkeddatahub/cli/LDH.java b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/LDH.java index 363eadfcec..64dc0ac666 100644 --- a/cli/src/main/java/com/atomgraph/linkeddatahub/cli/LDH.java +++ b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/LDH.java @@ -16,22 +16,17 @@ package com.atomgraph.linkeddatahub.cli; -import com.atomgraph.linkeddatahub.cli.command.AddConstruct; -import com.atomgraph.linkeddatahub.cli.command.AddFile; -import com.atomgraph.linkeddatahub.cli.command.AddGenericService; -import com.atomgraph.linkeddatahub.cli.command.AddResultSetChart; -import com.atomgraph.linkeddatahub.cli.command.AddSelect; -import com.atomgraph.linkeddatahub.cli.command.AddView; -import com.atomgraph.linkeddatahub.cli.command.CreateContainer; -import com.atomgraph.linkeddatahub.cli.command.CreateItem; +import com.atomgraph.linkeddatahub.cli.command.Add; +import com.atomgraph.linkeddatahub.cli.command.Create; import com.atomgraph.linkeddatahub.cli.command.Delete; import com.atomgraph.linkeddatahub.cli.command.Get; +import com.atomgraph.linkeddatahub.cli.command.Packages; import com.atomgraph.linkeddatahub.cli.command.Patch; import com.atomgraph.linkeddatahub.cli.command.Post; import com.atomgraph.linkeddatahub.cli.command.Put; +import com.atomgraph.linkeddatahub.cli.command.Remove; import com.atomgraph.linkeddatahub.cli.command.admin.Admin; -import com.atomgraph.linkeddatahub.cli.command.content.Content; -import com.atomgraph.linkeddatahub.cli.command.imports.Imports; +import com.atomgraph.linkeddatahub.cli.command.imports.Import; import org.apache.jena.sys.JenaSystem; import picocli.AutoComplete; import picocli.CommandLine; @@ -50,9 +45,8 @@ description = "Command line interface for the LinkedDataHub HTTP API.", subcommands = { Get.class, Post.class, Put.class, Patch.class, Delete.class, - CreateItem.class, CreateContainer.class, - AddView.class, AddConstruct.class, AddSelect.class, AddResultSetChart.class, AddFile.class, AddGenericService.class, - Admin.class, Content.class, Imports.class, + Create.class, Add.class, Remove.class, Import.class, + Packages.class, Admin.class, AutoComplete.GenerateCompletion.class }) public class LDH diff --git a/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/Add.java b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/Add.java new file mode 100644 index 0000000000..dd09ff0bc3 --- /dev/null +++ b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/Add.java @@ -0,0 +1,37 @@ +/* + * Copyright 2026 Martynas Jusevičius . + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.atomgraph.linkeddatahub.cli.command; + +import com.atomgraph.linkeddatahub.cli.CommandGroup; +import com.atomgraph.linkeddatahub.cli.command.content.AddObjectBlock; +import com.atomgraph.linkeddatahub.cli.command.content.AddXHTMLBlock; +import com.atomgraph.linkeddatahub.cli.command.imports.AddCSVImport; +import com.atomgraph.linkeddatahub.cli.command.imports.AddRDFImport; +import picocli.CommandLine.Command; + +/** + * Command group appending resources to a document. + * + * @author Martynas Jusevičius {@literal } + */ +@Command(name = "add", + description = "Appends resources to a document.", + subcommands = { AddView.class, AddConstruct.class, AddSelect.class, AddResultSetChart.class, AddFile.class, AddGenericService.class, + AddRDFImport.class, AddCSVImport.class, AddObjectBlock.class, AddXHTMLBlock.class }) +public class Add extends CommandGroup +{ +} diff --git a/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/AddConstruct.java b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/AddConstruct.java index 91194f7b21..ec84ddecf3 100644 --- a/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/AddConstruct.java +++ b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/AddConstruct.java @@ -39,7 +39,7 @@ * * @author Martynas Jusevičius {@literal } */ -@Command(name = "add-construct", description = "Adds a CONSTRUCT query to a document.") +@Command(name = "construct", description = "Adds a CONSTRUCT query to a document.") public class AddConstruct extends BaseCommand { diff --git a/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/AddFile.java b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/AddFile.java index 8114dec2ab..d141a417e8 100644 --- a/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/AddFile.java +++ b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/AddFile.java @@ -42,7 +42,7 @@ * * @author Martynas Jusevičius {@literal } */ -@Command(name = "add-file", description = "Uploads a file.") +@Command(name = "file", description = "Uploads a file.") public class AddFile extends BaseCommand { diff --git a/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/AddGenericService.java b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/AddGenericService.java index cb737c570a..554b41f0c5 100644 --- a/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/AddGenericService.java +++ b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/AddGenericService.java @@ -36,7 +36,7 @@ * * @author Martynas Jusevičius {@literal } */ -@Command(name = "add-generic-service", description = "Appends a generic SPARQL service to a document.") +@Command(name = "generic-service", description = "Appends a generic SPARQL service to a document.") public class AddGenericService extends BaseCommand { diff --git a/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/AddPackageImport.java b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/AddPackageImport.java new file mode 100644 index 0000000000..698c3652ca --- /dev/null +++ b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/AddPackageImport.java @@ -0,0 +1,56 @@ +/* + * Copyright 2026 Martynas Jusevičius . + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.atomgraph.linkeddatahub.cli.command; + +import com.atomgraph.linkeddatahub.cli.BaseCommand; +import com.atomgraph.linkeddatahub.cli.http.HttpException; +import com.atomgraph.linkeddatahub.cli.mixin.BaseMixin; +import com.atomgraph.linkeddatahub.cli.sparql.Updates; +import java.net.URI; +import picocli.CommandLine.Command; +import picocli.CommandLine.Mixin; +import picocli.CommandLine.Option; + +/** + * Imports a package into the application by declaring an ldh:import statement in its + * settings. The declaration is the installation: the package ontology joins the application's + * imports closure and its stylesheet is composed into the application stylesheet on the next + * request, with no restart. + * + * @author Martynas Jusevičius {@literal } + */ +@Command(name = "add", description = "Imports a package by declaring an ldh:import statement in the application settings.") +public class AddPackageImport extends BaseCommand +{ + + @Mixin + private BaseMixin baseMixin; + + @Option(names = "--package", required = true, paramLabel = "PACKAGE_URI", description = "URI of the imported package") + private URI packageURI; + + @Override + public Integer call() throws Exception + { + URI target = URI.create(baseMixin.require(getSpec()) + "settings"); + + HttpException.check(target, getClient().patch(target, Updates.insertPackageImport(packageURI))).close(); + + return 0; + } + +} diff --git a/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/AddResultSetChart.java b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/AddResultSetChart.java index 9b15553827..ee9ebeb9c7 100644 --- a/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/AddResultSetChart.java +++ b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/AddResultSetChart.java @@ -36,7 +36,7 @@ * * @author Martynas Jusevičius {@literal } */ -@Command(name = "add-result-set-chart", description = "Appends a result set chart to a document.") +@Command(name = "result-set-chart", description = "Appends a result set chart to a document.") public class AddResultSetChart extends BaseCommand { diff --git a/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/AddSelect.java b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/AddSelect.java index 63698b3420..cbcd857ca9 100644 --- a/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/AddSelect.java +++ b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/AddSelect.java @@ -32,7 +32,7 @@ * * @author Martynas Jusevičius {@literal } */ -@Command(name = "add-select", description = "Adds a SELECT query to a document.") +@Command(name = "select", description = "Adds a SELECT query to a document.") public class AddSelect extends BaseCommand { diff --git a/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/AddView.java b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/AddView.java index 345775f45f..ab21f69ce4 100644 --- a/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/AddView.java +++ b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/AddView.java @@ -37,7 +37,7 @@ * * @author Martynas Jusevičius {@literal } */ -@Command(name = "add-view", description = "Appends a view to a document.") +@Command(name = "view", description = "Appends a view to a document.") public class AddView extends BaseCommand { diff --git a/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/content/Content.java b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/Create.java similarity index 73% rename from cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/content/Content.java rename to cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/Create.java index 0458ead6c7..11d66ee627 100644 --- a/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/content/Content.java +++ b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/Create.java @@ -14,19 +14,19 @@ * limitations under the License. */ -package com.atomgraph.linkeddatahub.cli.command.content; +package com.atomgraph.linkeddatahub.cli.command; import com.atomgraph.linkeddatahub.cli.CommandGroup; import picocli.CommandLine.Command; /** - * Content block command group. + * Document creation command group. * * @author Martynas Jusevičius {@literal } */ -@Command(name = "content", - description = "Content block commands.", - subcommands = { AddObjectBlock.class, AddXHTMLBlock.class, RemoveBlock.class }) -public class Content extends CommandGroup +@Command(name = "create", + description = "Creates documents.", + subcommands = { CreateItem.class, CreateContainer.class }) +public class Create extends CommandGroup { } diff --git a/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/CreateContainer.java b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/CreateContainer.java index 5e3e327e1d..307fca8241 100644 --- a/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/CreateContainer.java +++ b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/CreateContainer.java @@ -39,7 +39,7 @@ * * @author Martynas Jusevičius {@literal } */ -@Command(name = "create-container", description = "Creates a container document.") +@Command(name = "container", description = "Creates a container document.") public class CreateContainer extends BaseCommand { diff --git a/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/CreateItem.java b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/CreateItem.java index 6f2e49b830..b7167444c2 100644 --- a/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/CreateItem.java +++ b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/CreateItem.java @@ -36,7 +36,7 @@ * * @author Martynas Jusevičius {@literal } */ -@Command(name = "create-item", description = "Creates an item document.") +@Command(name = "item", description = "Creates an item document.") public class CreateItem extends BaseCommand { diff --git a/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/Get.java b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/Get.java index 2244ad231e..71b1fd469c 100644 --- a/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/Get.java +++ b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/Get.java @@ -18,15 +18,22 @@ import com.atomgraph.linkeddatahub.cli.BaseCommand; import com.atomgraph.linkeddatahub.cli.http.HttpException; +import com.atomgraph.linkeddatahub.cli.util.Mementos; +import com.atomgraph.linkeddatahub.cli.util.URIRewriter; import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.MultivaluedHashMap; +import jakarta.ws.rs.core.MultivaluedMap; import jakarta.ws.rs.core.Response; import java.net.URI; +import picocli.CommandLine.ArgGroup; import picocli.CommandLine.Command; import picocli.CommandLine.Option; +import picocli.CommandLine.ParameterException; import picocli.CommandLine.Parameters; /** - * Retrieves an RDF description. Mirrors bin/get.sh. + * Retrieves an RDF description. Mirrors bin/get.sh, extended with the RFC 7089 Memento + * roles the document serves: a historical version, its version history and its TimeGate. * * @author Martynas Jusevičius {@literal } */ @@ -34,30 +41,94 @@ public class Get extends BaseCommand { - @Option(names = "--accept", required = true, paramLabel = "MEDIA_TYPE", description = "Requested media type (e.g. text/turtle)") + @Option(names = "--accept", paramLabel = "MEDIA_TYPE", description = "Requested media type (e.g. text/turtle). Required unless --timegate is given.") private String accept; @Option(names = "--head", description = "Requested headers only, no body (HEAD method)") private boolean head; + @ArgGroup(exclusive = true) + private Memento memento; + + @Option(names = "--datetime", paramLabel = "DATETIME", description = "Datetime the TimeGate negotiates on, RFC 1123 or ISO 8601 (requires --timegate)") + private String datetime; + @Parameters(paramLabel = "TARGET_URI", description = "URI of the document") private URI target; + /** The Memento role addressed instead of the document itself. The three are mutually exclusive. */ + static class Memento + { + + @Option(names = "--version", paramLabel = "SHA", required = true, description = "Commit SHA of the historical version to retrieve") + private String version; + + @Option(names = "--timemap", required = true, description = "Retrieve the version history (TimeMap) instead of the document") + private boolean timeMap; + + @Option(names = "--timegate", required = true, description = "Negotiate on datetime and print the URI of the selected version") + private boolean timeGate; + + } + @Override public Integer call() throws Exception { - MediaType[] acceptedTypes = { MediaType.valueOf(accept) }; + boolean timeGate = memento != null && memento.timeGate; + + if (datetime != null && !timeGate) throw new ParameterException(getSpec().commandLine(), "Option '--datetime' requires '--timegate'"); + // a TimeGate redirect has no representation of its own, so it is the one request that needs no media type + if (accept == null && !timeGate) throw new ParameterException(getSpec().commandLine(), "Missing required option: '--accept=MEDIA_TYPE'"); + + URI uri = requestURI(); + MediaType[] acceptedTypes = accept != null ? new MediaType[] { MediaType.valueOf(accept) } : new MediaType[0]; + + MultivaluedMap headers = new MultivaluedHashMap<>(); + if (datetime != null) headers.putSingle(Mementos.ACCEPT_DATETIME_HEADER, Mementos.acceptDatetime(datetime)); if (head) - try (Response response = HttpException.check(target, getClient().head(target, acceptedTypes))) + try (Response response = HttpException.check(uri, getClient().head(uri, acceptedTypes, headers))) { print("HTTP " + response.getStatus() + " " + response.getStatusInfo().getReasonPhrase()); response.getStringHeaders().forEach((name, values) -> values.forEach(value -> print(name + ": " + value))); } + else if (timeGate) + try (Response response = HttpException.check(uri, getClient().get(uri, acceptedTypes, headers))) + { + print(mementoURI(response)); + } else - printBody(HttpException.check(target, getClient().get(target, acceptedTypes))); + printBody(HttpException.check(uri, getClient().get(uri, acceptedTypes, headers))); return 0; } + /** + * Returns the URI of the Memento a TimeGate redirected to. + * + * @param response TimeGate response + * @return Memento URI, with the logical origin restored when the request went through a proxy + */ + private URI mementoURI(Response response) + { + URI location = response.getLocation(); + if (location == null) throw new IllegalStateException("TimeGate <" + requestURI() + "> did not redirect to a version of <" + target + ">"); + + return getEffectiveProxy() != null ? URIRewriter.rewrite(location, target) : location; + } + + /** + * Returns the URI requested: the target document, or one of the Memento roles it serves. + * + * @return request URI + */ + private URI requestURI() + { + if (memento == null) return target; + if (memento.version != null) return Mementos.version(target, memento.version); + if (memento.timeMap) return Mementos.timeMap(target); + + return Mementos.timeGate(target); + } + } diff --git a/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/ListPackages.java b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/ListPackages.java new file mode 100644 index 0000000000..8823f85499 --- /dev/null +++ b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/ListPackages.java @@ -0,0 +1,144 @@ +/* + * Copyright 2026 Martynas Jusevičius . + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.atomgraph.linkeddatahub.cli.command; + +import com.atomgraph.linkeddatahub.cli.BaseCommand; +import com.atomgraph.linkeddatahub.cli.http.HttpException; +import com.atomgraph.linkeddatahub.cli.mixin.BaseMixin; +import com.atomgraph.linkeddatahub.cli.vocab.LDH; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import java.net.URI; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.Comparator; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +import org.apache.jena.rdf.model.Model; +import org.apache.jena.rdf.model.RDFNode; +import org.apache.jena.rdf.model.Resource; +import org.apache.jena.vocabulary.DCTerms; +import org.apache.jena.vocabulary.RDFS; +import picocli.CommandLine.Command; +import picocli.CommandLine.Mixin; +import picocli.CommandLine.Option; + +/** + * Lists the packages the registry offers, marking the ones the application imports. Registry URIs + * are not the application's own, so the catalog is read through the Linked Data proxy - the same + * way the application settings modal reads it. + * + * Each package is one tab-separated line of state, URI and title, so the listing greps and cuts: + * ldh packages list | grep ^available | cut -f2. + * + * @author Martynas Jusevičius {@literal } + */ +@Command(name = "list", description = "Lists the packages available in the registry, marking the ones the application imports.") +public class ListPackages extends BaseCommand +{ + + /** Accepted response media type of the proxied catalog */ + private static final MediaType[] ACCEPT_RDF_XML = { com.atomgraph.core.MediaType.APPLICATION_RDF_XML_TYPE }; + + @Mixin + private BaseMixin baseMixin; + + @Option(names = "--registry", defaultValue = "https://packages.linkeddatahub.com/", paramLabel = "REGISTRY_URI", + description = "URI of the package registry (default: ${DEFAULT-VALUE})") + private URI registry; + + @Override + public Integer call() throws Exception + { + URI base = baseMixin.require(getSpec()); + Set imported = getImportedPackages(URI.create(base + "settings")); + + for (Resource pkg : getPackages(getCatalog(base))) + print((imported.contains(pkg.getURI()) ? "installed" : "available") + "\t" + pkg.getURI() + "\t" + getTitle(pkg)); + + return 0; + } + + /** + * Returns the URIs of the packages the application imports, read from its settings document. + * + * @param settings settings document URI + * @return imported package URIs + */ + protected Set getImportedPackages(URI settings) + { + try (Response response = HttpException.check(settings, getClient().get(settings, ACCEPT_TURTLE))) + { + return response.readEntity(Model.class). + listObjectsOfProperty(LDH.importPackage). + toList().stream(). + filter(RDFNode::isURIResource). + map(node -> node.asResource().getURI()). + collect(Collectors.toSet()); + } + } + + /** + * Fetches the registry catalog through the Linked Data proxy. + * + * @param base application base URI + * @return catalog model + */ + protected Model getCatalog(URI base) + { + URI target = URI.create(base + "?uri=" + URLEncoder.encode(registry.toString(), StandardCharsets.UTF_8)); + + try (Response response = HttpException.check(target, getClient().get(target, ACCEPT_RDF_XML))) + { + return response.readEntity(Model.class); + } + } + + /** + * Returns the packages the catalog lists, ordered by URI so the listing is stable. + * + * @param catalog catalog model + * @return package resources + */ + protected static List getPackages(Model catalog) + { + return catalog.listObjectsOfProperty(RDFS.member). + toList().stream(). + filter(RDFNode::isURIResource). + map(RDFNode::asResource). + distinct(). + sorted(Comparator.comparing(Resource::getURI)). + toList(); + } + + /** + * Returns the title of a package, falling back to its label and then to the empty string: + * the catalog describes its members loosely enough that neither is guaranteed. + * + * @param pkg package resource + * @return title + */ + protected static String getTitle(Resource pkg) + { + if (pkg.hasProperty(DCTerms.title)) return pkg.getProperty(DCTerms.title).getString(); + if (pkg.hasProperty(RDFS.label)) return pkg.getProperty(RDFS.label).getString(); + + return ""; + } + +} diff --git a/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/admin/acl/Acl.java b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/Packages.java similarity index 71% rename from cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/admin/acl/Acl.java rename to cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/Packages.java index 395f6f03eb..5914c0a7c0 100644 --- a/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/admin/acl/Acl.java +++ b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/Packages.java @@ -14,19 +14,19 @@ * limitations under the License. */ -package com.atomgraph.linkeddatahub.cli.command.admin.acl; +package com.atomgraph.linkeddatahub.cli.command; import com.atomgraph.linkeddatahub.cli.CommandGroup; import picocli.CommandLine.Command; /** - * Access control command group. + * Package management command group. * * @author Martynas Jusevičius {@literal } */ -@Command(name = "acl", - description = "Access control commands.", - subcommands = { CreateGroup.class, CreateAuthorization.class, AddAgentToGroup.class, MakePublic.class }) -public class Acl extends CommandGroup +@Command(name = "packages", + description = "Manages the application's package imports.", + subcommands = { ListPackages.class, AddPackageImport.class, RemovePackageImport.class }) +public class Packages extends CommandGroup { } diff --git a/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/admin/ontologies/Ontologies.java b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/Remove.java similarity index 66% rename from cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/admin/ontologies/Ontologies.java rename to cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/Remove.java index 75386ceb4e..fabba37796 100644 --- a/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/admin/ontologies/Ontologies.java +++ b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/Remove.java @@ -14,20 +14,20 @@ * limitations under the License. */ -package com.atomgraph.linkeddatahub.cli.command.admin.ontologies; +package com.atomgraph.linkeddatahub.cli.command; import com.atomgraph.linkeddatahub.cli.CommandGroup; +import com.atomgraph.linkeddatahub.cli.command.content.RemoveBlock; import picocli.CommandLine.Command; /** - * Ontology management command group. + * Command group removing resources from a document. * * @author Martynas Jusevičius {@literal } */ -@Command(name = "ontologies", - description = "Ontology management commands.", - subcommands = { CreateOntology.class, ImportOntology.class, AddClass.class, AddConstructor.class, - AddSelect.class, AddPropertyConstraint.class, AddRestriction.class }) -public class Ontologies extends CommandGroup +@Command(name = "remove", + description = "Removes resources from a document.", + subcommands = { RemoveBlock.class }) +public class Remove extends CommandGroup { } diff --git a/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/RemovePackageImport.java b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/RemovePackageImport.java new file mode 100644 index 0000000000..191f06b91b --- /dev/null +++ b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/RemovePackageImport.java @@ -0,0 +1,55 @@ +/* + * Copyright 2026 Martynas Jusevičius . + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.atomgraph.linkeddatahub.cli.command; + +import com.atomgraph.linkeddatahub.cli.BaseCommand; +import com.atomgraph.linkeddatahub.cli.http.HttpException; +import com.atomgraph.linkeddatahub.cli.mixin.BaseMixin; +import com.atomgraph.linkeddatahub.cli.sparql.Updates; +import java.net.URI; +import picocli.CommandLine.Command; +import picocli.CommandLine.Mixin; +import picocli.CommandLine.Option; + +/** + * Removes a package import from the application by deleting its ldh:import statement + * from the settings. The application drops the package ontology and stylesheet on the next request, + * with no restart. Removing an import the application does not have is a no-op. + * + * @author Martynas Jusevičius {@literal } + */ +@Command(name = "remove", description = "Removes a package import by deleting its ldh:import statement from the application settings.") +public class RemovePackageImport extends BaseCommand +{ + + @Mixin + private BaseMixin baseMixin; + + @Option(names = "--package", required = true, paramLabel = "PACKAGE_URI", description = "URI of the package whose import is removed") + private URI packageURI; + + @Override + public Integer call() throws Exception + { + URI target = URI.create(baseMixin.require(getSpec()) + "settings"); + + HttpException.check(target, getClient().patch(target, Updates.deletePackageImport(packageURI))).close(); + + return 0; + } + +} diff --git a/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/admin/Add.java b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/admin/Add.java new file mode 100644 index 0000000000..d5285bf7d1 --- /dev/null +++ b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/admin/Add.java @@ -0,0 +1,39 @@ +/* + * Copyright 2026 Martynas Jusevičius . + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.atomgraph.linkeddatahub.cli.command.admin; + +import com.atomgraph.linkeddatahub.cli.CommandGroup; +import com.atomgraph.linkeddatahub.cli.command.admin.acl.AddAgentToGroup; +import com.atomgraph.linkeddatahub.cli.command.admin.ontologies.AddClass; +import com.atomgraph.linkeddatahub.cli.command.admin.ontologies.AddConstructor; +import com.atomgraph.linkeddatahub.cli.command.admin.ontologies.AddPropertyConstraint; +import com.atomgraph.linkeddatahub.cli.command.admin.ontologies.AddRestriction; +import com.atomgraph.linkeddatahub.cli.command.admin.ontologies.AddSelect; +import picocli.CommandLine.Command; + +/** + * Command group appending resources to admin documents. + * + * @author Martynas Jusevičius {@literal } + */ +@Command(name = "add", + description = "Appends resources to admin documents.", + subcommands = { AddClass.class, AddConstructor.class, AddSelect.class, AddPropertyConstraint.class, AddRestriction.class, + AddOntologyImport.class, AddAgentToGroup.class }) +public class Add extends CommandGroup +{ +} diff --git a/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/admin/AddOntologyImport.java b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/admin/AddOntologyImport.java index 5a3ffc1498..f54d246ec4 100644 --- a/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/admin/AddOntologyImport.java +++ b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/admin/AddOntologyImport.java @@ -29,7 +29,7 @@ * * @author Martynas Jusevičius {@literal } */ -@Command(name = "add-ontology-import", description = "Adds an owl:imports statement to an ontology.") +@Command(name = "ontology-import", description = "Adds an owl:imports statement to an ontology.") public class AddOntologyImport extends BaseCommand { diff --git a/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/admin/Admin.java b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/admin/Admin.java index 40a0540aad..f1c361b6fa 100644 --- a/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/admin/Admin.java +++ b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/admin/Admin.java @@ -17,8 +17,7 @@ package com.atomgraph.linkeddatahub.cli.command.admin; import com.atomgraph.linkeddatahub.cli.CommandGroup; -import com.atomgraph.linkeddatahub.cli.command.admin.acl.Acl; -import com.atomgraph.linkeddatahub.cli.command.admin.ontologies.Ontologies; +import com.atomgraph.linkeddatahub.cli.command.admin.acl.MakePublic; import picocli.CommandLine.Command; /** @@ -28,7 +27,7 @@ */ @Command(name = "admin", description = "Administrative commands.", - subcommands = { Ontologies.class, Acl.class, ClearOntology.class, AddOntologyImport.class }) + subcommands = { Create.class, Add.class, Clear.class, Import.class, MakePublic.class }) public class Admin extends CommandGroup { } diff --git a/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/imports/Imports.java b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/admin/Clear.java similarity index 73% rename from cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/imports/Imports.java rename to cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/admin/Clear.java index 6900f3e87b..87e3e776b3 100644 --- a/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/imports/Imports.java +++ b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/admin/Clear.java @@ -14,19 +14,19 @@ * limitations under the License. */ -package com.atomgraph.linkeddatahub.cli.command.imports; +package com.atomgraph.linkeddatahub.cli.command.admin; import com.atomgraph.linkeddatahub.cli.CommandGroup; import picocli.CommandLine.Command; /** - * Data import command group. + * Command group clearing cached server-side state. * * @author Martynas Jusevičius {@literal } */ -@Command(name = "imports", - description = "Data import commands.", - subcommands = { AddCSVImport.class, AddRDFImport.class, ImportCSV.class, ImportRDF.class }) -public class Imports extends CommandGroup +@Command(name = "clear", + description = "Clears cached server-side state.", + subcommands = { ClearOntology.class }) +public class Clear extends CommandGroup { } diff --git a/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/admin/ClearOntology.java b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/admin/ClearOntology.java index 642451bb3e..d93659c340 100644 --- a/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/admin/ClearOntology.java +++ b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/admin/ClearOntology.java @@ -31,7 +31,7 @@ * * @author Martynas Jusevičius {@literal } */ -@Command(name = "clear-ontology", description = "Clears an ontology from memory and reloads it.") +@Command(name = "ontology", description = "Clears an ontology from memory and reloads it.") public class ClearOntology extends BaseCommand { diff --git a/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/admin/Create.java b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/admin/Create.java new file mode 100644 index 0000000000..6a89c7957e --- /dev/null +++ b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/admin/Create.java @@ -0,0 +1,35 @@ +/* + * Copyright 2026 Martynas Jusevičius . + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.atomgraph.linkeddatahub.cli.command.admin; + +import com.atomgraph.linkeddatahub.cli.CommandGroup; +import com.atomgraph.linkeddatahub.cli.command.admin.acl.CreateAuthorization; +import com.atomgraph.linkeddatahub.cli.command.admin.acl.CreateGroup; +import com.atomgraph.linkeddatahub.cli.command.admin.ontologies.CreateOntology; +import picocli.CommandLine.Command; + +/** + * Admin document creation command group. + * + * @author Martynas Jusevičius {@literal } + */ +@Command(name = "create", + description = "Creates documents in the admin application.", + subcommands = { CreateOntology.class, CreateGroup.class, CreateAuthorization.class }) +public class Create extends CommandGroup +{ +} diff --git a/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/admin/Import.java b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/admin/Import.java new file mode 100644 index 0000000000..ba57741185 --- /dev/null +++ b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/admin/Import.java @@ -0,0 +1,33 @@ +/* + * Copyright 2026 Martynas Jusevičius . + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.atomgraph.linkeddatahub.cli.command.admin; + +import com.atomgraph.linkeddatahub.cli.CommandGroup; +import com.atomgraph.linkeddatahub.cli.command.admin.ontologies.ImportOntology; +import picocli.CommandLine.Command; + +/** + * Ontology import workflow group. + * + * @author Martynas Jusevičius {@literal } + */ +@Command(name = "import", + description = "Ontology import workflows.", + subcommands = { ImportOntology.class }) +public class Import extends CommandGroup +{ +} diff --git a/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/admin/acl/AddAgentToGroup.java b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/admin/acl/AddAgentToGroup.java index 73abb82fff..a2a59dce76 100644 --- a/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/admin/acl/AddAgentToGroup.java +++ b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/admin/acl/AddAgentToGroup.java @@ -29,7 +29,7 @@ * * @author Martynas Jusevičius {@literal } */ -@Command(name = "add-agent-to-group", description = "Adds an agent to a group.") +@Command(name = "agent", description = "Adds an agent to a group.") public class AddAgentToGroup extends BaseCommand { diff --git a/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/admin/acl/CreateAuthorization.java b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/admin/acl/CreateAuthorization.java index 2c43b8ba46..8fbc01c6df 100644 --- a/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/admin/acl/CreateAuthorization.java +++ b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/admin/acl/CreateAuthorization.java @@ -43,7 +43,7 @@ * * @author Martynas Jusevičius {@literal } */ -@Command(name = "create-authorization", description = "Creates an ACL authorization.") +@Command(name = "authorization", description = "Creates an ACL authorization.") public class CreateAuthorization extends BaseCommand { diff --git a/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/admin/acl/CreateGroup.java b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/admin/acl/CreateGroup.java index 05ec6847e7..ca35618fb6 100644 --- a/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/admin/acl/CreateGroup.java +++ b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/admin/acl/CreateGroup.java @@ -38,7 +38,7 @@ * * @author Martynas Jusevičius {@literal } */ -@Command(name = "create-group", description = "Creates an agent group.") +@Command(name = "group", description = "Creates an agent group.") public class CreateGroup extends BaseCommand { diff --git a/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/admin/ontologies/AddClass.java b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/admin/ontologies/AddClass.java index 64c097ce2a..8303309576 100644 --- a/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/admin/ontologies/AddClass.java +++ b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/admin/ontologies/AddClass.java @@ -38,7 +38,7 @@ * * @author Martynas Jusevičius {@literal } */ -@Command(name = "add-class", description = "Adds a class to an ontology.") +@Command(name = "class", description = "Adds a class to an ontology.") public class AddClass extends BaseCommand { diff --git a/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/admin/ontologies/AddConstructor.java b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/admin/ontologies/AddConstructor.java index d5f222b642..ec1d1eb602 100644 --- a/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/admin/ontologies/AddConstructor.java +++ b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/admin/ontologies/AddConstructor.java @@ -38,7 +38,7 @@ * * @author Martynas Jusevičius {@literal } */ -@Command(name = "add-constructor", description = "Adds a CONSTRUCT query to an ontology.") +@Command(name = "constructor", description = "Adds a CONSTRUCT query to an ontology.") public class AddConstructor extends BaseCommand { diff --git a/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/admin/ontologies/AddPropertyConstraint.java b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/admin/ontologies/AddPropertyConstraint.java index 74ff2db865..eafc655b20 100644 --- a/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/admin/ontologies/AddPropertyConstraint.java +++ b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/admin/ontologies/AddPropertyConstraint.java @@ -36,7 +36,7 @@ * * @author Martynas Jusevičius {@literal } */ -@Command(name = "add-property-constraint", description = "Adds a constraint that makes a property required.") +@Command(name = "property-constraint", description = "Adds a constraint that makes a property required.") public class AddPropertyConstraint extends BaseCommand { diff --git a/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/admin/ontologies/AddRestriction.java b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/admin/ontologies/AddRestriction.java index c2b6ee6cea..9ac8ae79c6 100644 --- a/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/admin/ontologies/AddRestriction.java +++ b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/admin/ontologies/AddRestriction.java @@ -35,7 +35,7 @@ * * @author Martynas Jusevičius {@literal } */ -@Command(name = "add-restriction", description = "Adds an OWL restriction to an ontology.") +@Command(name = "restriction", description = "Adds an OWL restriction to an ontology.") public class AddRestriction extends BaseCommand { diff --git a/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/admin/ontologies/AddSelect.java b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/admin/ontologies/AddSelect.java index 0938ffc9e4..323e9ab34e 100644 --- a/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/admin/ontologies/AddSelect.java +++ b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/admin/ontologies/AddSelect.java @@ -32,7 +32,7 @@ * * @author Martynas Jusevičius {@literal } */ -@Command(name = "add-select", description = "Adds a SELECT query to an ontology.") +@Command(name = "select", description = "Adds a SELECT query to an ontology.") public class AddSelect extends BaseCommand { diff --git a/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/admin/ontologies/CreateOntology.java b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/admin/ontologies/CreateOntology.java index 96cface9ff..9b4312e759 100644 --- a/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/admin/ontologies/CreateOntology.java +++ b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/admin/ontologies/CreateOntology.java @@ -39,7 +39,7 @@ * * @author Martynas Jusevičius {@literal } */ -@Command(name = "create-ontology", description = "Creates a new ontology.") +@Command(name = "ontology", description = "Creates a new ontology.") public class CreateOntology extends BaseCommand { diff --git a/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/admin/ontologies/ImportOntology.java b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/admin/ontologies/ImportOntology.java index 94de67f7e7..fb85f6c652 100644 --- a/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/admin/ontologies/ImportOntology.java +++ b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/admin/ontologies/ImportOntology.java @@ -52,7 +52,7 @@ * * @author Martynas Jusevičius {@literal } */ -@Command(name = "import-ontology", description = "Derives class constructors from an external ontology and appends them, with an owl:imports of the source, to a document.") +@Command(name = "ontology", description = "Derives class constructors from an external ontology and appends them, with an owl:imports of the source, to a document.") public class ImportOntology extends BaseCommand { diff --git a/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/content/AddObjectBlock.java b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/content/AddObjectBlock.java index b9860d23dc..5345548611 100644 --- a/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/content/AddObjectBlock.java +++ b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/content/AddObjectBlock.java @@ -41,7 +41,7 @@ * * @author Martynas Jusevičius {@literal } */ -@Command(name = "add-object-block", description = "Appends an object content block to a document.") +@Command(name = "object-block", description = "Appends an object content block to a document.") public class AddObjectBlock extends BaseCommand { diff --git a/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/content/AddXHTMLBlock.java b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/content/AddXHTMLBlock.java index 7995fd0c2b..4237b7a985 100644 --- a/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/content/AddXHTMLBlock.java +++ b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/content/AddXHTMLBlock.java @@ -40,7 +40,7 @@ * * @author Martynas Jusevičius {@literal } */ -@Command(name = "add-xhtml-block", description = "Appends an XHTML content block to a document.") +@Command(name = "xhtml-block", description = "Appends an XHTML content block to a document.") public class AddXHTMLBlock extends BaseCommand { diff --git a/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/content/RemoveBlock.java b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/content/RemoveBlock.java index dd6bfee7cd..ee61864de4 100644 --- a/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/content/RemoveBlock.java +++ b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/content/RemoveBlock.java @@ -29,7 +29,7 @@ * * @author Martynas Jusevičius {@literal } */ -@Command(name = "remove-block", description = "Removes a content block from a document.") +@Command(name = "block", description = "Removes a content block from a document.") public class RemoveBlock extends BaseCommand { diff --git a/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/imports/AddCSVImport.java b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/imports/AddCSVImport.java index 0decc0d175..6a60f63e06 100644 --- a/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/imports/AddCSVImport.java +++ b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/imports/AddCSVImport.java @@ -37,7 +37,7 @@ * * @author Martynas Jusevičius {@literal } */ -@Command(name = "add-csv-import", description = "Adds CSV import metadata to a document.") +@Command(name = "csv-import", description = "Adds CSV import metadata to a document.") public class AddCSVImport extends BaseCommand { diff --git a/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/imports/AddRDFImport.java b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/imports/AddRDFImport.java index 4b91effcf6..f7d825da28 100644 --- a/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/imports/AddRDFImport.java +++ b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/imports/AddRDFImport.java @@ -38,7 +38,7 @@ * * @author Martynas Jusevičius {@literal } */ -@Command(name = "add-rdf-import", description = "Adds RDF import metadata to a document.") +@Command(name = "rdf-import", description = "Adds RDF import metadata to a document.") public class AddRDFImport extends BaseCommand { diff --git a/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/imports/Import.java b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/imports/Import.java new file mode 100644 index 0000000000..4c41b3d6f1 --- /dev/null +++ b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/imports/Import.java @@ -0,0 +1,33 @@ +/* + * Copyright 2026 Martynas Jusevičius . + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.atomgraph.linkeddatahub.cli.command.imports; + +import com.atomgraph.linkeddatahub.cli.CommandGroup; +import picocli.CommandLine.Command; + +/** + * Data import workflow group. Each subcommand composes the atomic commands that upload the file, + * add the transformation query and post the import metadata. + * + * @author Martynas Jusevičius {@literal } + */ +@Command(name = "import", + description = "Data import workflows.", + subcommands = { ImportRDF.class, ImportCSV.class }) +public class Import extends CommandGroup +{ +} diff --git a/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/imports/ImportCSV.java b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/imports/ImportCSV.java index ea45095184..b40415c2ff 100644 --- a/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/imports/ImportCSV.java +++ b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/imports/ImportCSV.java @@ -36,7 +36,7 @@ * * @author Martynas Jusevičius {@literal } */ -@Command(name = "import-csv", description = "Imports CSV data using a transformation query.") +@Command(name = "csv", description = "Imports CSV data using a transformation query. Workflow composing add construct, add file and add csv-import.") public class ImportCSV extends BaseCommand { diff --git a/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/imports/ImportRDF.java b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/imports/ImportRDF.java index db3cbed84e..08088f41f8 100644 --- a/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/imports/ImportRDF.java +++ b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/imports/ImportRDF.java @@ -36,7 +36,7 @@ * * @author Martynas Jusevičius {@literal } */ -@Command(name = "import-rdf", description = "Imports RDF data, optionally using a transformation query.") +@Command(name = "rdf", description = "Imports RDF data, optionally using a transformation query. Workflow composing add construct, add file and add rdf-import.") public class ImportRDF extends BaseCommand { diff --git a/cli/src/main/java/com/atomgraph/linkeddatahub/cli/sparql/Updates.java b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/sparql/Updates.java index cb621e377b..9566edf7bf 100644 --- a/cli/src/main/java/com/atomgraph/linkeddatahub/cli/sparql/Updates.java +++ b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/sparql/Updates.java @@ -55,6 +55,51 @@ public static String insertOntologyImport(URI ontologyDoc, URI importURI) return pss.toString(); } + /** + * Declares a package import on the application described by its settings document. The + * application is matched by type rather than named, because its URI is an opaque + * urn: identifier the caller has no way of knowing. + * + * @param packageURI imported package URI + * @return SPARQL update string + */ + public static String insertPackageImport(URI packageURI) + { + ParameterizedSparqlString pss = new ParameterizedSparqlString(""" + PREFIX ldh: + PREFIX lapp: + INSERT { + ?app ldh:import ?package . + } + WHERE { + ?app a lapp:Application . + } + """); + pss.setIri("package", packageURI.toString()); + return pss.toString(); + } + + /** + * Removes a package import declaration from the application described by its settings document. + * + * @param packageURI imported package URI + * @return SPARQL update string + */ + public static String deletePackageImport(URI packageURI) + { + ParameterizedSparqlString pss = new ParameterizedSparqlString(""" + PREFIX ldh: + DELETE { + ?app ldh:import ?package . + } + WHERE { + ?app ldh:import ?package . + } + """); + pss.setIri("package", packageURI.toString()); + return pss.toString(); + } + /** * Adds a foaf:member statement to the group that is the primary topic * of the given document. diff --git a/cli/src/main/java/com/atomgraph/linkeddatahub/cli/util/Mementos.java b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/util/Mementos.java new file mode 100644 index 0000000000..23d6747997 --- /dev/null +++ b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/util/Mementos.java @@ -0,0 +1,144 @@ +/* + * Copyright 2026 Martynas Jusevičius . + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.atomgraph.linkeddatahub.cli.util; + +import java.net.URI; +import java.time.Instant; +import java.time.OffsetDateTime; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.time.DateTimeException; +import java.util.Locale; + +/** + * Addressing of the RFC 7089 Memento roles a LinkedDataHub document serves: a historical version + * (Memento), its version history (TimeMap) and its datetime negotiation endpoint (TimeGate). + * Each is a query parameter on the document URI itself. + * + * @author Martynas Jusevičius {@literal } + */ +public final class Mementos +{ + + /** Name of the query parameter that selects a historical version by commit SHA */ + public static final String VERSION_PARAM_NAME = "version"; + + /** Name of the query parameter that addresses the version history */ + public static final String TIMEMAP_PARAM_NAME = "timemap"; + + /** Name of the query parameter that addresses the TimeGate */ + public static final String TIMEGATE_PARAM_NAME = "timegate"; + + /** Name of the request header conveying the datetime a TimeGate negotiates on */ + public static final String ACCEPT_DATETIME_HEADER = "Accept-Datetime"; + + /** + * RFC 1123 datetime in GMT with a zero-padded day of month, which the RFC 7089 grammar requires + * where {@link DateTimeFormatter#RFC_1123_DATE_TIME} leaves it unpadded. The same format the + * server writes Memento-Datetime and TimeMap datetimes in. + */ + public static final DateTimeFormatter RFC_1123_GMT = DateTimeFormatter.ofPattern("EEE, dd MMM yyyy HH:mm:ss 'GMT'", Locale.US). + withZone(ZoneId.of("GMT")); + + private Mementos() { } + + /** + * Returns the URI of a document's historical version. + * + * @param target document URI + * @param sha commit SHA + * @return Memento URI + */ + public static URI version(URI target, String sha) + { + return queryParam(target, VERSION_PARAM_NAME + "=" + sha); + } + + /** + * Returns the URI of a document's version history. + * + * @param target document URI + * @return TimeMap URI + */ + public static URI timeMap(URI target) + { + return queryParam(target, TIMEMAP_PARAM_NAME); + } + + /** + * Returns the URI of a document's TimeGate. + * + * @param target document URI + * @return TimeGate URI + */ + public static URI timeGate(URI target) + { + return queryParam(target, TIMEGATE_PARAM_NAME); + } + + /** + * Appends a query parameter to a URI, before its fragment. Valueless parameters keep the bare + * form the server advertises them in, e.g. ?timemap rather than ?timemap=. + * + * @param uri URI to append to + * @param param parameter, either a bare name or name=value + * @return URI with the parameter appended + */ + private static URI queryParam(URI uri, String param) + { + String string = uri.toString(); + int hash = string.indexOf('#'); + String fragment = hash == -1 ? "" : string.substring(hash); + + return URI.create((hash == -1 ? string : string.substring(0, hash)) + + (uri.getRawQuery() == null ? "?" : "&") + param + fragment); + } + + /** + * Formats a datetime as the Accept-Datetime header value. Accepts RFC 1123 + * (Wed, 20 Aug 2026 10:00:00 GMT) as well as ISO 8601 + * (2026-08-20T10:00:00Z), so that timestamps read out of a TimeMap, where they are + * xsd:dateTime, can be handed back without conversion. + * + * @param datetime datetime in either format + * @return RFC 1123 datetime in GMT + */ + public static String acceptDatetime(String datetime) + { + return RFC_1123_GMT.format(parse(datetime)); + } + + private static Instant parse(String datetime) + { + try + { + return Instant.from(DateTimeFormatter.RFC_1123_DATE_TIME.parse(datetime)); + } + catch (DateTimeException ex) + { + try + { + return OffsetDateTime.parse(datetime).toInstant(); + } + catch (DateTimeException ex2) + { + throw new IllegalArgumentException("Value '" + datetime + "' is neither an RFC 1123 nor an ISO 8601 datetime"); + } + } + } + +} diff --git a/cli/src/main/java/com/atomgraph/linkeddatahub/cli/vocab/LDH.java b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/vocab/LDH.java index d256327514..397f57f70b 100644 --- a/cli/src/main/java/com/atomgraph/linkeddatahub/cli/vocab/LDH.java +++ b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/vocab/LDH.java @@ -52,6 +52,8 @@ private LDH() { } /** ldh:SelectChildren query resource */ public static final Resource SelectChildren = ResourceFactory.createResource(NS + "SelectChildren"); + /** ldh:import property */ + public static final Property importPackage = ResourceFactory.createProperty(NS + "import"); /** ldh:service property */ public static final Property service = ResourceFactory.createProperty(NS + "service"); /** ldh:file property */ diff --git a/cli/src/test/java/com/atomgraph/linkeddatahub/cli/CommandOutputTest.java b/cli/src/test/java/com/atomgraph/linkeddatahub/cli/CommandOutputTest.java index 196fe31979..a98afd86ea 100644 --- a/cli/src/test/java/com/atomgraph/linkeddatahub/cli/CommandOutputTest.java +++ b/cli/src/test/java/com/atomgraph/linkeddatahub/cli/CommandOutputTest.java @@ -25,6 +25,7 @@ import java.nio.charset.StandardCharsets; import java.nio.file.Path; import java.nio.file.Paths; +import java.util.List; import org.junit.jupiter.api.Test; import picocli.CommandLine; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -36,7 +37,7 @@ * document prints its URL as the only line on stdout, diagnostics go to stderr, and an HTTP error * status leaves stdout empty and exits 1. * - * item=$(ldh create-item ...) breaks the moment anything else reaches stdout, and the + * item=$(ldh create item ...) breaks the moment anything else reaches stdout, and the * http-tests consume that substitution in dozens of places. */ public class CommandOutputTest @@ -65,7 +66,7 @@ public void createdURLIsTheOnlyLineOnStdout() throws Exception URI base = server.baseURI(); StringWriter out = new StringWriter(), err = new StringWriter(); - int code = commandLine(out, err).execute("create-container", + int code = commandLine(out, err).execute("create", "container", "-f", keyStorePath().toString(), "-p", "changeit", "-b", base.toString(), "--title", "Test", "--slug", "test", "--parent", base.toString()); @@ -85,7 +86,7 @@ public void createdURLMatchesTheDocumentActuallyWritten() throws Exception URI base = server.baseURI(); StringWriter out = new StringWriter(), err = new StringWriter(); - commandLine(out, err).execute("create-container", + commandLine(out, err).execute("create", "container", "-f", keyStorePath().toString(), "-p", "changeit", "-b", base.toString(), "--title", "Test", "--slug", "test", "--parent", base.toString()); @@ -104,7 +105,7 @@ public void slugIsPercentEncodedInBothURLAndRequest() throws Exception URI base = server.baseURI(); StringWriter out = new StringWriter(), err = new StringWriter(); - int code = commandLine(out, err).execute("create-container", + int code = commandLine(out, err).execute("create", "container", "-f", keyStorePath().toString(), "-p", "changeit", "-b", base.toString(), "--title", "Ö", "--slug", "ö x", "--parent", base.toString()); @@ -202,7 +203,7 @@ public void httpErrorStatusExitsOneWithEmptyStdout() throws Exception URI base = server.baseURI(); StringWriter out = new StringWriter(), err = new StringWriter(); - int code = commandLine(out, err).execute("create-container", + int code = commandLine(out, err).execute("create", "container", "-f", keyStorePath().toString(), "-p", "changeit", "-b", base.toString(), "--title", "Test", "--slug", "test", "--parent", base.toString()); @@ -223,7 +224,7 @@ public void connectionFailureExitsOneWithEmptyStdout() throws Exception StringWriter out = new StringWriter(), err = new StringWriter(); - int code = commandLine(out, err).execute("create-container", + int code = commandLine(out, err).execute("create", "container", "-f", keyStorePath().toString(), "-p", "changeit", "-b", base.toString(), "--title", "Test", "--slug", "test", "--parent", base.toString()); @@ -232,4 +233,141 @@ public void connectionFailureExitsOneWithEmptyStdout() throws Exception assertTrue(err.toString().contains("Connection refused"), err.toString()); } + @Test + public void timeGatePrintsTheMementoURIAsTheOnlyLineOnStdout() throws Exception + { + try (StubServer server = new StubServer()) + { + URI base = server.baseURI(); + server.responds(302, "").respondsWithHeader("Location", base + "some/?version=a1b2c3"); + StringWriter out = new StringWriter(), err = new StringWriter(); + + int code = commandLine(out, err).execute("get", + "-f", keyStorePath().toString(), "-p", "changeit", + "--timegate", "--datetime", "2026-08-20T10:00:00Z", base.resolve("some/").toString()); + + assertEquals(0, code); + assertEquals(base + "some/?version=a1b2c3", out.toString().strip()); + assertEquals(1, out.toString().strip().lines().count(), "stdout carries more than the Memento URI"); + assertEquals("", err.toString(), "stderr is not empty on success"); + assertEquals("/some/?timegate", server.getLastTarget()); + assertEquals("Thu, 20 Aug 2026 10:00:00 GMT", server.getLastHeader("Accept-Datetime")); + } + } + + @Test + public void aBareTimeGateNegotiatesWithoutADatetime() throws Exception + { + try (StubServer server = new StubServer()) + { + URI base = server.baseURI(); + server.responds(302, "").respondsWithHeader("Location", base + "some/?version=a1b2c3"); + StringWriter out = new StringWriter(), err = new StringWriter(); + + int code = commandLine(out, err).execute("get", + "-f", keyStorePath().toString(), "-p", "changeit", + "--timegate", base.resolve("some/").toString()); + + assertEquals(0, code); + assertNull(server.getLastHeader("Accept-Datetime"), "a bare --timegate must not send Accept-Datetime"); + } + } + + @Test + public void aTimeGateThatDoesNotRedirectExitsOneWithEmptyStdout() throws Exception + { + try (StubServer server = new StubServer()) + { + server.responds(200, ""); + StringWriter out = new StringWriter(), err = new StringWriter(); + + int code = commandLine(out, err).execute("get", + "-f", keyStorePath().toString(), "-p", "changeit", + "--timegate", server.baseURI().resolve("some/").toString()); + + assertEquals(CommandLine.ExitCode.SOFTWARE, code); + assertEquals("", out.toString(), "a failed command must print nothing on stdout"); + } + } + + @Test + public void mementoOptionsAddressTheDocumentsOwnQueryParams() throws Exception + { + try (StubServer server = new StubServer()) + { + server.responds(200, ""); + URI doc = server.baseURI().resolve("some/"); + StringWriter out = new StringWriter(), err = new StringWriter(); + + assertEquals(0, commandLine(out, err).execute("get", + "-f", keyStorePath().toString(), "-p", "changeit", + "--accept", "text/turtle", "--version", "a1b2c3", doc.toString())); + assertEquals("/some/?version=a1b2c3", server.getLastTarget()); + + assertEquals(0, commandLine(out, err).execute("get", + "-f", keyStorePath().toString(), "-p", "changeit", + "--accept", "text/turtle", "--timemap", doc.toString())); + assertEquals("/some/?timemap", server.getLastTarget()); + } + } + + @Test + public void acceptIsRequiredForEverythingButTheTimeGate() throws Exception + { + try (StubServer server = new StubServer()) + { + server.responds(200, ""); + StringWriter out = new StringWriter(), err = new StringWriter(); + + int code = commandLine(out, err).execute("get", + "-f", keyStorePath().toString(), "-p", "changeit", server.baseURI().toString()); + + assertEquals(CommandLine.ExitCode.USAGE, code); + assertNull(server.getLastMethod(), "a request went out without a requested media type"); + } + } + + @Test + public void listPackagesMarksTheImportedOnes() throws Exception + { + String settings = """ + @prefix ldh: . + ldh:import . + """; + String catalog = """ + + + + + + + SKOS + + + FOAF + + + """; + + try (StubServer server = new StubServer()) + { + server.respondsTo("/settings", 200, "text/turtle", settings); + server.respondsTo("/", 200, "application/rdf+xml", catalog); + URI base = server.baseURI(); + StringWriter out = new StringWriter(), err = new StringWriter(); + + int code = commandLine(out, err).execute("packages", "list", + "-f", keyStorePath().toString(), "-p", "changeit", "-b", base.toString()); + + assertEquals(0, code); + assertEquals(List.of("available\thttps://packages.linkeddatahub.com/foaf/#this\tFOAF", + "installed\thttps://packages.linkeddatahub.com/skos/#this\tSKOS"), + out.toString().lines().toList()); + assertEquals("", err.toString(), "stderr is not empty on success"); + // the registry is not the application's own URI, so the catalog is read through the proxy + assertEquals("/?uri=https%3A%2F%2Fpackages.linkeddatahub.com%2F", server.getLastTarget()); + } + } + } diff --git a/cli/src/test/java/com/atomgraph/linkeddatahub/cli/CommandParsingTest.java b/cli/src/test/java/com/atomgraph/linkeddatahub/cli/CommandParsingTest.java index 20d4d1877c..89ba766c81 100644 --- a/cli/src/test/java/com/atomgraph/linkeddatahub/cli/CommandParsingTest.java +++ b/cli/src/test/java/com/atomgraph/linkeddatahub/cli/CommandParsingTest.java @@ -43,37 +43,46 @@ static CommandLine commandLine() } @Test - public void commandTreeMirrorsScriptLayout() + public void commandTreeGroupsByVerb() { CommandLine root = commandLine(); - List.of("get", "post", "put", "patch", "delete", "create-item", "create-container", - "add-view", "add-construct", "add-select", "add-result-set-chart", "add-file", "add-generic-service", - "admin", "content", "imports"). + List.of("get", "post", "put", "patch", "delete", "create", "add", "remove", "import", "packages", "admin"). forEach(name -> assertTrue(root.getSubcommands().containsKey(name), name)); + List.of("item", "container"). + forEach(name -> assertTrue(root.getSubcommands().get("create").getSubcommands().containsKey(name), name)); + + List.of("view", "construct", "select", "result-set-chart", "file", "generic-service", + "rdf-import", "csv-import", "object-block", "xhtml-block"). + forEach(name -> assertTrue(root.getSubcommands().get("add").getSubcommands().containsKey(name), name)); + + assertTrue(root.getSubcommands().get("remove").getSubcommands().containsKey("block")); + + List.of("list", "add", "remove"). + forEach(name -> assertTrue(root.getSubcommands().get("packages").getSubcommands().containsKey(name), name)); + + List.of("rdf", "csv"). + forEach(name -> assertTrue(root.getSubcommands().get("import").getSubcommands().containsKey(name), name)); + CommandLine admin = root.getSubcommands().get("admin"); - List.of("ontologies", "acl", "clear-ontology", "add-ontology-import"). + List.of("create", "add", "clear", "import", "make-public"). forEach(name -> assertTrue(admin.getSubcommands().containsKey(name), name)); - List.of("create-ontology", "import-ontology", "add-class", "add-constructor", "add-select", - "add-property-constraint", "add-restriction"). - forEach(name -> assertTrue(admin.getSubcommands().get("ontologies").getSubcommands().containsKey(name), name)); - - List.of("create-group", "create-authorization", "add-agent-to-group", "make-public"). - forEach(name -> assertTrue(admin.getSubcommands().get("acl").getSubcommands().containsKey(name), name)); + List.of("ontology", "group", "authorization"). + forEach(name -> assertTrue(admin.getSubcommands().get("create").getSubcommands().containsKey(name), name)); - List.of("add-object-block", "add-xhtml-block", "remove-block"). - forEach(name -> assertTrue(root.getSubcommands().get("content").getSubcommands().containsKey(name), name)); + List.of("class", "constructor", "select", "property-constraint", "restriction", "ontology-import", "agent"). + forEach(name -> assertTrue(admin.getSubcommands().get("add").getSubcommands().containsKey(name), name)); - List.of("add-csv-import", "add-rdf-import", "import-csv", "import-rdf"). - forEach(name -> assertTrue(root.getSubcommands().get("imports").getSubcommands().containsKey(name), name)); + assertTrue(admin.getSubcommands().get("clear").getSubcommands().containsKey("ontology")); + assertTrue(admin.getSubcommands().get("import").getSubcommands().containsKey("ontology")); } @Test public void missingRequiredOptionIsUsageError() { - assertEquals(CommandLine.ExitCode.USAGE, commandLine().execute("create-item", "--container", "https://localhost:4443/some/")); + assertEquals(CommandLine.ExitCode.USAGE, commandLine().execute("create", "item", "--container", "https://localhost:4443/some/")); } @Test @@ -86,7 +95,7 @@ public void unknownOptionIsUsageError() public void bareGroupCommandIsUsageError() { assertEquals(CommandLine.ExitCode.USAGE, commandLine().execute("admin")); - assertEquals(CommandLine.ExitCode.USAGE, commandLine().execute("admin", "acl")); + assertEquals(CommandLine.ExitCode.USAGE, commandLine().execute("admin", "add")); } @Test @@ -95,13 +104,13 @@ public void helpIsRecognizedAtEveryNestingLevel() List.of(new String[] { "--help" }, new String[] { "get", "--help" }, new String[] { "admin", "--help" }, - new String[] { "content", "--help" }, - new String[] { "imports", "--help" }, - new String[] { "admin", "acl", "--help" }, - new String[] { "admin", "ontologies", "--help" }, - new String[] { "content", "remove-block", "--help" }, - new String[] { "imports", "import-csv", "-h" }, - new String[] { "admin", "ontologies", "add-class", "--help" }). + new String[] { "add", "--help" }, + new String[] { "import", "--help" }, + new String[] { "admin", "create", "--help" }, + new String[] { "admin", "add", "--help" }, + new String[] { "remove", "block", "--help" }, + new String[] { "import", "csv", "-h" }, + new String[] { "admin", "add", "class", "--help" }). forEach(args -> assertEquals(CommandLine.ExitCode.OK, commandLine().execute(args), String.join(" ", args))); } @@ -111,15 +120,15 @@ public void helpPrintsTheUsageOfTheCommandItWasAskedOn() StringWriter out = new StringWriter(); CommandLine cmd = commandLine(); cmd.setOut(new PrintWriter(out)); - cmd.execute("content", "remove-block", "--help"); + cmd.execute("remove", "block", "--help"); - assertTrue(out.toString().startsWith("Usage: ldh content remove-block"), out.toString()); + assertTrue(out.toString().startsWith("Usage: ldh remove block"), out.toString()); } @Test public void repeatableOptionsAccumulate() { - ParseResult parseResult = commandLine().parseArgs("admin", "acl", "create-group", + ParseResult parseResult = commandLine().parseArgs("admin", "create", "group", "-f", "cert.p12", "-p", "secret", "-b", "https://admin.localhost:4443/", "--name", "Editors", "--member", "https://localhost:4443/acl/agents/a/#this", @@ -138,4 +147,19 @@ public void missingCertOptionsFailValidationAtExecutionTime() assertEquals(CommandLine.ExitCode.USAGE, commandLine().execute("delete", "https://localhost:4443/some/")); } + @Test + public void mementoRolesAreMutuallyExclusive() + { + assertEquals(CommandLine.ExitCode.USAGE, commandLine().execute("get", "--timemap", "--version", "a1b2c3", "https://localhost:4443/some/")); + assertEquals(CommandLine.ExitCode.USAGE, commandLine().execute("get", "--timemap", "--timegate", "https://localhost:4443/some/")); + } + + @Test + public void datetimeIsOnlyValidWithTheTimeGate() + { + assertNotNull(commandLine().parseArgs("get", "--timegate", "--datetime", "2026-08-20T10:00:00Z", "https://localhost:4443/some/")); + assertEquals(CommandLine.ExitCode.USAGE, commandLine().execute("get", + "--accept", "text/turtle", "--datetime", "2026-08-20T10:00:00Z", "https://localhost:4443/some/")); + } + } diff --git a/cli/src/test/java/com/atomgraph/linkeddatahub/cli/http/StubServer.java b/cli/src/test/java/com/atomgraph/linkeddatahub/cli/http/StubServer.java index 0adfe63540..b2e62260c9 100644 --- a/cli/src/test/java/com/atomgraph/linkeddatahub/cli/http/StubServer.java +++ b/cli/src/test/java/com/atomgraph/linkeddatahub/cli/http/StubServer.java @@ -21,6 +21,9 @@ import java.net.InetSocketAddress; import java.net.URI; import java.nio.charset.StandardCharsets; +import java.util.LinkedHashMap; +import java.util.Locale; +import java.util.Map; /** * In-process HTTP server that answers every request with a canned status and body, and records @@ -35,10 +38,13 @@ public class StubServer implements AutoCloseable private volatile int status = 200; private volatile String body = ""; private volatile String contentType = "text/turtle"; + private final Map responseHeaders = new LinkedHashMap<>(); + private final Map routes = new LinkedHashMap<>(); private volatile String lastMethod; private volatile String lastTarget; private volatile String lastBody; + private volatile Map lastHeaders = Map.of(); /** * Starts the server on an ephemeral loopback port. @@ -55,9 +61,15 @@ public StubServer() throws IOException lastTarget = exchange.getRequestURI().toString(); lastBody = new String(exchange.getRequestBody().readAllBytes(), StandardCharsets.UTF_8); - byte[] out = body.getBytes(StandardCharsets.UTF_8); - exchange.getResponseHeaders().add("Content-Type", contentType); - exchange.sendResponseHeaders(status, out.length == 0 ? -1 : out.length); + Map headers = new LinkedHashMap<>(); + exchange.getRequestHeaders().forEach((name, values) -> headers.put(name.toLowerCase(Locale.ROOT), values.get(0))); + lastHeaders = headers; + + Route route = routes.get(exchange.getRequestURI().getPath()); + byte[] out = (route != null ? route.body() : body).getBytes(StandardCharsets.UTF_8); + exchange.getResponseHeaders().add("Content-Type", route != null ? route.contentType() : contentType); + responseHeaders.forEach((name, value) -> exchange.getResponseHeaders().add(name, value)); + exchange.sendResponseHeaders(route != null ? route.status() : status, out.length == 0 ? -1 : out.length); if (out.length > 0) exchange.getResponseBody().write(out); exchange.close(); }); @@ -75,6 +87,24 @@ public URI baseURI() return URI.create("http://127.0.0.1:" + server.getAddress().getPort() + "/"); } + /** A canned response bound to one request path, for the commands that read more than one document. */ + private record Route(int status, String contentType, String body) { } + + /** + * Binds a canned response to a request path, taking precedence over the default one. + * + * @param path request path + * @param status HTTP status code + * @param contentType response media type + * @param body response body + * @return this server + */ + public StubServer respondsTo(String path, int status, String contentType, String body) + { + routes.put(path, new Route(status, contentType, body)); + return this; + } + /** * Sets the canned response. * @@ -101,6 +131,19 @@ public StubServer respondsWithType(String contentType) return this; } + /** + * Adds a header to the canned response. + * + * @param name header name + * @param value header value + * @return this server + */ + public StubServer respondsWithHeader(String name, String value) + { + responseHeaders.put(name, value); + return this; + } + /** * Returns the method of the last request. * @@ -121,6 +164,17 @@ public String getLastTarget() return lastTarget; } + /** + * Returns a request header of the last request. + * + * @param name header name, matched case-insensitively + * @return header value, or null if the header was not sent + */ + public String getLastHeader(String name) + { + return lastHeaders.get(name.toLowerCase(Locale.ROOT)); + } + /** * Returns the body of the last request. * diff --git a/cli/src/test/java/com/atomgraph/linkeddatahub/cli/sparql/UpdatesTest.java b/cli/src/test/java/com/atomgraph/linkeddatahub/cli/sparql/UpdatesTest.java index f7608273a5..92d649369f 100644 --- a/cli/src/test/java/com/atomgraph/linkeddatahub/cli/sparql/UpdatesTest.java +++ b/cli/src/test/java/com/atomgraph/linkeddatahub/cli/sparql/UpdatesTest.java @@ -33,6 +33,7 @@ public class UpdatesTest private static final URI DOC = URI.create("https://localhost:4443/some/"); private static final URI BASE = URI.create("https://localhost:4443/"); private static final URI ADMIN_BASE = URI.create("https://admin.localhost:4443/"); + private static final URI PACKAGE = URI.create("https://packages.linkeddatahub.com/skos/#this"); @Test public void insertOntologyImportIsValidSPARQL11() @@ -43,6 +44,27 @@ public void insertOntologyImportIsValidSPARQL11() assertTrue(update.contains("")); } + @Test + public void insertPackageImportIsValidSPARQL11() + { + String update = Updates.insertPackageImport(PACKAGE); + + assertDoesNotThrow(() -> UpdateFactory.create(update, Syntax.syntaxSPARQL_11)); + assertTrue(update.contains("")); + // the application is matched by type, never named: its URI is an opaque urn: identifier + assertTrue(update.contains("?app")); + } + + @Test + public void deletePackageImportIsValidSPARQL11() + { + String update = Updates.deletePackageImport(PACKAGE); + + assertDoesNotThrow(() -> UpdateFactory.create(update, Syntax.syntaxSPARQL_11)); + assertTrue(update.contains("")); + assertFalse(update.contains("?package")); + } + @Test public void insertGroupMemberIsValidSPARQL11() { diff --git a/cli/src/test/java/com/atomgraph/linkeddatahub/cli/util/MementosTest.java b/cli/src/test/java/com/atomgraph/linkeddatahub/cli/util/MementosTest.java new file mode 100644 index 0000000000..7e3ea7ca9c --- /dev/null +++ b/cli/src/test/java/com/atomgraph/linkeddatahub/cli/util/MementosTest.java @@ -0,0 +1,76 @@ +/* + * Copyright 2026 Martynas Jusevičius . + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.atomgraph.linkeddatahub.cli.util; + +import java.net.URI; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +/** + * Tests the addressing of the Memento roles and the Accept-Datetime format. + */ +public class MementosTest +{ + + static final URI DOC = URI.create("https://localhost:4443/some/"); + + @Test + public void mementoRolesAreQueryParamsOnTheDocumentURI() + { + assertEquals(URI.create("https://localhost:4443/some/?version=a1b2c3"), Mementos.version(DOC, "a1b2c3")); + assertEquals(URI.create("https://localhost:4443/some/?timemap"), Mementos.timeMap(DOC)); + assertEquals(URI.create("https://localhost:4443/some/?timegate"), Mementos.timeGate(DOC)); + } + + @Test + public void valuelessParamsKeepTheBareFormTheServerAdvertises() + { + // the Link headers point at , not + assertEquals("timemap", Mementos.timeMap(DOC).getRawQuery()); + assertEquals("timegate", Mementos.timeGate(DOC).getRawQuery()); + } + + @Test + public void anExistingQueryIsKeptAndTheFragmentStaysLast() + { + assertEquals(URI.create("https://localhost:4443/some/?a=b&timemap"), Mementos.timeMap(URI.create("https://localhost:4443/some/?a=b"))); + assertEquals(URI.create("https://localhost:4443/some/?version=a1b2c3#this"), Mementos.version(URI.create("https://localhost:4443/some/#this"), "a1b2c3")); + } + + @Test + public void acceptDatetimeTakesRFC1123AndISO8601() + { + assertEquals("Thu, 20 Aug 2026 10:00:00 GMT", Mementos.acceptDatetime("Thu, 20 Aug 2026 10:00:00 GMT")); + assertEquals("Thu, 20 Aug 2026 10:00:00 GMT", Mementos.acceptDatetime("2026-08-20T10:00:00Z")); + assertEquals("Thu, 20 Aug 2026 10:00:00 GMT", Mementos.acceptDatetime("2026-08-20T12:00:00+02:00")); + } + + @Test + public void theDayOfMonthIsZeroPadded() + { + // RFC_1123_DATE_TIME leaves it unpadded where the RFC 7089 grammar wants 2DIGIT + assertEquals("Wed, 05 Aug 2026 10:00:00 GMT", Mementos.acceptDatetime("2026-08-05T10:00:00Z")); + } + + @Test + public void anUnparseableDatetimeIsRejected() + { + assertThrows(IllegalArgumentException.class, () -> Mementos.acceptDatetime("yesterday afternoon")); + } + +} diff --git a/http-tests/access/group-authorization.sh b/http-tests/access/group-authorization.sh index 92f4a8fc92..7f64436ef4 100755 --- a/http-tests/access/group-authorization.sh +++ b/http-tests/access/group-authorization.sh @@ -25,7 +25,7 @@ fi # add agent to the writers group -ldh admin acl add-agent-to-group \ +ldh admin add agent \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ --agent "$AGENT_URI" \ @@ -33,7 +33,7 @@ ldh admin acl add-agent-to-group \ # create container -ldh create-container \ +ldh create container \ -f "$AGENT_CERT_KEYSTORE" \ -p "$AGENT_CERT_PWD" \ -b "$END_USER_BASE_URL" \ diff --git a/http-tests/access/owner-authorization.sh b/http-tests/access/owner-authorization.sh index b59f30adaa..25230657cd 100755 --- a/http-tests/access/owner-authorization.sh +++ b/http-tests/access/owner-authorization.sh @@ -9,7 +9,7 @@ purge_cache "$FRONTEND_VARNISH_SERVICE" # add agent to the writers group -ldh admin acl add-agent-to-group \ +ldh admin add agent \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ --agent "$AGENT_URI" \ @@ -19,7 +19,7 @@ ldh admin acl add-agent-to-group \ slug="test" -container=$(ldh create-container \ +container=$(ldh create container \ -f "$AGENT_CERT_KEYSTORE" \ -p "$AGENT_CERT_PWD" \ -b "$END_USER_BASE_URL" \ diff --git a/http-tests/add/GET-proxied-source-POST-append.sh b/http-tests/add/GET-proxied-source-POST-append.sh index ebf7333fbb..5fc7a338e4 100755 --- a/http-tests/add/GET-proxied-source-POST-append.sh +++ b/http-tests/add/GET-proxied-source-POST-append.sh @@ -13,13 +13,13 @@ purge_cache "$FRONTEND_VARNISH_SERVICE" # add agent to the readers group (to read through the proxy) and the writers group (to append) -ldh admin acl add-agent-to-group \ +ldh admin add agent \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ --agent "$AGENT_URI" \ "${ADMIN_BASE_URL}acl/groups/readers/" -ldh admin acl add-agent-to-group \ +ldh admin add agent \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ --agent "$AGENT_URI" \ @@ -27,7 +27,7 @@ ldh admin acl add-agent-to-group \ # create the target container -container=$(ldh create-container \ +container=$(ldh create container \ -f "$AGENT_CERT_KEYSTORE" \ -p "$AGENT_CERT_PWD" \ -b "$END_USER_BASE_URL" \ diff --git a/http-tests/add/PUT-generate-container.sh b/http-tests/add/PUT-generate-container.sh index 52b661e887..ead8fb54fe 100755 --- a/http-tests/add/PUT-generate-container.sh +++ b/http-tests/add/PUT-generate-container.sh @@ -16,7 +16,7 @@ purge_cache "$FRONTEND_VARNISH_SERVICE" # add agent to the writers group -ldh admin acl add-agent-to-group \ +ldh admin add agent \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ --agent "$AGENT_URI" \ diff --git a/http-tests/admin/acl/add-delete-authorization.sh b/http-tests/admin/acl/add-delete-authorization.sh index b040aea527..866151d8b3 100755 --- a/http-tests/admin/acl/add-delete-authorization.sh +++ b/http-tests/admin/acl/add-delete-authorization.sh @@ -20,7 +20,7 @@ curl -k -w "%{http_code}\n" -o /dev/null -s \ slug="test" -container=$(ldh create-container \ +container=$(ldh create container \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ -b "$END_USER_BASE_URL" \ @@ -30,7 +30,7 @@ container=$(ldh create-container \ # create fake test.localhost authorization (should be filtered out) -ldh admin acl create-authorization \ +ldh admin create authorization \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ -b "https://admin.test.localhost:4443/" \ @@ -50,7 +50,7 @@ curl -k -w "%{http_code}\n" -o /dev/null -s \ # create real localhost authorization -ldh admin acl create-authorization \ +ldh admin create authorization \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ -b "$ADMIN_BASE_URL" \ diff --git a/http-tests/admin/acl/add-delete-class-authorization.sh b/http-tests/admin/acl/add-delete-class-authorization.sh index 1a8cbdab89..3f3b38741a 100755 --- a/http-tests/admin/acl/add-delete-class-authorization.sh +++ b/http-tests/admin/acl/add-delete-class-authorization.sh @@ -20,7 +20,7 @@ curl -k -w "%{http_code}\n" -o /dev/null -s \ slug="test" -container=$(ldh create-container \ +container=$(ldh create container \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ -b "$END_USER_BASE_URL" \ @@ -30,7 +30,7 @@ container=$(ldh create-container \ # create fake test.localhost authorization (should be filtered out) -ldh admin acl create-authorization \ +ldh admin create authorization \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ -b "https://admin.test.localhost:4443/" \ @@ -50,7 +50,7 @@ curl -k -w "%{http_code}\n" -o /dev/null -s \ # create real localhost authorization -ldh admin acl create-authorization \ +ldh admin create authorization \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ -b "$ADMIN_BASE_URL" \ diff --git a/http-tests/admin/acl/add-delete-group-authorization.sh b/http-tests/admin/acl/add-delete-group-authorization.sh index 781183ba25..b3404240b9 100755 --- a/http-tests/admin/acl/add-delete-group-authorization.sh +++ b/http-tests/admin/acl/add-delete-group-authorization.sh @@ -18,7 +18,7 @@ curl -k -w "%{http_code}\n" -o /dev/null -s \ # create group -group_doc=$(ldh admin acl create-group \ +group_doc=$(ldh admin create group \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ -b "$ADMIN_BASE_URL" \ @@ -36,7 +36,7 @@ group=$(curl -s -k \ slug="test" -container=$(ldh create-container \ +container=$(ldh create container \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ -b "$END_USER_BASE_URL" \ @@ -46,7 +46,7 @@ container=$(ldh create-container \ # create fake test.localhost authorization (should be filtered out) -ldh admin acl create-authorization \ +ldh admin create authorization \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ -b "https://admin.test.localhost:4443/" \ @@ -66,7 +66,7 @@ curl -k -w "%{http_code}\n" -o /dev/null -s \ # create real localhost authorization -ldh admin acl create-authorization \ +ldh admin create authorization \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ -b "$ADMIN_BASE_URL" \ diff --git a/http-tests/admin/acl/add-get-authorization.sh b/http-tests/admin/acl/add-get-authorization.sh index 77ecf4ee11..92e2695460 100755 --- a/http-tests/admin/acl/add-get-authorization.sh +++ b/http-tests/admin/acl/add-get-authorization.sh @@ -17,7 +17,7 @@ curl -k -w "%{http_code}\n" -o /dev/null -s \ # create fake test.localhost authorization (should be filtered out) -ldh admin acl create-authorization \ +ldh admin create authorization \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ -b "https://admin.test.localhost:4443/" \ @@ -36,7 +36,7 @@ curl -k -w "%{http_code}\n" -o /dev/null -s \ # create real localhost authorization -ldh admin acl create-authorization \ +ldh admin create authorization \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ -b "$ADMIN_BASE_URL" \ diff --git a/http-tests/admin/acl/add-get-class-authorization.sh b/http-tests/admin/acl/add-get-class-authorization.sh index 99ca051f98..f920205065 100755 --- a/http-tests/admin/acl/add-get-class-authorization.sh +++ b/http-tests/admin/acl/add-get-class-authorization.sh @@ -17,7 +17,7 @@ curl -k -w "%{http_code}\n" -o /dev/null -s \ # create fake test.localhost authorization (should be filtered out) -ldh admin acl create-authorization \ +ldh admin create authorization \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ -b "https://admin.test.localhost:4443/" \ @@ -36,7 +36,7 @@ curl -k -w "%{http_code}\n" -o /dev/null -s \ # create real localhost authorization -ldh admin acl create-authorization \ +ldh admin create authorization \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ -b "$ADMIN_BASE_URL" \ diff --git a/http-tests/admin/acl/add-get-group-authorization.sh b/http-tests/admin/acl/add-get-group-authorization.sh index 357b2b20d3..61817b8a13 100755 --- a/http-tests/admin/acl/add-get-group-authorization.sh +++ b/http-tests/admin/acl/add-get-group-authorization.sh @@ -17,7 +17,7 @@ curl -k -w "%{http_code}\n" -o /dev/null -s \ # create group -group_doc=$(ldh admin acl create-group \ +group_doc=$(ldh admin create group \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ -b "$ADMIN_BASE_URL" \ @@ -33,7 +33,7 @@ group=$(curl -s -k \ # create fake test.localhost authorization (should be filtered out) -ldh admin acl create-authorization \ +ldh admin create authorization \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ -b "https://admin.test.localhost:4443/" \ @@ -52,7 +52,7 @@ curl -k -w "%{http_code}\n" -o /dev/null -s \ # create real localhost authorization -ldh admin acl create-authorization \ +ldh admin create authorization \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ -b "$ADMIN_BASE_URL" \ diff --git a/http-tests/admin/acl/add-post-authorization.sh b/http-tests/admin/acl/add-post-authorization.sh index d443706cd9..a39c8ccb98 100755 --- a/http-tests/admin/acl/add-post-authorization.sh +++ b/http-tests/admin/acl/add-post-authorization.sh @@ -24,7 +24,7 @@ EOF # create fake test.localhost authorization (should be filtered out) -ldh admin acl create-authorization \ +ldh admin create authorization \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ -b "https://admin.test.localhost:4443/" \ @@ -50,7 +50,7 @@ EOF # create real localhost authorization -ldh admin acl create-authorization \ +ldh admin create authorization \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ -b "$ADMIN_BASE_URL" \ diff --git a/http-tests/admin/acl/add-post-class-authorization.sh b/http-tests/admin/acl/add-post-class-authorization.sh index fc0627e4e3..b22f65517b 100755 --- a/http-tests/admin/acl/add-post-class-authorization.sh +++ b/http-tests/admin/acl/add-post-class-authorization.sh @@ -24,7 +24,7 @@ EOF # create fake test.localhost authorization (should be filtered out) -ldh admin acl create-authorization \ +ldh admin create authorization \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ -b "https://admin.test.localhost:4443/" \ @@ -50,7 +50,7 @@ EOF # create real localhost authorization -ldh admin acl create-authorization \ +ldh admin create authorization \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ -b "$ADMIN_BASE_URL" \ diff --git a/http-tests/admin/acl/add-post-group-authorization.sh b/http-tests/admin/acl/add-post-group-authorization.sh index cb532f634e..2683256abe 100755 --- a/http-tests/admin/acl/add-post-group-authorization.sh +++ b/http-tests/admin/acl/add-post-group-authorization.sh @@ -24,7 +24,7 @@ EOF # create group -group_doc=$(ldh admin acl create-group \ +group_doc=$(ldh admin create group \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ -b "$ADMIN_BASE_URL" \ @@ -40,7 +40,7 @@ group=$(curl -s -k \ # create fake test.localhost authorization (should be filtered out) -ldh admin acl create-authorization \ +ldh admin create authorization \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ -b "https://admin.test.localhost:4443/" \ @@ -66,7 +66,7 @@ EOF # create real localhost authorization -ldh admin acl create-authorization \ +ldh admin create authorization \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ -b "$ADMIN_BASE_URL" \ diff --git a/http-tests/admin/acl/add-put-authorization.sh b/http-tests/admin/acl/add-put-authorization.sh index 896c626b78..753d19aeff 100755 --- a/http-tests/admin/acl/add-put-authorization.sh +++ b/http-tests/admin/acl/add-put-authorization.sh @@ -24,7 +24,7 @@ EOF # create fake test.localhost authorization (should be filtered out) -ldh admin acl create-authorization \ +ldh admin create authorization \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ -b "https://admin.test.localhost:4443/" \ @@ -50,7 +50,7 @@ EOF # create real localhost authorization -ldh admin acl create-authorization \ +ldh admin create authorization \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ -b "$ADMIN_BASE_URL" \ diff --git a/http-tests/admin/acl/add-put-class-authorization.sh b/http-tests/admin/acl/add-put-class-authorization.sh index 3938f74135..f8eae7d9d1 100755 --- a/http-tests/admin/acl/add-put-class-authorization.sh +++ b/http-tests/admin/acl/add-put-class-authorization.sh @@ -24,7 +24,7 @@ EOF # create fake test.localhost authorization (should be filtered out) -ldh admin acl create-authorization \ +ldh admin create authorization \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ -b "https://admin.test.localhost:4443/" \ @@ -50,7 +50,7 @@ EOF # create real localhost authorization -ldh admin acl create-authorization \ +ldh admin create authorization \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ -b "$ADMIN_BASE_URL" \ diff --git a/http-tests/admin/acl/add-put-group-authorization.sh b/http-tests/admin/acl/add-put-group-authorization.sh index e594d247c6..eb64c3fd56 100755 --- a/http-tests/admin/acl/add-put-group-authorization.sh +++ b/http-tests/admin/acl/add-put-group-authorization.sh @@ -24,7 +24,7 @@ EOF # create group -group_doc=$(ldh admin acl create-group \ +group_doc=$(ldh admin create group \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ -b "$ADMIN_BASE_URL" \ @@ -40,7 +40,7 @@ group=$(curl -s -k \ # create fake test.localhost authorization (should be filtered out) -ldh admin acl create-authorization \ +ldh admin create authorization \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ -b "https://admin.test.localhost:4443/" \ @@ -66,7 +66,7 @@ EOF # create real localhost authorization -ldh admin acl create-authorization \ +ldh admin create authorization \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ -b "$ADMIN_BASE_URL" \ diff --git a/http-tests/admin/acl/agent-acl-modes.sh b/http-tests/admin/acl/agent-acl-modes.sh index dfccf4f9ae..2f71bd695d 100644 --- a/http-tests/admin/acl/agent-acl-modes.sh +++ b/http-tests/admin/acl/agent-acl-modes.sh @@ -9,7 +9,7 @@ purge_cache "$FRONTEND_VARNISH_SERVICE" # add agent to the writers group -ldh admin acl add-agent-to-group \ +ldh admin add agent \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ --agent "$AGENT_URI" \ @@ -17,7 +17,7 @@ ldh admin acl add-agent-to-group \ # create a new document to test ACL modes against -doc_url=$(ldh create-item \ +doc_url=$(ldh create item \ -b "$END_USER_BASE_URL" \ -f "$AGENT_CERT_KEYSTORE" \ -p "$AGENT_CERT_PWD" \ diff --git a/http-tests/admin/acl/make-public.sh b/http-tests/admin/acl/make-public.sh index 1a148817c8..6bc17130bd 100755 --- a/http-tests/admin/acl/make-public.sh +++ b/http-tests/admin/acl/make-public.sh @@ -16,7 +16,7 @@ curl -k -w "%{http_code}\n" -o /dev/null -v \ # create fake test.localhost public authorization (should be filtered out) -ldh admin acl create-authorization \ +ldh admin create authorization \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ -b "https://admin.test.localhost:4443/" \ @@ -34,7 +34,7 @@ curl -k -w "%{http_code}\n" -o /dev/null -v \ # create real localhost public authorization -ldh admin acl create-authorization \ +ldh admin create authorization \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ -b "$ADMIN_BASE_URL" \ diff --git a/http-tests/admin/acl/owner-acl-modes.sh b/http-tests/admin/acl/owner-acl-modes.sh index fb0b938d0d..30795ad775 100644 --- a/http-tests/admin/acl/owner-acl-modes.sh +++ b/http-tests/admin/acl/owner-acl-modes.sh @@ -9,7 +9,7 @@ purge_cache "$FRONTEND_VARNISH_SERVICE" # create a new document to test ACL modes against -doc_url=$(ldh create-item \ +doc_url=$(ldh create item \ -b "$END_USER_BASE_URL" \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ diff --git a/http-tests/admin/model/add-class.sh b/http-tests/admin/model/add-class.sh index ab860c4104..3938434713 100755 --- a/http-tests/admin/model/add-class.sh +++ b/http-tests/admin/model/add-class.sh @@ -14,7 +14,7 @@ namespace="${namespace_doc}#" ontology_doc="${ADMIN_BASE_URL}ontologies/namespace/" class="${namespace_doc}#NewClass" -ldh admin ontologies add-class \ +ldh admin add class \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ -b "$ADMIN_BASE_URL" \ @@ -25,7 +25,7 @@ ldh admin ontologies add-class \ # clear ontology from memory -ldh admin clear-ontology \ +ldh admin clear ontology \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ -b "$ADMIN_BASE_URL" \ diff --git a/http-tests/admin/model/add-ontology-import.sh b/http-tests/admin/model/add-ontology-import.sh index 43c954cd13..75d7eff057 100755 --- a/http-tests/admin/model/add-ontology-import.sh +++ b/http-tests/admin/model/add-ontology-import.sh @@ -14,7 +14,7 @@ import_uri="http://www.w3.org/ns/auth/acl" # add ontology import -ldh admin add-ontology-import \ +ldh admin add ontology-import \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ --import "$import_uri" \ @@ -22,7 +22,7 @@ ldh admin add-ontology-import \ # clear the namespace ontology from memory -ldh admin clear-ontology \ +ldh admin clear ontology \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ -b "$ADMIN_BASE_URL" \ diff --git a/http-tests/admin/model/add-property-constraint.sh b/http-tests/admin/model/add-property-constraint.sh index d91e6cd8ae..60635b7c41 100755 --- a/http-tests/admin/model/add-property-constraint.sh +++ b/http-tests/admin/model/add-property-constraint.sh @@ -14,7 +14,7 @@ namespace="${namespace_doc}#" ontology_doc="${ADMIN_BASE_URL}ontologies/namespace/" constraint="${namespace_doc}#NewConstraint" -ldh admin ontologies add-property-constraint \ +ldh admin add property-constraint \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ -b "$ADMIN_BASE_URL" \ @@ -25,7 +25,7 @@ ldh admin ontologies add-property-constraint \ # create a class with the constraint -ldh admin ontologies add-class \ +ldh admin add class \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ -b "$ADMIN_BASE_URL" \ @@ -37,7 +37,7 @@ ldh admin ontologies add-class \ # clear ontology from memory -ldh admin clear-ontology \ +ldh admin clear ontology \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ -b "$ADMIN_BASE_URL" \ diff --git a/http-tests/admin/model/add-restriction.sh b/http-tests/admin/model/add-restriction.sh index 8f1b7ab012..89d275bf73 100755 --- a/http-tests/admin/model/add-restriction.sh +++ b/http-tests/admin/model/add-restriction.sh @@ -14,7 +14,7 @@ namespace="${namespace_doc}#" ontology_doc="${ADMIN_BASE_URL}ontologies/namespace/" restriction="${namespace_doc}#Restriction" -ldh admin ontologies add-restriction \ +ldh admin add restriction \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ -b "$ADMIN_BASE_URL" \ @@ -26,7 +26,7 @@ ldh admin ontologies add-restriction \ # clear ontology from memory -ldh admin clear-ontology \ +ldh admin clear ontology \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ -b "$ADMIN_BASE_URL" \ diff --git a/http-tests/admin/model/import-ontology.sh b/http-tests/admin/model/import-ontology.sh index 6b39b0173f..e6a9ef2d7a 100755 --- a/http-tests/admin/model/import-ontology.sh +++ b/http-tests/admin/model/import-ontology.sh @@ -15,7 +15,7 @@ import_uri="http://www.w3.org/2004/02/skos/core" slug="test" -item=$(ldh create-item \ +item=$(ldh create item \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ -b "$ADMIN_BASE_URL" \ @@ -26,7 +26,7 @@ item=$(ldh create-item \ # import the ontology: derive class constructors into the item document; the vocabulary itself only # passes through a scratch document and is not persisted -ldh admin ontologies import-ontology \ +ldh admin import ontology \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ -b "$ADMIN_BASE_URL" \ @@ -45,7 +45,6 @@ result=$(curl -k -f -s \ "${ADMIN_BASE_URL}sparql") count=$(echo "$result" | xmllint --xpath "count(//*[local-name() = 'result'])" -) if [ "$count" != "0" ]; then - echo "DEBUG: Expected 0 raw vocabulary triples in the item graph, got: $count" exit 1 fi @@ -74,7 +73,7 @@ curl -k -f -s \ # make the annotation document part of the application ontology (the vocabulary rides in via the # document's own owl:imports) -ldh admin add-ontology-import \ +ldh admin add ontology-import \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ --import "$item" \ @@ -82,7 +81,7 @@ ldh admin add-ontology-import \ # clear the namespace ontology from memory -ldh admin clear-ontology \ +ldh admin clear ontology \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ -b "$ADMIN_BASE_URL" \ diff --git a/http-tests/admin/model/ontology-import-upload-no-deadlock.sh b/http-tests/admin/model/ontology-import-upload-no-deadlock.sh index fba3d6207e..e0d7b11004 100755 --- a/http-tests/admin/model/ontology-import-upload-no-deadlock.sh +++ b/http-tests/admin/model/ontology-import-upload-no-deadlock.sh @@ -19,7 +19,7 @@ pwd=$(realpath "$PWD") # add agent to the writers group so they can upload files -ldh admin acl add-agent-to-group \ +ldh admin add agent \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ --agent "$AGENT_URI" \ @@ -31,7 +31,7 @@ file_content_type="text/turtle" slug=$(uuidgen | tr '[:upper:]' '[:lower:]') # Create an item document to hold the file -file_doc=$(ldh create-item \ +file_doc=$(ldh create item \ -f "$AGENT_CERT_KEYSTORE" \ -p "$AGENT_CERT_PWD" \ -b "$END_USER_BASE_URL" \ @@ -40,7 +40,7 @@ file_doc=$(ldh create-item \ --slug "$slug") # Add the file to the document -ldh add-file \ +ldh add file \ -f "$AGENT_CERT_KEYSTORE" \ -p "$AGENT_CERT_PWD" \ -b "$END_USER_BASE_URL" \ @@ -67,7 +67,7 @@ namespace_doc="${END_USER_BASE_URL}ns" namespace="${namespace_doc}#" ontology_doc="${ADMIN_BASE_URL}ontologies/namespace/" -ldh admin add-ontology-import \ +ldh admin add ontology-import \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ --import "$upload_uri" \ @@ -75,7 +75,7 @@ ldh admin add-ontology-import \ # Step 4: Clear the namespace ontology from memory to force reload on next request -ldh admin clear-ontology \ +ldh admin clear ontology \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ -b "$ADMIN_BASE_URL" \ diff --git a/http-tests/document-hierarchy/DELETE-404.sh b/http-tests/document-hierarchy/DELETE-404.sh index 2b0516003d..b0e11887a8 100755 --- a/http-tests/document-hierarchy/DELETE-404.sh +++ b/http-tests/document-hierarchy/DELETE-404.sh @@ -9,7 +9,7 @@ purge_cache "$FRONTEND_VARNISH_SERVICE" # add agent to the writers -ldh admin acl add-agent-to-group \ +ldh admin add agent \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ --agent "$AGENT_URI" \ diff --git a/http-tests/document-hierarchy/DELETE-conditional-412.sh b/http-tests/document-hierarchy/DELETE-conditional-412.sh index bda0c28751..19cb2481f7 100755 --- a/http-tests/document-hierarchy/DELETE-conditional-412.sh +++ b/http-tests/document-hierarchy/DELETE-conditional-412.sh @@ -11,7 +11,7 @@ purge_cache "$FRONTEND_VARNISH_SERVICE" slug="test" -container=$(ldh create-container \ +container=$(ldh create container \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ -b "$END_USER_BASE_URL" \ @@ -21,7 +21,7 @@ container=$(ldh create-container \ # add an explicit read/write authorization for the owner because add-agent-to-group.sh won't work non-existing URI -ldh admin acl create-authorization \ +ldh admin create authorization \ -b "$ADMIN_BASE_URL" \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ diff --git a/http-tests/document-hierarchy/DELETE-conditional.sh b/http-tests/document-hierarchy/DELETE-conditional.sh index 40726579f0..4683363b84 100755 --- a/http-tests/document-hierarchy/DELETE-conditional.sh +++ b/http-tests/document-hierarchy/DELETE-conditional.sh @@ -11,7 +11,7 @@ purge_cache "$FRONTEND_VARNISH_SERVICE" slug="test" -container=$(ldh create-container \ +container=$(ldh create container \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ -b "$END_USER_BASE_URL" \ @@ -21,7 +21,7 @@ container=$(ldh create-container \ # add an explicit read/write authorization for the owner because add-agent-to-group.sh won't work non-existing URI -ldh admin acl create-authorization \ +ldh admin create authorization \ -b "$ADMIN_BASE_URL" \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ diff --git a/http-tests/document-hierarchy/DELETE-owner-405.sh b/http-tests/document-hierarchy/DELETE-owner-405.sh index 5e7d1260fc..9f06327318 100755 --- a/http-tests/document-hierarchy/DELETE-owner-405.sh +++ b/http-tests/document-hierarchy/DELETE-owner-405.sh @@ -9,7 +9,7 @@ purge_cache "$FRONTEND_VARNISH_SERVICE" # add agent to the owners -ldh admin acl add-agent-to-group \ +ldh admin add agent \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ --agent "$AGENT_URI" \ diff --git a/http-tests/document-hierarchy/DELETE-root-405.sh b/http-tests/document-hierarchy/DELETE-root-405.sh index fc421d88fd..54fa6822c6 100755 --- a/http-tests/document-hierarchy/DELETE-root-405.sh +++ b/http-tests/document-hierarchy/DELETE-root-405.sh @@ -9,7 +9,7 @@ purge_cache "$FRONTEND_VARNISH_SERVICE" # add agent to the owners -ldh admin acl add-agent-to-group \ +ldh admin add agent \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ --agent "$AGENT_URI" \ diff --git a/http-tests/document-hierarchy/DELETE-secretary-405.sh b/http-tests/document-hierarchy/DELETE-secretary-405.sh index c542de8f5f..5cb9d488ca 100755 --- a/http-tests/document-hierarchy/DELETE-secretary-405.sh +++ b/http-tests/document-hierarchy/DELETE-secretary-405.sh @@ -9,7 +9,7 @@ purge_cache "$FRONTEND_VARNISH_SERVICE" # add agent to the owners -ldh admin acl add-agent-to-group \ +ldh admin add agent \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ --agent "$AGENT_URI" \ diff --git a/http-tests/document-hierarchy/DELETE.sh b/http-tests/document-hierarchy/DELETE.sh index b7724dec73..4bc4d0c9bf 100755 --- a/http-tests/document-hierarchy/DELETE.sh +++ b/http-tests/document-hierarchy/DELETE.sh @@ -11,7 +11,7 @@ purge_cache "$FRONTEND_VARNISH_SERVICE" slug="test" -container=$(ldh create-container \ +container=$(ldh create container \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ -b "$END_USER_BASE_URL" \ @@ -21,7 +21,7 @@ container=$(ldh create-container \ # add an explicit read/write authorization for the owner because add-agent-to-group.sh won't work non-existing URI -ldh admin acl create-authorization \ +ldh admin create authorization \ -b "$ADMIN_BASE_URL" \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ diff --git a/http-tests/document-hierarchy/GET-404.sh b/http-tests/document-hierarchy/GET-404.sh index a5f2a86514..cd12940ae2 100755 --- a/http-tests/document-hierarchy/GET-404.sh +++ b/http-tests/document-hierarchy/GET-404.sh @@ -9,7 +9,7 @@ purge_cache "$FRONTEND_VARNISH_SERVICE" # add agent to the writers -ldh admin acl add-agent-to-group \ +ldh admin add agent \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ --agent "$AGENT_URI" \ diff --git a/http-tests/document-hierarchy/GET-admin-html.sh b/http-tests/document-hierarchy/GET-admin-html.sh index 1bbe9b09c6..252ad80f95 100755 --- a/http-tests/document-hierarchy/GET-admin-html.sh +++ b/http-tests/document-hierarchy/GET-admin-html.sh @@ -9,7 +9,7 @@ purge_cache "$FRONTEND_VARNISH_SERVICE" # add agent to the owners group -ldh admin acl add-agent-to-group \ +ldh admin add agent \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ --agent "$AGENT_URI" \ diff --git a/http-tests/document-hierarchy/GET-admin.sh b/http-tests/document-hierarchy/GET-admin.sh index 49d1c2f54b..65d7a2964d 100755 --- a/http-tests/document-hierarchy/GET-admin.sh +++ b/http-tests/document-hierarchy/GET-admin.sh @@ -9,7 +9,7 @@ purge_cache "$FRONTEND_VARNISH_SERVICE" # add agent to the owners group -ldh admin acl add-agent-to-group \ +ldh admin add agent \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ --agent "$AGENT_URI" \ diff --git a/http-tests/document-hierarchy/GET-children.sh b/http-tests/document-hierarchy/GET-children.sh index 88c8372539..02fa2c4938 100755 --- a/http-tests/document-hierarchy/GET-children.sh +++ b/http-tests/document-hierarchy/GET-children.sh @@ -9,7 +9,7 @@ purge_cache "$FRONTEND_VARNISH_SERVICE" # add agent to the writers group -ldh admin acl add-agent-to-group \ +ldh admin add agent \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ --agent "$AGENT_URI" \ @@ -34,7 +34,7 @@ curl -k -f -s \ slug="test-children-query" -container=$(ldh create-container \ +container=$(ldh create container \ -f "$AGENT_CERT_KEYSTORE" \ -p "$AGENT_CERT_PWD" \ -b "$END_USER_BASE_URL" \ @@ -54,7 +54,5 @@ response=$(curl -k -f -s \ "${END_USER_BASE_URL}sparql?query=${encoded_query}") if ! grep -qF "<${container}>" <<< "$response"; then - echo "DEBUG: Expected the new container in the query results: <${container}>" - echo "DEBUG: Got: $response" exit 1 fi diff --git a/http-tests/document-hierarchy/GET-conditional-412.sh b/http-tests/document-hierarchy/GET-conditional-412.sh index fe019e810e..192bf0865f 100755 --- a/http-tests/document-hierarchy/GET-conditional-412.sh +++ b/http-tests/document-hierarchy/GET-conditional-412.sh @@ -9,7 +9,7 @@ purge_cache "$FRONTEND_VARNISH_SERVICE" # add agent to the readers group -ldh admin acl add-agent-to-group \ +ldh admin add agent \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ --agent "$AGENT_URI" \ diff --git a/http-tests/document-hierarchy/GET-conditional.sh b/http-tests/document-hierarchy/GET-conditional.sh index 6d9b0fa7f6..3096328687 100755 --- a/http-tests/document-hierarchy/GET-conditional.sh +++ b/http-tests/document-hierarchy/GET-conditional.sh @@ -9,7 +9,7 @@ purge_cache "$FRONTEND_VARNISH_SERVICE" # add agent to the readers group -ldh admin acl add-agent-to-group \ +ldh admin add agent \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ --agent "$AGENT_URI" \ diff --git a/http-tests/document-hierarchy/GET-html.sh b/http-tests/document-hierarchy/GET-html.sh index 92627cc47b..377e4e6160 100755 --- a/http-tests/document-hierarchy/GET-html.sh +++ b/http-tests/document-hierarchy/GET-html.sh @@ -9,7 +9,7 @@ purge_cache "$FRONTEND_VARNISH_SERVICE" # add agent to the readers group -ldh admin acl add-agent-to-group \ +ldh admin add agent \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ --agent "$AGENT_URI" \ diff --git a/http-tests/document-hierarchy/GET-ntriples.sh b/http-tests/document-hierarchy/GET-ntriples.sh index 10e5c0d146..66de847d53 100755 --- a/http-tests/document-hierarchy/GET-ntriples.sh +++ b/http-tests/document-hierarchy/GET-ntriples.sh @@ -9,7 +9,7 @@ purge_cache "$FRONTEND_VARNISH_SERVICE" # add agent to the writers group -ldh admin acl add-agent-to-group \ +ldh admin add agent \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ --agent "$AGENT_URI" \ diff --git a/http-tests/document-hierarchy/GET-sparql.sh b/http-tests/document-hierarchy/GET-sparql.sh index 811f95df86..e64cc3a507 100755 --- a/http-tests/document-hierarchy/GET-sparql.sh +++ b/http-tests/document-hierarchy/GET-sparql.sh @@ -9,7 +9,7 @@ purge_cache "$FRONTEND_VARNISH_SERVICE" # add agent to the readers group -ldh admin acl add-agent-to-group \ +ldh admin add agent \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ --agent "$AGENT_URI" \ diff --git a/http-tests/document-hierarchy/GET.sh b/http-tests/document-hierarchy/GET.sh index 95e9b2fa48..9663f76fe9 100755 --- a/http-tests/document-hierarchy/GET.sh +++ b/http-tests/document-hierarchy/GET.sh @@ -9,7 +9,7 @@ purge_cache "$FRONTEND_VARNISH_SERVICE" # add agent to the readers group -ldh admin acl add-agent-to-group \ +ldh admin add agent \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ --agent "$AGENT_URI" \ diff --git a/http-tests/document-hierarchy/HEAD-accept-lang.sh b/http-tests/document-hierarchy/HEAD-accept-lang.sh index edc4d5c51a..cf5ab43688 100755 --- a/http-tests/document-hierarchy/HEAD-accept-lang.sh +++ b/http-tests/document-hierarchy/HEAD-accept-lang.sh @@ -9,7 +9,7 @@ purge_cache "$FRONTEND_VARNISH_SERVICE" # add agent to the readers group to be able to read documents -ldh admin acl add-agent-to-group \ +ldh admin add agent \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ --agent "$AGENT_URI" \ diff --git a/http-tests/document-hierarchy/HEAD-accept.sh b/http-tests/document-hierarchy/HEAD-accept.sh index d7f6888f4d..6cd3bfada0 100755 --- a/http-tests/document-hierarchy/HEAD-accept.sh +++ b/http-tests/document-hierarchy/HEAD-accept.sh @@ -9,7 +9,7 @@ purge_cache "$FRONTEND_VARNISH_SERVICE" # add agent to the readers group to be able to read documents -ldh admin acl add-agent-to-group \ +ldh admin add agent \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ --agent "$AGENT_URI" \ diff --git a/http-tests/document-hierarchy/PATCH-404.sh b/http-tests/document-hierarchy/PATCH-404.sh index 0de72b24ab..9b33817843 100755 --- a/http-tests/document-hierarchy/PATCH-404.sh +++ b/http-tests/document-hierarchy/PATCH-404.sh @@ -9,7 +9,7 @@ purge_cache "$FRONTEND_VARNISH_SERVICE" # add agent to the writers -ldh admin acl add-agent-to-group \ +ldh admin add agent \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ --agent "$AGENT_URI" \ diff --git a/http-tests/document-hierarchy/PATCH-blank-node-skolemized.sh b/http-tests/document-hierarchy/PATCH-blank-node-skolemized.sh index 4fc76956cf..ee28c79ae6 100755 --- a/http-tests/document-hierarchy/PATCH-blank-node-skolemized.sh +++ b/http-tests/document-hierarchy/PATCH-blank-node-skolemized.sh @@ -9,7 +9,7 @@ purge_cache "$FRONTEND_VARNISH_SERVICE" # add agent to the writers group -ldh admin acl add-agent-to-group \ +ldh admin add agent \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ --agent "$AGENT_URI" \ diff --git a/http-tests/document-hierarchy/PATCH-constraint-422.sh b/http-tests/document-hierarchy/PATCH-constraint-422.sh index 8061c117c8..254c5bd937 100755 --- a/http-tests/document-hierarchy/PATCH-constraint-422.sh +++ b/http-tests/document-hierarchy/PATCH-constraint-422.sh @@ -9,7 +9,7 @@ purge_cache "$FRONTEND_VARNISH_SERVICE" # add agent to the writers group -ldh admin acl add-agent-to-group \ +ldh admin add agent \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ --agent "$AGENT_URI" \ diff --git a/http-tests/document-hierarchy/PATCH-delete-where-no-match.sh b/http-tests/document-hierarchy/PATCH-delete-where-no-match.sh index b905ccf6b4..d763a43a4c 100755 --- a/http-tests/document-hierarchy/PATCH-delete-where-no-match.sh +++ b/http-tests/document-hierarchy/PATCH-delete-where-no-match.sh @@ -9,7 +9,7 @@ purge_cache "$FRONTEND_VARNISH_SERVICE" # add agent to the writers group -ldh admin acl add-agent-to-group \ +ldh admin add agent \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ --agent "$AGENT_URI" \ diff --git a/http-tests/document-hierarchy/PATCH-empty-container.sh b/http-tests/document-hierarchy/PATCH-empty-container.sh index 0dabcc6859..63c3d73ce1 100755 --- a/http-tests/document-hierarchy/PATCH-empty-container.sh +++ b/http-tests/document-hierarchy/PATCH-empty-container.sh @@ -9,7 +9,7 @@ purge_cache "$FRONTEND_VARNISH_SERVICE" # add agent to the writers group -ldh admin acl add-agent-to-group \ +ldh admin add agent \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ --agent "$AGENT_URI" \ @@ -19,7 +19,7 @@ ldh admin acl add-agent-to-group \ slug=$(uuidgen | tr '[:upper:]' '[:lower:]') -container=$(ldh create-container \ +container=$(ldh create container \ -f "$AGENT_CERT_KEYSTORE" \ -p "$AGENT_CERT_PWD" \ -b "$END_USER_BASE_URL" \ diff --git a/http-tests/document-hierarchy/PATCH-empty-item.sh b/http-tests/document-hierarchy/PATCH-empty-item.sh index 9a0654f7da..e074aa93b3 100755 --- a/http-tests/document-hierarchy/PATCH-empty-item.sh +++ b/http-tests/document-hierarchy/PATCH-empty-item.sh @@ -9,7 +9,7 @@ purge_cache "$FRONTEND_VARNISH_SERVICE" # add agent to the writers group -ldh admin acl add-agent-to-group \ +ldh admin add agent \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ --agent "$AGENT_URI" \ @@ -19,7 +19,7 @@ ldh admin acl add-agent-to-group \ slug=$(uuidgen | tr '[:upper:]' '[:lower:]') -item=$(ldh create-item \ +item=$(ldh create item \ -f "$AGENT_CERT_KEYSTORE" \ -p "$AGENT_CERT_PWD" \ -b "$END_USER_BASE_URL" \ diff --git a/http-tests/document-hierarchy/PATCH-empty-root-405.sh b/http-tests/document-hierarchy/PATCH-empty-root-405.sh index 1797220764..51c25e05e4 100755 --- a/http-tests/document-hierarchy/PATCH-empty-root-405.sh +++ b/http-tests/document-hierarchy/PATCH-empty-root-405.sh @@ -9,7 +9,7 @@ purge_cache "$FRONTEND_VARNISH_SERVICE" # add agent to the owners group -ldh admin acl add-agent-to-group \ +ldh admin add agent \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ --agent "$AGENT_URI" \ diff --git a/http-tests/document-hierarchy/PATCH-graph-422.sh b/http-tests/document-hierarchy/PATCH-graph-422.sh index a6532fcb26..a7b478d3ee 100755 --- a/http-tests/document-hierarchy/PATCH-graph-422.sh +++ b/http-tests/document-hierarchy/PATCH-graph-422.sh @@ -9,7 +9,7 @@ purge_cache "$FRONTEND_VARNISH_SERVICE" # add agent to the writers group -ldh admin acl add-agent-to-group \ +ldh admin add agent \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ --agent "$AGENT_URI" \ diff --git a/http-tests/document-hierarchy/PATCH-invalid-content-block-422.sh b/http-tests/document-hierarchy/PATCH-invalid-content-block-422.sh index e7a01120b0..0e57f30987 100755 --- a/http-tests/document-hierarchy/PATCH-invalid-content-block-422.sh +++ b/http-tests/document-hierarchy/PATCH-invalid-content-block-422.sh @@ -9,7 +9,7 @@ purge_cache "$FRONTEND_VARNISH_SERVICE" # add agent to the writers group -ldh admin acl add-agent-to-group \ +ldh admin add agent \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ --agent "$AGENT_URI" \ diff --git a/http-tests/document-hierarchy/PATCH-remove-subresource.sh b/http-tests/document-hierarchy/PATCH-remove-subresource.sh index fb872f11e0..1e63e6fbf6 100755 --- a/http-tests/document-hierarchy/PATCH-remove-subresource.sh +++ b/http-tests/document-hierarchy/PATCH-remove-subresource.sh @@ -9,7 +9,7 @@ purge_cache "$FRONTEND_VARNISH_SERVICE" # add agent to the writers group -ldh admin acl add-agent-to-group \ +ldh admin add agent \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ --agent "$AGENT_URI" \ diff --git a/http-tests/document-hierarchy/PATCH-strips-rdf-type-422.sh b/http-tests/document-hierarchy/PATCH-strips-rdf-type-422.sh index b5c145cc7e..28fa250eed 100755 --- a/http-tests/document-hierarchy/PATCH-strips-rdf-type-422.sh +++ b/http-tests/document-hierarchy/PATCH-strips-rdf-type-422.sh @@ -9,7 +9,7 @@ purge_cache "$FRONTEND_VARNISH_SERVICE" # add agent to the writers group -ldh admin acl add-agent-to-group \ +ldh admin add agent \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ --agent "$AGENT_URI" \ diff --git a/http-tests/document-hierarchy/PATCH-swap-type.sh b/http-tests/document-hierarchy/PATCH-swap-type.sh index 320a8f34b9..bec5f4cf43 100755 --- a/http-tests/document-hierarchy/PATCH-swap-type.sh +++ b/http-tests/document-hierarchy/PATCH-swap-type.sh @@ -9,7 +9,7 @@ purge_cache "$FRONTEND_VARNISH_SERVICE" # add agent to the writers group -ldh admin acl add-agent-to-group \ +ldh admin add agent \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ --agent "$AGENT_URI" \ diff --git a/http-tests/document-hierarchy/PATCH.sh b/http-tests/document-hierarchy/PATCH.sh index 15fe0edc6e..88b64ea992 100755 --- a/http-tests/document-hierarchy/PATCH.sh +++ b/http-tests/document-hierarchy/PATCH.sh @@ -9,7 +9,7 @@ purge_cache "$FRONTEND_VARNISH_SERVICE" # add agent to the writers group -ldh admin acl add-agent-to-group \ +ldh admin add agent \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ --agent "$AGENT_URI" \ diff --git a/http-tests/document-hierarchy/POST-404.sh b/http-tests/document-hierarchy/POST-404.sh index b328a599fb..fc687d804b 100755 --- a/http-tests/document-hierarchy/POST-404.sh +++ b/http-tests/document-hierarchy/POST-404.sh @@ -9,7 +9,7 @@ purge_cache "$FRONTEND_VARNISH_SERVICE" # add agent to the writers -ldh admin acl add-agent-to-group \ +ldh admin add agent \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ --agent "$AGENT_URI" \ diff --git a/http-tests/document-hierarchy/POST-conditional-412.sh b/http-tests/document-hierarchy/POST-conditional-412.sh index e6d7d7a278..87b1108c25 100755 --- a/http-tests/document-hierarchy/POST-conditional-412.sh +++ b/http-tests/document-hierarchy/POST-conditional-412.sh @@ -9,7 +9,7 @@ purge_cache "$FRONTEND_VARNISH_SERVICE" # add agent to the writers group -ldh admin acl add-agent-to-group \ +ldh admin add agent \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ --agent "$AGENT_URI" \ diff --git a/http-tests/document-hierarchy/POST-conditional.sh b/http-tests/document-hierarchy/POST-conditional.sh index 1fa89bdee8..684afa4e84 100755 --- a/http-tests/document-hierarchy/POST-conditional.sh +++ b/http-tests/document-hierarchy/POST-conditional.sh @@ -9,7 +9,7 @@ purge_cache "$FRONTEND_VARNISH_SERVICE" # add agent to the writers group -ldh admin acl add-agent-to-group \ +ldh admin add agent \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ --agent "$AGENT_URI" \ diff --git a/http-tests/document-hierarchy/POST-html-jsonld.sh b/http-tests/document-hierarchy/POST-html-jsonld.sh index 6f9d480cad..f08bb4c33d 100755 --- a/http-tests/document-hierarchy/POST-html-jsonld.sh +++ b/http-tests/document-hierarchy/POST-html-jsonld.sh @@ -9,7 +9,7 @@ purge_cache "$FRONTEND_VARNISH_SERVICE" # add agent to the writers group -ldh admin acl add-agent-to-group \ +ldh admin add agent \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ --agent "$AGENT_URI" \ diff --git a/http-tests/document-hierarchy/POST-item-metadata.sh b/http-tests/document-hierarchy/POST-item-metadata.sh index 6bcc27f411..a775df1154 100755 --- a/http-tests/document-hierarchy/POST-item-metadata.sh +++ b/http-tests/document-hierarchy/POST-item-metadata.sh @@ -9,7 +9,7 @@ purge_cache "$FRONTEND_VARNISH_SERVICE" # add agent to the writers group -ldh admin acl add-agent-to-group \ +ldh admin add agent \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ --agent "$AGENT_URI" \ diff --git a/http-tests/document-hierarchy/POST-ntriples-etag.sh b/http-tests/document-hierarchy/POST-ntriples-etag.sh index 163654d128..39ceb9e952 100755 --- a/http-tests/document-hierarchy/POST-ntriples-etag.sh +++ b/http-tests/document-hierarchy/POST-ntriples-etag.sh @@ -9,7 +9,7 @@ purge_cache "$FRONTEND_VARNISH_SERVICE" # add agent to the writers group -ldh admin acl add-agent-to-group \ +ldh admin add agent \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ --agent "$AGENT_URI" \ diff --git a/http-tests/document-hierarchy/POST-ntriples-public-etag.sh b/http-tests/document-hierarchy/POST-ntriples-public-etag.sh index 2e8ea9768a..c7127fd4dd 100755 --- a/http-tests/document-hierarchy/POST-ntriples-public-etag.sh +++ b/http-tests/document-hierarchy/POST-ntriples-public-etag.sh @@ -9,7 +9,7 @@ purge_cache "$FRONTEND_VARNISH_SERVICE" # add agent to the writers group -ldh admin acl add-agent-to-group \ +ldh admin add agent \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ --agent "$AGENT_URI" \ @@ -17,7 +17,7 @@ ldh admin acl add-agent-to-group \ # create public authorization -ldh admin acl create-authorization \ +ldh admin create authorization \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ -b "$ADMIN_BASE_URL" \ diff --git a/http-tests/document-hierarchy/POST-ntriples.sh b/http-tests/document-hierarchy/POST-ntriples.sh index 3a91306b53..e4967cb1cd 100755 --- a/http-tests/document-hierarchy/POST-ntriples.sh +++ b/http-tests/document-hierarchy/POST-ntriples.sh @@ -9,7 +9,7 @@ purge_cache "$FRONTEND_VARNISH_SERVICE" # add agent to the writers group -ldh admin acl add-agent-to-group \ +ldh admin add agent \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ --agent "$AGENT_URI" \ diff --git a/http-tests/document-hierarchy/PUT-conditional-412.sh b/http-tests/document-hierarchy/PUT-conditional-412.sh index ca81070b3c..0a55f0fb98 100755 --- a/http-tests/document-hierarchy/PUT-conditional-412.sh +++ b/http-tests/document-hierarchy/PUT-conditional-412.sh @@ -9,7 +9,7 @@ purge_cache "$FRONTEND_VARNISH_SERVICE" # add agent to the writers group -ldh admin acl add-agent-to-group \ +ldh admin add agent \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ --agent "$AGENT_URI" \ diff --git a/http-tests/document-hierarchy/PUT-conditional.sh b/http-tests/document-hierarchy/PUT-conditional.sh index 36f4931387..9be74e0ce9 100755 --- a/http-tests/document-hierarchy/PUT-conditional.sh +++ b/http-tests/document-hierarchy/PUT-conditional.sh @@ -9,7 +9,7 @@ purge_cache "$FRONTEND_VARNISH_SERVICE" # add agent to the writers group -ldh admin acl add-agent-to-group \ +ldh admin add agent \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ --agent "$AGENT_URI" \ diff --git a/http-tests/document-hierarchy/PUT-content-blocks.sh b/http-tests/document-hierarchy/PUT-content-blocks.sh index e15909ede7..39408cb325 100755 --- a/http-tests/document-hierarchy/PUT-content-blocks.sh +++ b/http-tests/document-hierarchy/PUT-content-blocks.sh @@ -9,7 +9,7 @@ purge_cache "$FRONTEND_VARNISH_SERVICE" # add agent to the writers group -ldh admin acl add-agent-to-group \ +ldh admin add agent \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ --agent "$AGENT_URI" \ @@ -19,7 +19,7 @@ ldh admin acl add-agent-to-group \ slug=$(uuidgen | tr '[:upper:]' '[:lower:]') -item=$(ldh create-item \ +item=$(ldh create item \ -f "$AGENT_CERT_KEYSTORE" \ -p "$AGENT_CERT_PWD" \ -b "$END_USER_BASE_URL" \ diff --git a/http-tests/document-hierarchy/PUT-double-slash-uri-400.sh b/http-tests/document-hierarchy/PUT-double-slash-uri-400.sh index cb461b6dd8..9530a09a0d 100755 --- a/http-tests/document-hierarchy/PUT-double-slash-uri-400.sh +++ b/http-tests/document-hierarchy/PUT-double-slash-uri-400.sh @@ -9,7 +9,7 @@ purge_cache "$FRONTEND_VARNISH_SERVICE" # add agent to the writers group -ldh admin acl add-agent-to-group \ +ldh admin add agent \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ --agent "$AGENT_URI" \ @@ -18,7 +18,7 @@ ldh admin acl add-agent-to-group \ # create a container - IRIx resolves ".." on "new-item//" to "new-item/" (one segment per slash), # so the parent container must exist for authorization to pass and reach the // validation in put() -container=$(ldh create-container \ +container=$(ldh create container \ -f "$AGENT_CERT_KEYSTORE" \ -p "$AGENT_CERT_PWD" \ -b "$END_USER_BASE_URL" \ diff --git a/http-tests/document-hierarchy/PUT-empty.sh b/http-tests/document-hierarchy/PUT-empty.sh index 5651a9c74b..88c666597a 100755 --- a/http-tests/document-hierarchy/PUT-empty.sh +++ b/http-tests/document-hierarchy/PUT-empty.sh @@ -9,7 +9,7 @@ purge_cache "$FRONTEND_VARNISH_SERVICE" # add agent to the writers -ldh admin acl add-agent-to-group \ +ldh admin add agent \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ --agent "$AGENT_URI" \ diff --git a/http-tests/document-hierarchy/PUT-invalid-content-block-422.sh b/http-tests/document-hierarchy/PUT-invalid-content-block-422.sh index b0cf90848b..6fc01ff7c9 100755 --- a/http-tests/document-hierarchy/PUT-invalid-content-block-422.sh +++ b/http-tests/document-hierarchy/PUT-invalid-content-block-422.sh @@ -9,7 +9,7 @@ purge_cache "$FRONTEND_VARNISH_SERVICE" # add agent to the writers group -ldh admin acl add-agent-to-group \ +ldh admin add agent \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ --agent "$AGENT_URI" \ @@ -19,7 +19,7 @@ ldh admin acl add-agent-to-group \ slug=$(uuidgen | tr '[:upper:]' '[:lower:]') -item=$(ldh create-item \ +item=$(ldh create item \ -f "$AGENT_CERT_KEYSTORE" \ -p "$AGENT_CERT_PWD" \ -b "$END_USER_BASE_URL" \ diff --git a/http-tests/document-hierarchy/PUT-item-metadata.sh b/http-tests/document-hierarchy/PUT-item-metadata.sh index e012b47397..34543cb53f 100755 --- a/http-tests/document-hierarchy/PUT-item-metadata.sh +++ b/http-tests/document-hierarchy/PUT-item-metadata.sh @@ -9,7 +9,7 @@ purge_cache "$FRONTEND_VARNISH_SERVICE" # add agent to the writers group -ldh admin acl add-agent-to-group \ +ldh admin add agent \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ --agent "$AGENT_URI" \ diff --git a/http-tests/document-hierarchy/PUT-item-parent-403.sh b/http-tests/document-hierarchy/PUT-item-parent-403.sh index d9ce58f83d..bb1fa2f9f9 100755 --- a/http-tests/document-hierarchy/PUT-item-parent-403.sh +++ b/http-tests/document-hierarchy/PUT-item-parent-403.sh @@ -9,7 +9,7 @@ purge_cache "$FRONTEND_VARNISH_SERVICE" # add agent to the writers -ldh admin acl add-agent-to-group \ +ldh admin add agent \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ --agent "$AGENT_URI" \ @@ -19,7 +19,7 @@ ldh admin acl add-agent-to-group \ slug="test-item" -item=$(ldh create-item \ +item=$(ldh create item \ -f "$AGENT_CERT_KEYSTORE" \ -p "$AGENT_CERT_PWD" \ -b "$END_USER_BASE_URL" \ diff --git a/http-tests/document-hierarchy/PUT-location.sh b/http-tests/document-hierarchy/PUT-location.sh index dadb8f8f95..9ccf166922 100755 --- a/http-tests/document-hierarchy/PUT-location.sh +++ b/http-tests/document-hierarchy/PUT-location.sh @@ -9,7 +9,7 @@ purge_cache "$FRONTEND_VARNISH_SERVICE" # add agent to the writers group -ldh admin acl add-agent-to-group \ +ldh admin add agent \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ --agent "$AGENT_URI" \ diff --git a/http-tests/document-hierarchy/PUT-no-parent-403.sh b/http-tests/document-hierarchy/PUT-no-parent-403.sh index b6494eb06d..4bb30da09b 100755 --- a/http-tests/document-hierarchy/PUT-no-parent-403.sh +++ b/http-tests/document-hierarchy/PUT-no-parent-403.sh @@ -9,7 +9,7 @@ purge_cache "$FRONTEND_VARNISH_SERVICE" # add agent to the writers -ldh admin acl add-agent-to-group \ +ldh admin add agent \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ --agent "$AGENT_URI" \ diff --git a/http-tests/document-hierarchy/PUT-no-slash-308.sh b/http-tests/document-hierarchy/PUT-no-slash-308.sh index af0326de48..e9bb6ad15e 100755 --- a/http-tests/document-hierarchy/PUT-no-slash-308.sh +++ b/http-tests/document-hierarchy/PUT-no-slash-308.sh @@ -9,7 +9,7 @@ purge_cache "$FRONTEND_VARNISH_SERVICE" # add agent to the writers group -ldh admin acl add-agent-to-group \ +ldh admin add agent \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ --agent "$AGENT_URI" \ @@ -17,7 +17,7 @@ ldh admin acl add-agent-to-group \ # add an explicit read/write authorization for the parent since the child document will inherit it -ldh admin acl create-authorization \ +ldh admin create authorization \ -b "$ADMIN_BASE_URL" \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ diff --git a/http-tests/document-hierarchy/PUT-ntriples-etag.sh b/http-tests/document-hierarchy/PUT-ntriples-etag.sh index 4b7732dcf1..0400a459e4 100755 --- a/http-tests/document-hierarchy/PUT-ntriples-etag.sh +++ b/http-tests/document-hierarchy/PUT-ntriples-etag.sh @@ -9,7 +9,7 @@ purge_cache "$FRONTEND_VARNISH_SERVICE" # add agent to the writers group -ldh admin acl add-agent-to-group \ +ldh admin add agent \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ --agent "$AGENT_URI" \ diff --git a/http-tests/document-hierarchy/PUT-ntriples-public-etag.sh b/http-tests/document-hierarchy/PUT-ntriples-public-etag.sh index ff30219b84..2bc3db7781 100755 --- a/http-tests/document-hierarchy/PUT-ntriples-public-etag.sh +++ b/http-tests/document-hierarchy/PUT-ntriples-public-etag.sh @@ -9,7 +9,7 @@ purge_cache "$FRONTEND_VARNISH_SERVICE" # add agent to the writers group -ldh admin acl add-agent-to-group \ +ldh admin add agent \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ --agent "$AGENT_URI" \ @@ -17,7 +17,7 @@ ldh admin acl add-agent-to-group \ # create public authorization -ldh admin acl create-authorization \ +ldh admin create authorization \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ -b "$ADMIN_BASE_URL" \ diff --git a/http-tests/document-hierarchy/PUT-ntriples.sh b/http-tests/document-hierarchy/PUT-ntriples.sh index c2e3384386..5f36b9fdca 100755 --- a/http-tests/document-hierarchy/PUT-ntriples.sh +++ b/http-tests/document-hierarchy/PUT-ntriples.sh @@ -9,7 +9,7 @@ purge_cache "$FRONTEND_VARNISH_SERVICE" # add agent to the writers group -ldh admin acl add-agent-to-group \ +ldh admin add agent \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ --agent "$AGENT_URI" \ diff --git a/http-tests/document-hierarchy/PUT-orphan-bnode-object-skolemized.sh b/http-tests/document-hierarchy/PUT-orphan-bnode-object-skolemized.sh index f4b7f48cff..a0fcb78460 100755 --- a/http-tests/document-hierarchy/PUT-orphan-bnode-object-skolemized.sh +++ b/http-tests/document-hierarchy/PUT-orphan-bnode-object-skolemized.sh @@ -9,7 +9,7 @@ purge_cache "$FRONTEND_VARNISH_SERVICE" # add agent to the writers group -ldh admin acl add-agent-to-group \ +ldh admin add agent \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ --agent "$AGENT_URI" \ @@ -19,7 +19,7 @@ ldh admin acl add-agent-to-group \ slug=$(uuidgen | tr '[:upper:]' '[:lower:]') -container=$(ldh create-container \ +container=$(ldh create container \ -f "$AGENT_CERT_KEYSTORE" \ -p "$AGENT_CERT_PWD" \ -b "$END_USER_BASE_URL" \ diff --git a/http-tests/document-hierarchy/PUT-owner-405.sh b/http-tests/document-hierarchy/PUT-owner-405.sh index 39a25a0803..7baab95226 100755 --- a/http-tests/document-hierarchy/PUT-owner-405.sh +++ b/http-tests/document-hierarchy/PUT-owner-405.sh @@ -9,7 +9,7 @@ purge_cache "$FRONTEND_VARNISH_SERVICE" # add agent to the owners -ldh admin acl add-agent-to-group \ +ldh admin add agent \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ --agent "$AGENT_URI" \ diff --git a/http-tests/document-hierarchy/PUT-relative-uri-ntriples-400.sh b/http-tests/document-hierarchy/PUT-relative-uri-ntriples-400.sh index 3e8409d2b0..408a850504 100755 --- a/http-tests/document-hierarchy/PUT-relative-uri-ntriples-400.sh +++ b/http-tests/document-hierarchy/PUT-relative-uri-ntriples-400.sh @@ -9,7 +9,7 @@ purge_cache "$FRONTEND_VARNISH_SERVICE" # add agent to the writers group -ldh admin acl add-agent-to-group \ +ldh admin add agent \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ --agent "$AGENT_URI" \ diff --git a/http-tests/document-hierarchy/PUT-relative-uri-turtle.sh b/http-tests/document-hierarchy/PUT-relative-uri-turtle.sh index a01570203d..9d5b8a7974 100755 --- a/http-tests/document-hierarchy/PUT-relative-uri-turtle.sh +++ b/http-tests/document-hierarchy/PUT-relative-uri-turtle.sh @@ -9,7 +9,7 @@ purge_cache "$FRONTEND_VARNISH_SERVICE" # add agent to the writers group -ldh admin acl add-agent-to-group \ +ldh admin add agent \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ --agent "$AGENT_URI" \ @@ -32,7 +32,6 @@ EOF ) if [ "$status" != "$STATUS_CREATED" ]; then - echo "DEBUG: Expected $STATUS_CREATED from the PUT, got: $status" exit 1 fi @@ -52,8 +51,6 @@ for triple in \ "<${item}named-subject-put> \"named object PUT\" ." do if ! grep -qF "$triple" <<< "$response"; then - echo "DEBUG: Expected triple: $triple" - echo "DEBUG: Got: $response" exit 1 fi done diff --git a/http-tests/document-hierarchy/PUT-secretary-405.sh b/http-tests/document-hierarchy/PUT-secretary-405.sh index c015c64ea8..4f0c7c2b42 100755 --- a/http-tests/document-hierarchy/PUT-secretary-405.sh +++ b/http-tests/document-hierarchy/PUT-secretary-405.sh @@ -9,7 +9,7 @@ purge_cache "$FRONTEND_VARNISH_SERVICE" # add agent to the owners -ldh admin acl add-agent-to-group \ +ldh admin add agent \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ --agent "$AGENT_URI" \ diff --git a/http-tests/document-hierarchy/PUT-twice.sh b/http-tests/document-hierarchy/PUT-twice.sh index c58e46e8d3..910285ae96 100755 --- a/http-tests/document-hierarchy/PUT-twice.sh +++ b/http-tests/document-hierarchy/PUT-twice.sh @@ -9,7 +9,7 @@ purge_cache "$FRONTEND_VARNISH_SERVICE" # add agent to the writers group -ldh admin acl add-agent-to-group \ +ldh admin add agent \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ --agent "$AGENT_URI" \ diff --git a/http-tests/document-hierarchy/create-container.sh b/http-tests/document-hierarchy/create-container.sh index 16af5be945..42e4bee135 100755 --- a/http-tests/document-hierarchy/create-container.sh +++ b/http-tests/document-hierarchy/create-container.sh @@ -9,7 +9,7 @@ purge_cache "$FRONTEND_VARNISH_SERVICE" # add agent to the writers group -ldh admin acl add-agent-to-group \ +ldh admin add agent \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ --agent "$AGENT_URI" \ @@ -19,7 +19,7 @@ ldh admin acl add-agent-to-group \ slug="test" -container=$(ldh create-container \ +container=$(ldh create container \ -f "$AGENT_CERT_KEYSTORE" \ -p "$AGENT_CERT_PWD" \ -b "$END_USER_BASE_URL" \ diff --git a/http-tests/document-hierarchy/create-item.sh b/http-tests/document-hierarchy/create-item.sh index 07c36767d4..4de1ef134a 100755 --- a/http-tests/document-hierarchy/create-item.sh +++ b/http-tests/document-hierarchy/create-item.sh @@ -9,7 +9,7 @@ purge_cache "$FRONTEND_VARNISH_SERVICE" # add agent to the writers group -ldh admin acl add-agent-to-group \ +ldh admin add agent \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ --agent "$AGENT_URI" \ @@ -19,7 +19,7 @@ ldh admin acl add-agent-to-group \ slug="test-item" -item=$(ldh create-item \ +item=$(ldh create item \ -f "$AGENT_CERT_KEYSTORE" \ -p "$AGENT_CERT_PWD" \ -b "$END_USER_BASE_URL" \ diff --git a/http-tests/document-hierarchy/owner-container.sh b/http-tests/document-hierarchy/owner-container.sh index 4e3df66f80..b76add519c 100755 --- a/http-tests/document-hierarchy/owner-container.sh +++ b/http-tests/document-hierarchy/owner-container.sh @@ -9,7 +9,7 @@ purge_cache "$FRONTEND_VARNISH_SERVICE" # add agent to the writers group -ldh admin acl add-agent-to-group \ +ldh admin add agent \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ --agent "$AGENT_URI" \ @@ -19,7 +19,7 @@ ldh admin acl add-agent-to-group \ slug="test" -container=$(ldh create-container \ +container=$(ldh create container \ -f "$AGENT_CERT_KEYSTORE" \ -p "$AGENT_CERT_PWD" \ -b "$END_USER_BASE_URL" \ diff --git a/http-tests/document-hierarchy/owner-item.sh b/http-tests/document-hierarchy/owner-item.sh index bc718f18de..d656af7ebc 100755 --- a/http-tests/document-hierarchy/owner-item.sh +++ b/http-tests/document-hierarchy/owner-item.sh @@ -9,7 +9,7 @@ purge_cache "$FRONTEND_VARNISH_SERVICE" # add agent to the writers group -ldh admin acl add-agent-to-group \ +ldh admin add agent \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ --agent "$AGENT_URI" \ @@ -19,7 +19,7 @@ ldh admin acl add-agent-to-group \ slug="test-item" -item=$(ldh create-item \ +item=$(ldh create item \ -f "$AGENT_CERT_KEYSTORE" \ -p "$AGENT_CERT_PWD" \ -b "$END_USER_BASE_URL" \ diff --git a/http-tests/federation/GET-remote-document-links.sh b/http-tests/federation/GET-remote-document-links.sh index 0417656a84..2b6327526f 100755 --- a/http-tests/federation/GET-remote-document-links.sh +++ b/http-tests/federation/GET-remote-document-links.sh @@ -44,8 +44,6 @@ direct_etag=$(curl -k -f -s -I \ "$remote_base" \ | grep -i '^etag:' | tr -d '\r' | awk '{print $2}') -echo "DEBUG: proxied ETag: $proxied_etag direct ETag: $direct_etag" if [ -z "$proxied_etag" ] || [ "$proxied_etag" != "$direct_etag" ]; then - echo "DEBUG: proxied ETag does not match the origin's ETag" exit 1 fi diff --git a/http-tests/federation/PATCH-remote-document-unauthorized.sh b/http-tests/federation/PATCH-remote-document-unauthorized.sh index 3516cca1dc..7eaaa7855e 100755 --- a/http-tests/federation/PATCH-remote-document-unauthorized.sh +++ b/http-tests/federation/PATCH-remote-document-unauthorized.sh @@ -18,7 +18,7 @@ remote_base="https://test.localhost:4443/" # create the target on B as the owner (authorized), so only the *writer* differs from the # positive test -item=$(ldh create-item \ +item=$(ldh create item \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ -b "$remote_base" \ @@ -48,9 +48,7 @@ code=$(curl -k -w "%{http_code}" -o /dev/null -s \ --data-binary "$update" \ "$END_USER_BASE_URL") -echo "DEBUG: unauthorized cross-instance PATCH returned: $code" if ! echo "$code" | grep -qE "^($STATUS_UNAUTHORIZED|$STATUS_FORBIDDEN)$"; then - echo "DEBUG: expected 401 or 403 for the unauthorized delegated write, got: $code" exit 1 fi @@ -61,6 +59,5 @@ if curl -k -f -s \ -H "Accept: application/n-triples" \ "$item" \ | grep -q "Should not land"; then - echo "DEBUG: unauthorized delta landed on the remote document" exit 1 fi diff --git a/http-tests/federation/PATCH-remote-document.sh b/http-tests/federation/PATCH-remote-document.sh index c3d2d89338..a59cd16384 100755 --- a/http-tests/federation/PATCH-remote-document.sh +++ b/http-tests/federation/PATCH-remote-document.sh @@ -17,7 +17,7 @@ remote_base="https://test.localhost:4443/" # create the document on B (the owner is authorized on both dataspaces in the test setup) -item=$(ldh create-item \ +item=$(ldh create item \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ -b "$remote_base" \ @@ -35,9 +35,7 @@ etag=$(curl -k -f -s -o /dev/null -D - \ "$END_USER_BASE_URL" \ | grep -i '^etag:' | tr -d '\r' | awk '{print $2}') -echo "DEBUG: ETag for If-Match: $etag" if [ -z "$etag" ]; then - echo "DEBUG: no ETag on the proxied response" exit 1 fi @@ -66,7 +64,6 @@ stale_code=$(curl -k -w "%{http_code}" -o /dev/null -s \ --data-binary "$update" \ "$END_USER_BASE_URL") -echo "DEBUG: stale If-Match returned: $stale_code (expected $STATUS_PRECONDITION_FAILED)" if [ "$stale_code" != "$STATUS_PRECONDITION_FAILED" ]; then exit 1 fi @@ -83,7 +80,6 @@ valid_code=$(curl -k -w "%{http_code}" -o /dev/null -s \ --data-binary "$update" \ "$END_USER_BASE_URL") -echo "DEBUG: valid If-Match returned: $valid_code (expected $STATUS_NO_CONTENT)" if [ "$valid_code" != "$STATUS_NO_CONTENT" ]; then exit 1 fi diff --git a/http-tests/federation/POST-remote-endpoint-query.sh b/http-tests/federation/POST-remote-endpoint-query.sh index c057a4b32c..12d1f980f2 100755 --- a/http-tests/federation/POST-remote-endpoint-query.sh +++ b/http-tests/federation/POST-remote-endpoint-query.sh @@ -25,9 +25,7 @@ curl -k -f -s -o /dev/null -D "$headers" \ endpoint=$(grep -i '^link:' "$headers" | tr ',' '\n' | grep 'sparql-service-description#endpoint' | sed 's/.*<\([^>]*\)>.*/\1/') -echo "DEBUG: discovered endpoint: $endpoint" if [ -z "$endpoint" ]; then - echo "DEBUG: no sd:endpoint Link header forwarded" exit 1 fi @@ -46,6 +44,5 @@ count=$(curl -k -f -s \ | xmllint --xpath "count(//*[local-name() = 'result'])" -) if [ "$count" != "1" ]; then - echo "DEBUG: Expected 1 result from the discovered remote endpoint, got: $count" exit 1 fi diff --git a/http-tests/federation/POST-remote-ns-constructors.sh b/http-tests/federation/POST-remote-ns-constructors.sh index c15ece2448..6e8d7753da 100755 --- a/http-tests/federation/POST-remote-ns-constructors.sh +++ b/http-tests/federation/POST-remote-ns-constructors.sh @@ -32,6 +32,5 @@ echo "$results" | grep -q "https://w3id.org/atomgraph/linkeddatahub#TitleConstru count=$(echo "$results" | xmllint --xpath "count(//*[local-name() = 'binding'][@name = 'text']/*[local-name() = 'literal'][contains(., 'CONSTRUCT')])" -) if [ "$count" -lt 1 ]; then - echo "DEBUG: Expected at least 1 constructor text from the remote ns, got: $count" exit 1 fi diff --git a/http-tests/imports/GET-file-404.sh b/http-tests/imports/GET-file-404.sh index 88bea1336f..177d37f913 100755 --- a/http-tests/imports/GET-file-404.sh +++ b/http-tests/imports/GET-file-404.sh @@ -8,7 +8,7 @@ purge_cache "$ADMIN_VARNISH_SERVICE" purge_cache "$FRONTEND_VARNISH_SERVICE" # add agent to the writers group -ldh admin acl add-agent-to-group \ +ldh admin add agent \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ --agent "$AGENT_URI" \ diff --git a/http-tests/imports/GET-file-range.sh b/http-tests/imports/GET-file-range.sh index 954c6fb8ff..d810aaea3d 100755 --- a/http-tests/imports/GET-file-range.sh +++ b/http-tests/imports/GET-file-range.sh @@ -11,7 +11,7 @@ pwd=$(realpath "$PWD") # add agent to the writers group -ldh admin acl add-agent-to-group \ +ldh admin add agent \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ --agent "$AGENT_URI" \ @@ -25,7 +25,7 @@ file_content_type="application/octet-stream" slug=$(uuidgen | tr '[:upper:]' '[:lower:]') # Create an item document to hold the file -file_doc=$(ldh create-item \ +file_doc=$(ldh create item \ -f "$AGENT_CERT_KEYSTORE" \ -p "$AGENT_CERT_PWD" \ -b "$END_USER_BASE_URL" \ @@ -34,7 +34,7 @@ file_doc=$(ldh create-item \ --slug "$slug") # Add the file to the document -ldh add-file \ +ldh add file \ -f "$AGENT_CERT_KEYSTORE" \ -p "$AGENT_CERT_PWD" \ -b "$END_USER_BASE_URL" \ diff --git a/http-tests/imports/GET-file-sha1sum.sh b/http-tests/imports/GET-file-sha1sum.sh index 5f52a76506..34772f10ef 100755 --- a/http-tests/imports/GET-file-sha1sum.sh +++ b/http-tests/imports/GET-file-sha1sum.sh @@ -11,7 +11,7 @@ pwd=$(realpath "$PWD") # add agent to the writers group -ldh admin acl add-agent-to-group \ +ldh admin add agent \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ --agent "$AGENT_URI" \ @@ -24,7 +24,7 @@ time dd if=/dev/urandom of="$filename" bs=1 count=1024 file_content_type="application/octet-stream" # Create a container for files first -ldh create-container \ +ldh create container \ -f "$AGENT_CERT_KEYSTORE" \ -p "$AGENT_CERT_PWD" \ -b "$END_USER_BASE_URL" \ @@ -33,7 +33,7 @@ ldh create-container \ --slug "files" # Create an item document to hold the file -file_doc=$(ldh create-item \ +file_doc=$(ldh create item \ -f "$AGENT_CERT_KEYSTORE" \ -p "$AGENT_CERT_PWD" \ -b "$END_USER_BASE_URL" \ @@ -41,7 +41,7 @@ file_doc=$(ldh create-item \ --container "${END_USER_BASE_URL}files/") # Add the file to the document -ldh add-file \ +ldh add file \ -f "$AGENT_CERT_KEYSTORE" \ -p "$AGENT_CERT_PWD" \ -b "$END_USER_BASE_URL" \ diff --git a/http-tests/imports/GET-file-utf8-charset.sh b/http-tests/imports/GET-file-utf8-charset.sh index d7b40159d8..b31253c546 100755 --- a/http-tests/imports/GET-file-utf8-charset.sh +++ b/http-tests/imports/GET-file-utf8-charset.sh @@ -11,7 +11,7 @@ pwd=$(realpath "$PWD") # add agent to the writers group -ldh admin acl add-agent-to-group \ +ldh admin add agent \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ --agent "$AGENT_URI" \ @@ -33,7 +33,7 @@ EOF file_content_type="text/markdown" # Create a container for files first -ldh create-container \ +ldh create container \ -f "$AGENT_CERT_KEYSTORE" \ -p "$AGENT_CERT_PWD" \ -b "$END_USER_BASE_URL" \ @@ -42,7 +42,7 @@ ldh create-container \ --slug "files" # Create an item document to hold the file -file_doc=$(ldh create-item \ +file_doc=$(ldh create item \ -f "$AGENT_CERT_KEYSTORE" \ -p "$AGENT_CERT_PWD" \ -b "$END_USER_BASE_URL" \ @@ -50,7 +50,7 @@ file_doc=$(ldh create-item \ --container "${END_USER_BASE_URL}files/") # Add the file to the document -ldh add-file \ +ldh add file \ -f "$AGENT_CERT_KEYSTORE" \ -p "$AGENT_CERT_PWD" \ -b "$END_USER_BASE_URL" \ diff --git a/http-tests/imports/PUT-file-format-explicit.sh b/http-tests/imports/PUT-file-format-explicit.sh index a90ab21895..25c6576449 100755 --- a/http-tests/imports/PUT-file-format-explicit.sh +++ b/http-tests/imports/PUT-file-format-explicit.sh @@ -11,7 +11,7 @@ pwd=$(realpath "$PWD") # add agent to the writers group -ldh admin acl add-agent-to-group \ +ldh admin add agent \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ --agent "$AGENT_URI" \ @@ -27,7 +27,7 @@ echo "4,5,6" >> "$test_file" slug=$(uuidgen | tr '[:upper:]' '[:lower:]') # Create an item document to hold the file -file_doc=$(ldh create-item \ +file_doc=$(ldh create item \ -f "$AGENT_CERT_KEYSTORE" \ -p "$AGENT_CERT_PWD" \ -b "$END_USER_BASE_URL" \ @@ -36,7 +36,7 @@ file_doc=$(ldh create-item \ --slug "$slug") # upload file with explicit media type: text/plain -ldh add-file \ +ldh add file \ -f "$AGENT_CERT_KEYSTORE" \ -p "$AGENT_CERT_PWD" \ -b "$END_USER_BASE_URL" \ @@ -72,7 +72,7 @@ fi # re-upload the same file but different explicit media type: text/csv # this simulates editing the file document through the UI and uploading a new file -ldh add-file \ +ldh add file \ -f "$AGENT_CERT_KEYSTORE" \ -p "$AGENT_CERT_PWD" \ -b "$END_USER_BASE_URL" \ diff --git a/http-tests/imports/PUT-file-format.sh b/http-tests/imports/PUT-file-format.sh index f07859a506..87df855258 100755 --- a/http-tests/imports/PUT-file-format.sh +++ b/http-tests/imports/PUT-file-format.sh @@ -11,7 +11,7 @@ pwd=$(realpath "$PWD") # add agent to the writers group -ldh admin acl add-agent-to-group \ +ldh admin add agent \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ --agent "$AGENT_URI" \ @@ -27,7 +27,7 @@ echo "4,5,6" >> "$test_file" slug=$(uuidgen | tr '[:upper:]' '[:lower:]') # Create an item document to hold the file -file_doc=$(ldh create-item \ +file_doc=$(ldh create item \ -f "$AGENT_CERT_KEYSTORE" \ -p "$AGENT_CERT_PWD" \ -b "$END_USER_BASE_URL" \ @@ -36,7 +36,7 @@ file_doc=$(ldh create-item \ --slug "$slug") # upload file WITHOUT explicit media type (rely on browser detection via `file -b --mime-type`) -ldh add-file \ +ldh add file \ -f "$AGENT_CERT_KEYSTORE" \ -p "$AGENT_CERT_PWD" \ -b "$END_USER_BASE_URL" \ @@ -65,7 +65,7 @@ initial_format=$(echo "$file_doc_ntriples" | sed -rn "s/<${file_uri//\//\\/}> &2 -echo "DEBUG: Got: $file" >&2 [ "$file" = "${END_USER_BASE_URL}uploads/${sha1sum}" ] echo "$file" # file URL used in other tests diff --git a/http-tests/imports/import-csv.sh b/http-tests/imports/import-csv.sh index 275e191cd2..33909c1aec 100755 --- a/http-tests/imports/import-csv.sh +++ b/http-tests/imports/import-csv.sh @@ -11,7 +11,7 @@ pwd=$(realpath "$PWD") # add agent to the writers group -ldh admin acl add-agent-to-group \ +ldh admin add agent \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ --agent "$AGENT_URI" \ @@ -19,7 +19,7 @@ ldh admin acl add-agent-to-group \ # create import item -item=$(ldh create-item \ +item=$(ldh create item \ -f "$AGENT_CERT_KEYSTORE" \ -p "$AGENT_CERT_PWD" \ -b "$END_USER_BASE_URL" \ @@ -28,7 +28,7 @@ item=$(ldh create-item \ # create target container -container=$(ldh create-container \ +container=$(ldh create container \ -f "$AGENT_CERT_KEYSTORE" \ -p "$AGENT_CERT_PWD" \ -b "$END_USER_BASE_URL" \ @@ -38,7 +38,7 @@ container=$(ldh create-container \ # import CSV -ldh imports import-csv \ +ldh import csv \ -f "$AGENT_CERT_KEYSTORE" \ -p "$AGENT_CERT_PWD" \ -b "$END_USER_BASE_URL" \ diff --git a/http-tests/imports/import-rdf-no-query.sh b/http-tests/imports/import-rdf-no-query.sh index 32abd5e495..647065df0e 100755 --- a/http-tests/imports/import-rdf-no-query.sh +++ b/http-tests/imports/import-rdf-no-query.sh @@ -11,7 +11,7 @@ pwd=$(realpath "$PWD") # add agent to the writers group -ldh admin acl add-agent-to-group \ +ldh admin add agent \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ --agent "$AGENT_URI" \ @@ -19,7 +19,7 @@ ldh admin acl add-agent-to-group \ # create import item -item=$(ldh create-item \ +item=$(ldh create item \ -f "$AGENT_CERT_KEYSTORE" \ -p "$AGENT_CERT_PWD" \ -b "$END_USER_BASE_URL" \ @@ -28,7 +28,7 @@ item=$(ldh create-item \ # create target item -graph=$(ldh create-item \ +graph=$(ldh create item \ -f "$AGENT_CERT_KEYSTORE" \ -p "$AGENT_CERT_PWD" \ -b "$END_USER_BASE_URL" \ @@ -38,7 +38,7 @@ graph=$(ldh create-item \ # import RDF -ldh imports import-rdf \ +ldh import rdf \ -f "$AGENT_CERT_KEYSTORE" \ -p "$AGENT_CERT_PWD" \ -b "$END_USER_BASE_URL" \ diff --git a/http-tests/imports/import-rdf.sh b/http-tests/imports/import-rdf.sh index f66a3af38e..a4b7cd04df 100755 --- a/http-tests/imports/import-rdf.sh +++ b/http-tests/imports/import-rdf.sh @@ -11,7 +11,7 @@ pwd=$(realpath "$PWD") # add agent to the writers group -ldh admin acl add-agent-to-group \ +ldh admin add agent \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ --agent "$AGENT_URI" \ @@ -19,7 +19,7 @@ ldh admin acl add-agent-to-group \ # create import item -item=$(ldh create-item \ +item=$(ldh create item \ -f "$AGENT_CERT_KEYSTORE" \ -p "$AGENT_CERT_PWD" \ -b "$END_USER_BASE_URL" \ @@ -28,7 +28,7 @@ item=$(ldh create-item \ # create target container -container=$(ldh create-container \ +container=$(ldh create container \ -f "$AGENT_CERT_KEYSTORE" \ -p "$AGENT_CERT_PWD" \ -b "$END_USER_BASE_URL" \ @@ -38,7 +38,7 @@ container=$(ldh create-container \ # import RDF -ldh imports import-rdf \ +ldh import rdf \ -f "$AGENT_CERT_KEYSTORE" \ -p "$AGENT_CERT_PWD" \ -b "$END_USER_BASE_URL" \ diff --git a/http-tests/language/content-language-by-reader.sh b/http-tests/language/content-language-by-reader.sh new file mode 100755 index 0000000000..b8c2c84aef --- /dev/null +++ b/http-tests/language/content-language-by-reader.sh @@ -0,0 +1,65 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Content-Language names the language the page was composed in, which is the first of the reader's accepted languages that +# the UI translation bundle actually has - not the reader's top preference. The bundle ships English and Spanish, so a +# Lithuanian or German reader gets an honest en rather than their own request echoed back at them. What is published is the +# shortest tag the bundle justifies: its keys are en-US and es-ES, but neither region distinguishes anything. + +purge_cache "$END_USER_VARNISH_SERVICE" +purge_cache "$FRONTEND_VARNISH_SERVICE" + +response() +{ + curl -i -k -s \ + -E "$OWNER_CERT_FILE":"$OWNER_CERT_PWD" \ + -H "Accept: text/html" \ + "$@" \ + "$END_USER_BASE_URL" \ + | tr -d '\r' +} + +assert_language() +{ + local accept_language="$1" + local expected="$2" + local response actual + + if [ -n "$accept_language" ]; then + response=$(response -H "Accept-Language: $accept_language") + else + response=$(response) + fi + + # an error page is rendered through the same stylesheet and carries a Content-Language of its own, so every probe + # confirms this is the document rather than a failure that happens to be labelled + echo "$response" | grep -qE "^HTTP/[0-9.]+ 200" + + actual=$(echo "$response" | grep -i "^Content-Language:" | sed 's/^Content-Language: *//i' || true) + + [ "$actual" = "$expected" ] +} + +# the bundle keys are region-qualified while browsers send primary subtags, so matching is RFC 4647 Basic +# Filtering - but the published tag drops a region that distinguishes nothing, per BCP 47 + +assert_language "en-US,en;q=0.9,da;q=0.8,lt;q=0.7" "en" +assert_language "en" "en" +assert_language "es" "es" + +# preference order decides between two languages the bundle has, rather than bundle order + +assert_language "es,en;q=0.9" "es" +assert_language "en,es;q=0.9" "en" + +# the bundle has no Lithuanian, so the page is composed in the next language it does have + +assert_language "lt,en-US;q=0.9,en;q=0.8" "en" + +# none of the reader's languages are available: fall back rather than claim a language the page is not in + +assert_language "de,fr;q=0.9" "en" + +# no preference expressed at all + +assert_language "" "en" diff --git a/http-tests/language/content-language-media-type.sh b/http-tests/language/content-language-media-type.sh new file mode 100755 index 0000000000..3dc3cde3f3 --- /dev/null +++ b/http-tests/language/content-language-media-type.sh @@ -0,0 +1,55 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Only a representation whose rendering depends on language gets labelled with one. An RDF representation is byte-identical +# for every reader - its literals carry their own tags and none is dropped - so it is intended for all language audiences, +# which RFC 9110 spells as no Content-Language at all. + +purge_cache "$END_USER_VARNISH_SERVICE" +purge_cache "$FRONTEND_VARNISH_SERVICE" + +headers() +{ + curl -i -k -s \ + -E "$OWNER_CERT_FILE":"$OWNER_CERT_PWD" \ + -H "Accept: $1" \ + -H "Accept-Language: en" \ + "$END_USER_BASE_URL" \ + | tr -d '\r' +} + +assert_labelled() +{ + local response actual + + response=$(headers "$1") + + # an error page is rendered through the same stylesheet and carries a Content-Language of its own, so every probe + # confirms this is the representation rather than a failure that happens to be labelled + echo "$response" | grep -qE "^HTTP/[0-9.]+ 200" + + actual=$(echo "$response" | grep -i "^Content-Language:" | sed 's/^Content-Language: *//i' || true) + + [ "$actual" = "en" ] +} + +assert_unlabelled() +{ + local response + + response=$(headers "$1") + + echo "$response" | grep -qE "^HTTP/[0-9.]+ 200" + + ! echo "$response" | grep -qi "^Content-Language:" +} + +# both HTML flavours are rendered through the translation bundle and so are composed in one language + +assert_labelled "text/html" +assert_labelled "application/xhtml+xml" + +assert_unlabelled "application/rdf+xml" +assert_unlabelled "text/turtle" +assert_unlabelled "application/n-triples" +assert_unlabelled "application/ld+json" diff --git a/http-tests/language/etag-varies-by-language.sh b/http-tests/language/etag-varies-by-language.sh new file mode 100755 index 0000000000..e29c4c3a89 --- /dev/null +++ b/http-tests/language/etag-varies-by-language.sh @@ -0,0 +1,53 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Two readers get two different HTML representations of the same document, so the two must not share an entity tag: a +# Spanish reader arriving with the English reader's tag would be told 304 and shown a page in a language they never asked +# for. The RDF representation is the same bytes for both, so its tag has to stay stable across them. + +purge_cache "$END_USER_VARNISH_SERVICE" +purge_cache "$FRONTEND_VARNISH_SERVICE" + +etag() +{ + curl -i -k -s \ + -E "$OWNER_CERT_FILE":"$OWNER_CERT_PWD" \ + -H "Accept: $1" \ + -H "Accept-Language: $2" \ + "$END_USER_BASE_URL" \ + | tr -d '\r' \ + | grep -i "^ETag:" \ + | sed 's/^ETag: *//i' || true +} + +html_en=$(etag "text/html" "en") +html_es=$(etag "text/html" "es") + +[ -n "$html_en" ] + +# two languages must not share one entity tag, or a conditional request would serve the wrong language + +[ "$html_en" != "$html_es" ] + +# the tag is still stable for one reader, otherwise conditional requests never revalidate + +[ "$html_en" = "$(etag "text/html" "en")" ] + +status() +{ + curl -k -w "%{http_code}" -o /dev/null -s \ + -E "$OWNER_CERT_FILE":"$OWNER_CERT_PWD" \ + -H "Accept: text/html" \ + -H "Accept-Language: $1" \ + -H "If-None-Match: $2" \ + "$END_USER_BASE_URL" +} + +# the reader's own tag revalidates, another reader's does not + +[ "$(status "en" "$html_en")" = "$STATUS_NOT_MODIFIED" ] +[ "$(status "es" "$html_en")" = "$STATUS_OK" ] + +# RDF carries every language at once, so language is not part of its identity + +[ "$(etag "application/n-triples" "en")" = "$(etag "application/n-triples" "es")" ] diff --git a/http-tests/language/html-lang-matches-content-language.sh b/http-tests/language/html-lang-matches-content-language.sh new file mode 100755 index 0000000000..6db397dbe9 --- /dev/null +++ b/http-tests/language/html-lang-matches-content-language.sh @@ -0,0 +1,45 @@ +#!/usr/bin/env bash +set -euo pipefail + +# The header and the document have to agree about what language the page is in. They do because both read the same +# computation: the stylesheet takes the root lang straight from Content-Language rather than from the request. A reader whose +# language the bundle lacks is the case that used to disagree - the header said nothing while the document claimed lt. + +purge_cache "$END_USER_VARNISH_SERVICE" +purge_cache "$FRONTEND_VARNISH_SERVICE" + +assert_agrees() +{ + local expected="$2" + local headers body header document + + headers=$(mktemp) + + # XHTML is the same rendering as text/html but well-formed, so the document can be queried with XPath rather than + # matched as text - attribute order is a serialization detail and nothing here depends on it + body=$(curl -k -s \ + -D "$headers" \ + -E "$OWNER_CERT_FILE":"$OWNER_CERT_PWD" \ + -H "Accept: application/xhtml+xml" \ + -H "Accept-Language: $1" \ + "$END_USER_BASE_URL") + + # an error page is rendered through the same stylesheet and agrees with itself just as happily, so confirm this is the + # document before comparing the two + head -1 "$headers" | grep -qE "^HTTP/[0-9.]+ 200" + + header=$(tr -d '\r' < "$headers" | grep -i "^Content-Language:" | sed 's/^Content-Language: *//i' || true) + rm -f "$headers" + + document=$(echo "$body" | xmllint --xpath 'string(/*[local-name() = "html"]/@lang)' - 2> /dev/null || true) + + [ "$header" = "$expected" ] && [ "$document" = "$expected" ] +} + +assert_agrees "en" "en" +assert_agrees "es" "es" + +# the bundle has no Lithuanian and no German: the root must not claim a language the page is not written in + +assert_agrees "lt,en;q=0.9" "en" +assert_agrees "de" "en" diff --git a/http-tests/language/literal-lang-attributes.sh b/http-tests/language/literal-lang-attributes.sh new file mode 100755 index 0000000000..752bfb07b4 --- /dev/null +++ b/http-tests/language/literal-lang-attributes.sh @@ -0,0 +1,72 @@ +#!/usr/bin/env bash +set -euo pipefail + +# RDF carries language per literal, HTML per element, so every rendered value declares its own rather than inheriting the +# document's. A property renders every language it has, side by side, and the root value is wrong for at least one of them: +# without this a screen reader says "Square" with Lithuanian phonetics and "Aikštė" with an English voice. An untagged +# literal makes no language claim at all, which HTML spells lang="" - the exact counterpart of RDF's absent tag - while a +# number is not prose and inherits, so it is read out in the reader's own language. + +purge_cache "$END_USER_VARNISH_SERVICE" +purge_cache "$FRONTEND_VARNISH_SERVICE" + +slug=$(uuidgen | tr '[:upper:]' '[:lower:]') +doc_url="${END_USER_BASE_URL}${slug}/" + +# the properties belong to the document's primary topic: a document describing nothing but itself renders its title and +# an empty body, so the values would never reach a dd + +echo "<${doc_url}> . +<${doc_url}> \"Language marking\" . +<${doc_url}> <${doc_url}#this> . +<${doc_url}#this> . +<${doc_url}#this> \"Aikštė\"@lt . +<${doc_url}#this> \"Square\"@en . +<${doc_url}#this> \"Untagged value\" . +<${doc_url}#this> \"42\"^^ ." | \ + ldh put \ + -f "$OWNER_CERT_KEYSTORE" \ + -p "$OWNER_CERT_PWD" \ + -t "application/n-triples" \ + "$doc_url" + +purge_cache "$END_USER_VARNISH_SERVICE" +purge_cache "$FRONTEND_VARNISH_SERVICE" + +# XHTML is the same rendering as text/html but well-formed, so the values can be queried with XPath rather than matched as +# text. Attribute order is a serialization detail - the build inlines entities through an XML round trip that alphabetizes +# literal-result-element attributes, while xsl:attribute instructions append in execution order - and nothing here depends +# on it +body=$(curl -k -s \ + -E "$OWNER_CERT_FILE":"$OWNER_CERT_PWD" \ + -H "Accept: application/xhtml+xml" \ + -H "Accept-Language: en" \ + "$doc_url") + +count() +{ + echo "$body" | xmllint --xpath "count($1)" - 2> /dev/null || echo "0" +} + +# the page is in English and both values are on it: each says which language it is in + +[ "$(count "//*[local-name() = 'dd'][@property = 'https://example.org/test#tagged'][@lang = 'lt']")" = "1" ] +[ "$(count "//*[local-name() = 'dd'][@property = 'https://example.org/test#tagged'][@lang = 'en']")" = "1" ] + +# an untagged literal claims no language rather than inheriting the document's + +[ "$(count "//*[local-name() = 'dd'][@property = 'https://example.org/test#plain'][@lang = '']")" = "1" ] + +# a number is not prose - it inherits, so it is read out in whatever language the reader is in + +[ "$(count "//*[local-name() = 'dd'][@property = 'https://example.org/test#typed']")" = "1" ] +[ "$(count "//*[local-name() = 'dd'][@property = 'https://example.org/test#typed'][@lang]")" = "0" ] + +# both languages of the property survive to the page - suppressing one is the defect this marking exists to make safe + +[ "$(count "//*[local-name() = 'span'][@class = 'chip-inline']")" = "2" ] + +ldh delete \ + -f "$OWNER_CERT_KEYSTORE" \ + -p "$OWNER_CERT_PWD" \ + "$doc_url" > /dev/null diff --git a/http-tests/language/vary-accept-language.sh b/http-tests/language/vary-accept-language.sh new file mode 100755 index 0000000000..19fffde866 --- /dev/null +++ b/http-tests/language/vary-accept-language.sh @@ -0,0 +1,39 @@ +#!/usr/bin/env bash +set -euo pipefail + +# An HTML representation differs by the reader's language, so the response has to say so or a shared cache will hand one +# reader's page to another. This is the header the caches downstream key on, and the reason two readers can be served from +# the same URL at all. + +purge_cache "$END_USER_VARNISH_SERVICE" +purge_cache "$FRONTEND_VARNISH_SERVICE" + +curl -i -k -s \ + -E "$OWNER_CERT_FILE":"$OWNER_CERT_PWD" \ + -H "Accept: text/html" \ + -H "Accept-Language: en" \ + "$END_USER_BASE_URL" \ +| tr -d '\r' \ +| grep -i "^Vary:" \ +| grep -qi "Accept-Language" + +# the same URL must survive two readers in sequence through the caches in front of it + +purge_cache "$END_USER_VARNISH_SERVICE" +purge_cache "$FRONTEND_VARNISH_SERVICE" + +fetch_language() +{ + curl -i -k -s \ + -E "$OWNER_CERT_FILE":"$OWNER_CERT_PWD" \ + -H "Accept: text/html" \ + -H "Accept-Language: $1" \ + "$END_USER_BASE_URL" \ + | tr -d '\r' \ + | grep -i "^Content-Language:" \ + | sed 's/^Content-Language: *//i' || true +} + +[ "$(fetch_language "en")" = "en" ] +[ "$(fetch_language "es")" = "es" ] +[ "$(fetch_language "en")" = "en" ] diff --git a/http-tests/misc/PATCH-settings-package-import.sh b/http-tests/misc/PATCH-settings-package-import.sh index f9e080e3c1..08408c91cd 100755 --- a/http-tests/misc/PATCH-settings-package-import.sh +++ b/http-tests/misc/PATCH-settings-package-import.sh @@ -41,15 +41,6 @@ function patch_settings() "${END_USER_BASE_URL}settings" } -function import_triple_count() -{ - curl -k -s \ - -E "$OWNER_CERT_FILE":"$OWNER_CERT_PWD" \ - -H "Accept: application/n-triples" \ - "${END_USER_BASE_URL}settings" \ - | grep -c "linkeddatahub#import" || true -} - # The settings live in the in-memory application model, which initialize_dataset does not # reset - an import left behind by a failed assertion would stay for the rest of the suite. @@ -64,7 +55,6 @@ trap remove_import EXIT response=$(homepage) if grep -qF "$marker" <<< "$response"; then - echo "DEBUG: package stylesheet marker present before import" exit 1 fi @@ -72,7 +62,6 @@ fi status=$(patch_settings "INSERT { <${app_uri}> <${package_uri}> . } WHERE { }") if [[ ! "$status" =~ ^($STATUS_NO_CONTENT)$ ]]; then - echo "DEBUG: Expected $STATUS_NO_CONTENT from the INSERT PATCH, got: $status" exit 1 fi @@ -80,10 +69,6 @@ fi response=$(homepage) if ! grep -qF "$marker" <<< "$response"; then - echo "DEBUG: package stylesheet marker missing after import" - echo "DEBUG: Expected marker: $marker" - echo "DEBUG: Response size: $(wc -c <<< "$response") bytes" - echo "DEBUG: ldh:import triple count in settings = $(import_triple_count)" exit 1 fi @@ -91,7 +76,6 @@ fi status=$(patch_settings "DELETE { <${app_uri}> <${package_uri}> . } WHERE { }") if [[ ! "$status" =~ ^($STATUS_NO_CONTENT)$ ]]; then - echo "DEBUG: Expected $STATUS_NO_CONTENT from the DELETE PATCH, got: $status" exit 1 fi @@ -99,7 +83,5 @@ fi response=$(homepage) if grep -qF "$marker" <<< "$response"; then - echo "DEBUG: package stylesheet marker still present after removal" - echo "DEBUG: ldh:import triple count in settings = $(import_triple_count)" exit 1 fi diff --git a/http-tests/misc/PATCH-settings-package-ontology.sh b/http-tests/misc/PATCH-settings-package-ontology.sh index 6d415ba911..8642cbc195 100755 --- a/http-tests/misc/PATCH-settings-package-ontology.sh +++ b/http-tests/misc/PATCH-settings-package-ontology.sh @@ -16,19 +16,35 @@ package_uri="https://packages.linkeddatahub.com/skos/#this" query='SELECT ?text WHERE { ?constructor . ?constructor ?text . }' -constructor_count() { +# the ldh:view declarations are what render the Broader/Narrower concept blocks - the user-facing +# feature of the package, and the thing that goes silent if the import stops being composed +view_query='SELECT ?view WHERE { VALUES ?property { } ?property ?view . }' + +ns_result_count() { curl -k -f -s -G \ -E "$OWNER_CERT_FILE":"$OWNER_CERT_PWD" \ -H "Accept: application/sparql-results+xml" \ "${END_USER_BASE_URL}ns" \ - --data-urlencode "query=${query}" \ + --data-urlencode "query=${1}" \ | xmllint --xpath "count(//*[local-name() = 'result'])" - } +constructor_count() { + ns_result_count "$query" +} + +view_count() { + ns_result_count "$view_query" +} + # the skos:Concept constructor is not in the app ontology closure initially count=$(constructor_count) if [ "$count" != "0" ]; then - echo "DEBUG: Expected 0 skos:Concept constructors before import, got: $count" + exit 1 +fi + +views=$(view_count) +if [ "$views" != "0" ]; then exit 1 fi @@ -50,7 +66,11 @@ purge_cache "$FRONTEND_VARNISH_SERVICE" # the package ontology joined the closure - no restart, no sleep count=$(constructor_count) if [ "$count" != "1" ]; then - echo "DEBUG: Expected 1 skos:Concept constructor after import, got: $count" + exit 1 +fi + +views=$(view_count) +if [ "$views" != "2" ]; then exit 1 fi @@ -69,14 +89,12 @@ purge_cache "$END_USER_VARNISH_SERVICE" purge_cache "$FRONTEND_VARNISH_SERVICE" # the package ontology left the closure +views=$(view_count) +if [ "$views" != "0" ]; then + exit 1 +fi + count=$(constructor_count) if [ "$count" != "0" ]; then - echo "DEBUG: Expected 0 skos:Concept constructors after removal, got: $count" - echo "DEBUG: does /settings still carry the ldh:import triple after the DELETE?" - curl -k -s \ - -E "$OWNER_CERT_FILE":"$OWNER_CERT_PWD" \ - -H "Accept: application/n-triples" \ - "${END_USER_BASE_URL}settings" \ - | grep -c "linkeddatahub#import" | sed 's/^/DEBUG: ldh:import triple count in settings = /' exit 1 fi diff --git a/http-tests/misc/POST-content-length-413.sh b/http-tests/misc/POST-content-length-413.sh index 171b2dbe55..5af79a6a51 100755 --- a/http-tests/misc/POST-content-length-413.sh +++ b/http-tests/misc/POST-content-length-413.sh @@ -11,7 +11,7 @@ pwd=$(realpath "$PWD") # add agent to the writers group to be able to read/write documents -ldh admin acl add-agent-to-group \ +ldh admin add agent \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ --agent "$AGENT_URI" \ diff --git a/http-tests/misc/POST-transfer-chunked-413.sh b/http-tests/misc/POST-transfer-chunked-413.sh index 3b5d8b2d07..ea19088f7f 100755 --- a/http-tests/misc/POST-transfer-chunked-413.sh +++ b/http-tests/misc/POST-transfer-chunked-413.sh @@ -11,7 +11,7 @@ pwd=$(realpath "$PWD") # add agent to the writers group to be able to read/write documents -ldh admin acl add-agent-to-group \ +ldh admin add agent \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ --agent "$AGENT_URI" \ diff --git a/http-tests/misc/admin-webid-delegation.sh b/http-tests/misc/admin-webid-delegation.sh index 58a08694ba..b27d444874 100755 --- a/http-tests/misc/admin-webid-delegation.sh +++ b/http-tests/misc/admin-webid-delegation.sh @@ -27,7 +27,7 @@ curl --head -k -w "%{http_code}\n" -o /dev/null -s \ # add agent to the owners group to be able to control the admin app -ldh admin acl add-agent-to-group \ +ldh admin add agent \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ --agent "$AGENT_URI" \ diff --git a/http-tests/misc/cors-static.sh b/http-tests/misc/cors-static.sh index ae39023f76..00c676bdf4 100755 --- a/http-tests/misc/cors-static.sh +++ b/http-tests/misc/cors-static.sh @@ -6,7 +6,7 @@ set -euo pipefail response=$(curl -i -k -s \ -H "Origin: https://example.com" \ - "${END_USER_BASE_URL}static/com/atomgraph/linkeddatahub/css/bootstrap.css") + "${END_USER_BASE_URL}static/com/atomgraph/linkeddatahub/css/ldh.css") # Verify Access-Control-Allow-Origin header is present if ! echo "$response" | grep -q "Access-Control-Allow-Origin: \*"; then @@ -26,7 +26,7 @@ preflight=$(curl -i -k -s \ -X OPTIONS \ -H "Origin: https://example.com" \ -H "Access-Control-Request-Method: GET" \ - "${END_USER_BASE_URL}static/com/atomgraph/linkeddatahub/css/bootstrap.css") + "${END_USER_BASE_URL}static/com/atomgraph/linkeddatahub/css/ldh.css") # Verify preflight response has CORS headers if ! echo "$preflight" | grep -q "Access-Control-Allow-Origin: \*"; then diff --git a/http-tests/misc/webid-delegation.sh b/http-tests/misc/webid-delegation.sh index c331049bab..f95fb7ffe3 100755 --- a/http-tests/misc/webid-delegation.sh +++ b/http-tests/misc/webid-delegation.sh @@ -27,7 +27,7 @@ curl --head -k -w "%{http_code}\n" -o /dev/null -s \ # add agent to the writers group to be able to read/write documents -ldh admin acl add-agent-to-group \ +ldh admin add agent \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ --agent "$AGENT_URI" \ diff --git a/http-tests/proxy/GET-proxied-404.sh b/http-tests/proxy/GET-proxied-404.sh index fd573b562a..0ce17e840b 100755 --- a/http-tests/proxy/GET-proxied-404.sh +++ b/http-tests/proxy/GET-proxied-404.sh @@ -9,7 +9,7 @@ purge_cache "$FRONTEND_VARNISH_SERVICE" # add agent to the readers group to be able to read documents -ldh admin acl add-agent-to-group \ +ldh admin add agent \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ --agent "$AGENT_URI" \ diff --git a/http-tests/proxy/GET-proxied-accept-forwarded.sh b/http-tests/proxy/GET-proxied-accept-forwarded.sh index 3709e398f2..73c16ed791 100755 --- a/http-tests/proxy/GET-proxied-accept-forwarded.sh +++ b/http-tests/proxy/GET-proxied-accept-forwarded.sh @@ -9,7 +9,7 @@ purge_cache "$FRONTEND_VARNISH_SERVICE" # add agent to the readers group to be able to read documents -ldh admin acl add-agent-to-group \ +ldh admin add agent \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ --agent "$AGENT_URI" \ diff --git a/http-tests/proxy/GET-proxied-accept-html-not-preferred.sh b/http-tests/proxy/GET-proxied-accept-html-not-preferred.sh index a907af72c4..3cdcd0ff17 100755 --- a/http-tests/proxy/GET-proxied-accept-html-not-preferred.sh +++ b/http-tests/proxy/GET-proxied-accept-html-not-preferred.sh @@ -9,7 +9,7 @@ purge_cache "$FRONTEND_VARNISH_SERVICE" # add agent to the readers group to be able to read documents -ldh admin acl add-agent-to-group \ +ldh admin add agent \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ --agent "$AGENT_URI" \ diff --git a/http-tests/proxy/GET-proxied-external-502.sh b/http-tests/proxy/GET-proxied-external-502.sh index 957a5c3ff5..8b3ec1c2d5 100755 --- a/http-tests/proxy/GET-proxied-external-502.sh +++ b/http-tests/proxy/GET-proxied-external-502.sh @@ -9,7 +9,7 @@ purge_cache "$FRONTEND_VARNISH_SERVICE" # add agent to the readers group to be able to read documents -ldh admin acl add-agent-to-group \ +ldh admin add agent \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ --agent "$AGENT_URI" \ diff --git a/http-tests/proxy/GET-proxied-external.sh b/http-tests/proxy/GET-proxied-external.sh index 7382e708c4..d321b3a939 100755 --- a/http-tests/proxy/GET-proxied-external.sh +++ b/http-tests/proxy/GET-proxied-external.sh @@ -9,7 +9,7 @@ purge_cache "$FRONTEND_VARNISH_SERVICE" # add agent to the readers group to be able to read documents -ldh admin acl add-agent-to-group \ +ldh admin add agent \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ --agent "$AGENT_URI" \ diff --git a/http-tests/proxy/GET-proxied-html-jsonld.sh b/http-tests/proxy/GET-proxied-html-jsonld.sh index 9a127b46fe..bcd6c1a14b 100755 --- a/http-tests/proxy/GET-proxied-html-jsonld.sh +++ b/http-tests/proxy/GET-proxied-html-jsonld.sh @@ -9,7 +9,7 @@ purge_cache "$FRONTEND_VARNISH_SERVICE" # add agent to the readers group to be able to read documents -ldh admin acl add-agent-to-group \ +ldh admin add agent \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ --agent "$AGENT_URI" \ diff --git a/http-tests/proxy/GET-proxied-internal-403.sh b/http-tests/proxy/GET-proxied-internal-403.sh index 752c3ef51f..79ab8deac5 100755 --- a/http-tests/proxy/GET-proxied-internal-403.sh +++ b/http-tests/proxy/GET-proxied-internal-403.sh @@ -9,7 +9,7 @@ purge_cache "$FRONTEND_VARNISH_SERVICE" # add agent to the readers group to be able to read documents -ldh admin acl add-agent-to-group \ +ldh admin add agent \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ --agent "$AGENT_URI" \ diff --git a/http-tests/proxy/GET-proxied-mapped-vocab.sh b/http-tests/proxy/GET-proxied-mapped-vocab.sh index 29e025cb66..4ec6d241e5 100755 --- a/http-tests/proxy/GET-proxied-mapped-vocab.sh +++ b/http-tests/proxy/GET-proxied-mapped-vocab.sh @@ -9,7 +9,7 @@ purge_cache "$FRONTEND_VARNISH_SERVICE" # add agent to the readers group to be able to read documents -ldh admin acl add-agent-to-group \ +ldh admin add agent \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ --agent "$AGENT_URI" \ diff --git a/http-tests/proxy/GET-proxied-rfc1918-403.sh b/http-tests/proxy/GET-proxied-rfc1918-403.sh index ddcded0060..ee84256d3c 100755 --- a/http-tests/proxy/GET-proxied-rfc1918-403.sh +++ b/http-tests/proxy/GET-proxied-rfc1918-403.sh @@ -9,7 +9,7 @@ purge_cache "$FRONTEND_VARNISH_SERVICE" # add agent to the readers group to be able to read documents -ldh admin acl add-agent-to-group \ +ldh admin add agent \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ --agent "$AGENT_URI" \ diff --git a/http-tests/proxy/GET-proxied.sh b/http-tests/proxy/GET-proxied.sh index b38546b89c..9af9534118 100755 --- a/http-tests/proxy/GET-proxied.sh +++ b/http-tests/proxy/GET-proxied.sh @@ -9,7 +9,7 @@ purge_cache "$FRONTEND_VARNISH_SERVICE" # add agent to the readers group to be able to read documents -ldh admin acl add-agent-to-group \ +ldh admin add agent \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ --agent "$AGENT_URI" \ diff --git a/http-tests/proxy/HEAD-proxied-accept.sh b/http-tests/proxy/HEAD-proxied-accept.sh index 56fa126c95..580aeccf3b 100755 --- a/http-tests/proxy/HEAD-proxied-accept.sh +++ b/http-tests/proxy/HEAD-proxied-accept.sh @@ -9,7 +9,7 @@ purge_cache "$FRONTEND_VARNISH_SERVICE" # add agent to the readers group to be able to read documents -ldh admin acl add-agent-to-group \ +ldh admin add agent \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ --agent "$AGENT_URI" \ diff --git a/http-tests/proxy/HEAD-proxied-etag.sh b/http-tests/proxy/HEAD-proxied-etag.sh index 0a1e067c2f..fb3b689f51 100755 --- a/http-tests/proxy/HEAD-proxied-etag.sh +++ b/http-tests/proxy/HEAD-proxied-etag.sh @@ -9,7 +9,7 @@ purge_cache "$FRONTEND_VARNISH_SERVICE" # add agent to the readers group to be able to read documents -ldh admin acl add-agent-to-group \ +ldh admin add agent \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ --agent "$AGENT_URI" \ diff --git a/http-tests/proxy/PATCH-proxied-constructor-update.sh b/http-tests/proxy/PATCH-proxied-constructor-update.sh index 2edbceebe2..19857f0a68 100755 --- a/http-tests/proxy/PATCH-proxied-constructor-update.sh +++ b/http-tests/proxy/PATCH-proxied-constructor-update.sh @@ -41,7 +41,7 @@ curl -k -f -s -o /dev/null \ # Rebuild the in-memory ontology so the constructor hash URI enters the OntModel. # After this, the DESCRIBE check in ProxyRequestFilter will fire for the PATCH. -ldh admin clear-ontology \ +ldh admin clear ontology \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ -b "$ADMIN_BASE_URL" \ @@ -66,7 +66,6 @@ status=$(curl -k -w "%{http_code}" -o /dev/null -s \ "$END_USER_BASE_URL") if [[ ! "$status" =~ ^($STATUS_PATCH_SUCCESS)$ ]]; then - echo "DEBUG: Expected $STATUS_PATCH_SUCCESS from the proxied PATCH, got: $status" exit 1 fi @@ -81,7 +80,5 @@ response=$(curl -k -f -s \ "$ontology_doc") if ! grep -qF "TestClassUpdated" <<< "$response"; then - echo "DEBUG: Expected the constructor text to contain: TestClassUpdated" - echo "DEBUG: Got: $response" exit 1 fi diff --git a/http-tests/proxy/PATCH-proxied-update.sh b/http-tests/proxy/PATCH-proxied-update.sh index 22a83b4626..56f8451942 100755 --- a/http-tests/proxy/PATCH-proxied-update.sh +++ b/http-tests/proxy/PATCH-proxied-update.sh @@ -9,7 +9,7 @@ purge_cache "$FRONTEND_VARNISH_SERVICE" # add agent to the writers group -ldh admin acl add-agent-to-group \ +ldh admin add agent \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ --agent "$AGENT_URI" \ @@ -17,7 +17,7 @@ ldh admin acl add-agent-to-group \ # create item document to PATCH -item=$(ldh create-item \ +item=$(ldh create item \ -f "$AGENT_CERT_KEYSTORE" \ -p "$AGENT_CERT_PWD" \ -b "$END_USER_BASE_URL" \ diff --git a/http-tests/proxy/POST-proxied-form.sh b/http-tests/proxy/POST-proxied-form.sh index 3a0f900dbb..a0d60079cd 100755 --- a/http-tests/proxy/POST-proxied-form.sh +++ b/http-tests/proxy/POST-proxied-form.sh @@ -9,7 +9,7 @@ purge_cache "$FRONTEND_VARNISH_SERVICE" # add agent to the writers group -ldh admin acl add-agent-to-group \ +ldh admin add agent \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ --agent "$AGENT_URI" \ diff --git a/http-tests/proxy/POST-proxied-query.sh b/http-tests/proxy/POST-proxied-query.sh index 58b38b4083..b97a632532 100644 --- a/http-tests/proxy/POST-proxied-query.sh +++ b/http-tests/proxy/POST-proxied-query.sh @@ -9,7 +9,7 @@ purge_cache "$FRONTEND_VARNISH_SERVICE" # add agent to the writers group - POST requests count as write operations -ldh admin acl add-agent-to-group \ +ldh admin add agent \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ --agent "$AGENT_URI" \ diff --git a/http-tests/proxy/PUT-proxied-location.sh b/http-tests/proxy/PUT-proxied-location.sh index f724d820f3..7169dbddeb 100755 --- a/http-tests/proxy/PUT-proxied-location.sh +++ b/http-tests/proxy/PUT-proxied-location.sh @@ -9,7 +9,7 @@ purge_cache "$FRONTEND_VARNISH_SERVICE" # add agent to the writers group -ldh admin acl add-agent-to-group \ +ldh admin add agent \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ --agent "$AGENT_URI" \ diff --git a/http-tests/run.sh b/http-tests/run.sh index 54b294201a..12eb8c4b4e 100755 --- a/http-tests/run.sh +++ b/http-tests/run.sh @@ -284,6 +284,8 @@ run_tests "sparql-protocol" $(find ./sparql-protocol/ -type f -name '*.sh') (( error_count += $? )) run_tests "versioning" $(find ./versioning/ -type f -name '*.sh') (( error_count += $? )) +run_tests "language" $(find ./language/ -type f -name '*.sh') +(( error_count += $? )) end_time=$(date +%s) runtime=$((end_time-start_time)) diff --git a/http-tests/sparql-protocol/query/GET-ns-constructors.sh b/http-tests/sparql-protocol/query/GET-ns-constructors.sh index 349e1fe3a0..7980781bc7 100755 --- a/http-tests/sparql-protocol/query/GET-ns-constructors.sh +++ b/http-tests/sparql-protocol/query/GET-ns-constructors.sh @@ -27,6 +27,5 @@ echo "$results" | grep -q "https://w3id.org/atomgraph/linkeddatahub#TitleConstru # the constructor texts are returned (CONSTRUCT templates the client instantiates) count=$(echo "$results" | xmllint --xpath "count(//*[local-name() = 'binding'][@name = 'text']/*[local-name() = 'literal'][contains(., 'CONSTRUCT')])" -) if [ "$count" -lt 3 ]; then - echo "DEBUG: Expected at least 3 constructor texts, got: $count" exit 1 fi diff --git a/http-tests/sparql-protocol/query/GET-ns-no-query.sh b/http-tests/sparql-protocol/query/GET-ns-no-query.sh index 5cb09bd307..88124aa92a 100755 --- a/http-tests/sparql-protocol/query/GET-ns-no-query.sh +++ b/http-tests/sparql-protocol/query/GET-ns-no-query.sh @@ -14,7 +14,7 @@ namespace="${namespace_doc}#" ontology_doc="${ADMIN_BASE_URL}ontologies/namespace/" class="${namespace}ClassThree" -ldh admin ontologies add-class \ +ldh admin add class \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ -b "$ADMIN_BASE_URL" \ @@ -24,7 +24,7 @@ ldh admin ontologies add-class \ # clear ontology from memory so the new class is loaded on next request -ldh admin clear-ontology \ +ldh admin clear ontology \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ -b "$ADMIN_BASE_URL" \ diff --git a/http-tests/sparql-protocol/query/GET-ns-relative-uri.sh b/http-tests/sparql-protocol/query/GET-ns-relative-uri.sh index fa65fc40d9..a53333aece 100755 --- a/http-tests/sparql-protocol/query/GET-ns-relative-uri.sh +++ b/http-tests/sparql-protocol/query/GET-ns-relative-uri.sh @@ -14,7 +14,7 @@ namespace="${namespace_doc}#" ontology_doc="${ADMIN_BASE_URL}ontologies/namespace/" class="${namespace}NewClass" -ldh admin ontologies add-class \ +ldh admin add class \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ -b "$ADMIN_BASE_URL" \ @@ -24,7 +24,7 @@ ldh admin ontologies add-class \ # clear ontology from memory -ldh admin clear-ontology \ +ldh admin clear ontology \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ -b "$ADMIN_BASE_URL" \ diff --git a/http-tests/sparql-protocol/query/GET-sparql-default-graph-uri-from-override.sh b/http-tests/sparql-protocol/query/GET-sparql-default-graph-uri-from-override.sh index 23eb625342..90775f3149 100755 --- a/http-tests/sparql-protocol/query/GET-sparql-default-graph-uri-from-override.sh +++ b/http-tests/sparql-protocol/query/GET-sparql-default-graph-uri-from-override.sh @@ -12,7 +12,7 @@ purge_cache "$FRONTEND_VARNISH_SERVICE" slug_one=$(uuidgen | tr '[:upper:]' '[:lower:]') slug_two=$(uuidgen | tr '[:upper:]' '[:lower:]') -container_one=$(ldh create-container \ +container_one=$(ldh create container \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ -b "$END_USER_BASE_URL" \ @@ -20,7 +20,7 @@ container_one=$(ldh create-container \ --slug "$slug_one" \ --parent "$END_USER_BASE_URL") -container_two=$(ldh create-container \ +container_two=$(ldh create container \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ -b "$END_USER_BASE_URL" \ diff --git a/http-tests/sparql-protocol/query/GET-sparql-default-graph-uri-graph-pattern.sh b/http-tests/sparql-protocol/query/GET-sparql-default-graph-uri-graph-pattern.sh index e831ad6b24..3af6b26f60 100755 --- a/http-tests/sparql-protocol/query/GET-sparql-default-graph-uri-graph-pattern.sh +++ b/http-tests/sparql-protocol/query/GET-sparql-default-graph-uri-graph-pattern.sh @@ -11,7 +11,7 @@ purge_cache "$FRONTEND_VARNISH_SERVICE" slug=$(uuidgen | tr '[:upper:]' '[:lower:]') -container=$(ldh create-container \ +container=$(ldh create container \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ -b "$END_USER_BASE_URL" \ diff --git a/http-tests/sparql-protocol/query/GET-sparql-default-graph-uri.sh b/http-tests/sparql-protocol/query/GET-sparql-default-graph-uri.sh index 187d1587b6..b7172725e9 100755 --- a/http-tests/sparql-protocol/query/GET-sparql-default-graph-uri.sh +++ b/http-tests/sparql-protocol/query/GET-sparql-default-graph-uri.sh @@ -12,7 +12,7 @@ purge_cache "$FRONTEND_VARNISH_SERVICE" slug_one=$(uuidgen | tr '[:upper:]' '[:lower:]') slug_two=$(uuidgen | tr '[:upper:]' '[:lower:]') -container_one=$(ldh create-container \ +container_one=$(ldh create container \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ -b "$END_USER_BASE_URL" \ @@ -20,7 +20,7 @@ container_one=$(ldh create-container \ --slug "$slug_one" \ --parent "$END_USER_BASE_URL") -container_two=$(ldh create-container \ +container_two=$(ldh create container \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ -b "$END_USER_BASE_URL" \ diff --git a/http-tests/sparql-protocol/query/POST-ns-relative-uri.sh b/http-tests/sparql-protocol/query/POST-ns-relative-uri.sh index fc08375556..94f62fa9b6 100644 --- a/http-tests/sparql-protocol/query/POST-ns-relative-uri.sh +++ b/http-tests/sparql-protocol/query/POST-ns-relative-uri.sh @@ -14,7 +14,7 @@ namespace="${namespace_doc}#" ontology_doc="${ADMIN_BASE_URL}ontologies/namespace/" class="${namespace}NewClass" -ldh admin ontologies add-class \ +ldh admin add class \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ -b "$ADMIN_BASE_URL" \ @@ -24,7 +24,7 @@ ldh admin ontologies add-class \ # clear ontology from memory -ldh admin clear-ontology \ +ldh admin clear ontology \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ -b "$ADMIN_BASE_URL" \ diff --git a/http-tests/sparql-protocol/query/POST-sparql-default-graph-uri.sh b/http-tests/sparql-protocol/query/POST-sparql-default-graph-uri.sh index 7d3cecfb40..db544f2b7f 100755 --- a/http-tests/sparql-protocol/query/POST-sparql-default-graph-uri.sh +++ b/http-tests/sparql-protocol/query/POST-sparql-default-graph-uri.sh @@ -12,7 +12,7 @@ purge_cache "$FRONTEND_VARNISH_SERVICE" slug_one=$(uuidgen | tr '[:upper:]' '[:lower:]') slug_two=$(uuidgen | tr '[:upper:]' '[:lower:]') -container_one=$(ldh create-container \ +container_one=$(ldh create container \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ -b "$END_USER_BASE_URL" \ @@ -20,7 +20,7 @@ container_one=$(ldh create-container \ --slug "$slug_one" \ --parent "$END_USER_BASE_URL") -container_two=$(ldh create-container \ +container_two=$(ldh create container \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ -b "$END_USER_BASE_URL" \ diff --git a/http-tests/system/admin/POST-clear-403.sh b/http-tests/system/admin/POST-clear-403.sh index 5e9f822c57..0005bc3c85 100755 --- a/http-tests/system/admin/POST-clear-403.sh +++ b/http-tests/system/admin/POST-clear-403.sh @@ -10,7 +10,7 @@ purge_cache "$FRONTEND_VARNISH_SERVICE" # POST /clear with a writer (not owner) should return 403 # /clear is only in the full-control authorization which is restricted to owners -ldh admin acl add-agent-to-group \ +ldh admin add agent \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ --agent "$AGENT_URI" \ diff --git a/http-tests/system/admin/POST-clear-readers-403.sh b/http-tests/system/admin/POST-clear-readers-403.sh index 56a2f8ad98..9c99655c95 100755 --- a/http-tests/system/admin/POST-clear-readers-403.sh +++ b/http-tests/system/admin/POST-clear-readers-403.sh @@ -10,7 +10,7 @@ purge_cache "$FRONTEND_VARNISH_SERVICE" # POST /clear with a reader should return 403 # /clear is only in the full-control authorization which is restricted to owners -ldh admin acl add-agent-to-group \ +ldh admin add agent \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ --agent "$AGENT_URI" \ diff --git a/http-tests/system/end-user/GET-settings-403.sh b/http-tests/system/end-user/GET-settings-403.sh index a05ff98159..978c5e8db9 100755 --- a/http-tests/system/end-user/GET-settings-403.sh +++ b/http-tests/system/end-user/GET-settings-403.sh @@ -10,7 +10,7 @@ purge_cache "$FRONTEND_VARNISH_SERVICE" # GET /settings with a writer (not owner) should return 403 # /settings is only in the full-control authorization which is restricted to owners -ldh admin acl add-agent-to-group \ +ldh admin add agent \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ --agent "$AGENT_URI" \ diff --git a/http-tests/system/end-user/GET-settings-readers-403.sh b/http-tests/system/end-user/GET-settings-readers-403.sh index a7361daad0..2cf5dd24d3 100755 --- a/http-tests/system/end-user/GET-settings-readers-403.sh +++ b/http-tests/system/end-user/GET-settings-readers-403.sh @@ -10,7 +10,7 @@ purge_cache "$FRONTEND_VARNISH_SERVICE" # GET /settings with a reader should return 403 # /settings is only in the full-control authorization which is restricted to owners -ldh admin acl add-agent-to-group \ +ldh admin add agent \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ --agent "$AGENT_URI" \ diff --git a/http-tests/system/end-user/PATCH-settings-403.sh b/http-tests/system/end-user/PATCH-settings-403.sh index 199d83e789..e2dd469444 100755 --- a/http-tests/system/end-user/PATCH-settings-403.sh +++ b/http-tests/system/end-user/PATCH-settings-403.sh @@ -10,7 +10,7 @@ purge_cache "$FRONTEND_VARNISH_SERVICE" # PATCH /settings with a writer (not owner) should return 403 # /settings is only in the full-control authorization which is restricted to owners -ldh admin acl add-agent-to-group \ +ldh admin add agent \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ --agent "$AGENT_URI" \ diff --git a/http-tests/system/end-user/PATCH-settings-readers-403.sh b/http-tests/system/end-user/PATCH-settings-readers-403.sh index 13fda096be..4822aaeb54 100755 --- a/http-tests/system/end-user/PATCH-settings-readers-403.sh +++ b/http-tests/system/end-user/PATCH-settings-readers-403.sh @@ -10,7 +10,7 @@ purge_cache "$FRONTEND_VARNISH_SERVICE" # PATCH /settings with a reader should return 403 # /settings is only in the full-control authorization which is restricted to owners -ldh admin acl add-agent-to-group \ +ldh admin add agent \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ --agent "$AGENT_URI" \ diff --git a/http-tests/versioning/DELETE-removes-file.sh b/http-tests/versioning/DELETE-removes-file.sh index 847e1254e8..bf9a7ee06a 100755 --- a/http-tests/versioning/DELETE-removes-file.sh +++ b/http-tests/versioning/DELETE-removes-file.sh @@ -17,7 +17,7 @@ purge_cache "$FRONTEND_VARNISH_SERVICE" # add agent to the writers group -ldh admin acl add-agent-to-group \ +ldh admin add agent \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ --agent "$AGENT_URI" \ @@ -37,13 +37,22 @@ echo "<${doc_url}> /dev/null 2>&1; then + found=1 break fi sleep 1 done -gh api "repos/${VERSIONING_TEST_REPO}/contents/${path}?ref=${VERSIONING_TEST_BRANCH:-main}" > /dev/null + +if [ -z "$found" ]; then + exit 1 +fi # delete the document and check that the file disappears @@ -59,5 +68,4 @@ for i in $(seq 1 30); do sleep 1 done -echo "DEBUG: file '${path}' still present in ${VERSIONING_TEST_REPO} after document deletion" exit 1 diff --git a/http-tests/versioning/GET-timegate.sh b/http-tests/versioning/GET-timegate.sh index cba201d672..dabd95b04a 100755 --- a/http-tests/versioning/GET-timegate.sh +++ b/http-tests/versioning/GET-timegate.sh @@ -17,7 +17,7 @@ purge_cache "$FRONTEND_VARNISH_SERVICE" # add agent to the writers group -ldh admin acl add-agent-to-group \ +ldh admin add agent \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ --agent "$AGENT_URI" \ @@ -76,23 +76,20 @@ ldh get \ "$doc_url" \ | tr -d '\r') -echo "DEBUG: Original Resource headers:" -echo "$response_headers" - echo "$response_headers" | grep -q "<${doc_url}?timegate>; rel=timegate" # without Accept-Datetime the TimeGate selects the most recent Memento timegate_headers=$( -curl -k -s -D - -o /dev/null \ - -E "$AGENT_CERT_FILE":"$AGENT_CERT_PWD" \ - "${doc_url}?timegate" \ +ldh get \ + -f "$AGENT_CERT_KEYSTORE" \ + -p "$AGENT_CERT_PWD" \ + --timegate \ + --head \ + "$doc_url" \ | tr -d '\r') -echo "DEBUG: TimeGate headers (no Accept-Datetime):" -echo "$timegate_headers" - -echo "$timegate_headers" | grep -q '^HTTP/.* 302' +echo "$timegate_headers" | grep -q '^HTTP 302' echo "$timegate_headers" | grep -qi "^Location: ${doc_url}?version=${sha2}" echo "$timegate_headers" | grep -qi '^Vary:.*accept-datetime' echo "$timegate_headers" | grep -q "<${doc_url}>; rel=original" @@ -102,28 +99,32 @@ echo "$timegate_headers" | grep -qi '^Cache-Control:.*no-store' # a 302 TimeGate response must not carry Memento-Datetime if echo "$timegate_headers" | grep -qi '^Memento-Datetime:'; then - echo "DEBUG: TimeGate 302 response must not carry Memento-Datetime" exit 1 fi -# with Accept-Datetime at the first commit's time, the TimeGate selects the first Memento +# the negotiated Memento is the only line the command prints, so it composes into a `ldh get` -accept_datetime=$(date -u -j -f "%Y-%m-%dT%H:%M:%SZ" "$first_datetime" "+%a, %d %b %Y %H:%M:%S GMT" 2> /dev/null \ - || date -u -d "$first_datetime" "+%a, %d %b %Y %H:%M:%S GMT") +memento=$( +ldh get \ + -f "$AGENT_CERT_KEYSTORE" \ + -p "$AGENT_CERT_PWD" \ + --timegate \ + "$doc_url") -echo "DEBUG: Accept-Datetime: $accept_datetime (commit $sha1 at $first_datetime)" +[ "$memento" = "${doc_url}?version=${sha2}" ] -dated_headers=$( -curl -k -s -D - -o /dev/null \ - -E "$AGENT_CERT_FILE":"$AGENT_CERT_PWD" \ - -H "Accept-Datetime: ${accept_datetime}" \ - "${doc_url}?timegate" \ -| tr -d '\r') +# with Accept-Datetime at the first commit's time, the TimeGate selects the first Memento. +# --datetime takes the ISO 8601 datetime the commit carries, no shell date conversion -echo "DEBUG: TimeGate headers (Accept-Datetime at first commit):" -echo "$dated_headers" +dated_memento=$( +ldh get \ + -f "$AGENT_CERT_KEYSTORE" \ + -p "$AGENT_CERT_PWD" \ + --timegate \ + --datetime "$first_datetime" \ + "$doc_url") -echo "$dated_headers" | grep -qi "^Location: ${doc_url}?version=${sha1}" +[ "$dated_memento" = "${doc_url}?version=${sha1}" ] # a malformed Accept-Datetime is rejected @@ -133,7 +134,6 @@ curl -k -w "%{http_code}\n" -o /dev/null -s \ -H "Accept-Datetime: yesterday afternoon" \ "${doc_url}?timegate") -echo "DEBUG: malformed Accept-Datetime status: $status (expected 400)" [ "$status" = "400" ] # the TimeGate is read-only @@ -146,5 +146,4 @@ curl -k -w "%{http_code}\n" -o /dev/null -s \ --data-binary "<${doc_url}> \"Overwrite attempt\" ." \ "${doc_url}?timegate") -echo "DEBUG: PUT to TimeGate status: $status (expected 405)" [ "$status" = "405" ] diff --git a/http-tests/versioning/GET-timemap.sh b/http-tests/versioning/GET-timemap.sh index 543f2cc85d..358bc0b0bc 100755 --- a/http-tests/versioning/GET-timemap.sh +++ b/http-tests/versioning/GET-timemap.sh @@ -17,7 +17,7 @@ purge_cache "$FRONTEND_VARNISH_SERVICE" # add agent to the writers group -ldh admin acl add-agent-to-group \ +ldh admin add agent \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ --agent "$AGENT_URI" \ @@ -37,13 +37,25 @@ echo "<${doc_url}> /dev/null || true +} + +sha="" + for i in $(seq 1 30); do - if gh api "repos/${VERSIONING_TEST_REPO}/contents/${path}?ref=${VERSIONING_TEST_BRANCH:-main}" > /dev/null 2>&1; then - break - fi + sha=$(head_sha) + if [ -n "$sha" ]; then break; fi sleep 1 done -gh api "repos/${VERSIONING_TEST_REPO}/contents/${path}?ref=${VERSIONING_TEST_BRANCH:-main}" > /dev/null + +if [ -z "$sha" ]; then + exit 1 +fi # check that the document advertises its TimeMap via the Link header @@ -56,16 +68,12 @@ ldh get \ "$doc_url" \ | tr -d '\r') -echo "DEBUG: Response headers:" -echo "$response_headers" - # RFC 7089: the Original Resource advertises rel=timemap with the link-format media type, # and MUST NOT carry rel=original echo "$response_headers" | grep -q 'rel=timemap' echo "$response_headers" | grep -q 'type="application/link-format"' if echo "$response_headers" | grep -q 'rel=original'; then - echo "DEBUG: Original Resource must not carry rel=original" exit 1 fi @@ -76,10 +84,8 @@ ldh get \ -f "$AGENT_CERT_KEYSTORE" \ -p "$AGENT_CERT_PWD" \ --accept 'application/n-triples' \ - "${doc_url}?timemap") - -echo "DEBUG: TimeMap:" -echo "$timemap" + --timemap \ + "$doc_url") echo "$timemap" | grep -q "ns/prov#Collection" echo "$timemap" | grep -q "ns/prov#hadMember" @@ -95,10 +101,8 @@ ldh get \ -f "$AGENT_CERT_KEYSTORE" \ -p "$AGENT_CERT_PWD" \ --accept 'application/link-format' \ - "${doc_url}?timemap") - -echo "DEBUG: link-format TimeMap:" -echo "$link_format" + --timemap \ + "$doc_url") echo "$link_format" | grep -q "<${doc_url}>;rel=\"original\"" echo "$link_format" | grep -q "<${doc_url}?timemap>;rel=\"self\";type=\"application/link-format\"" @@ -114,5 +118,4 @@ curl -k -w "%{http_code}\n" -o /dev/null -s \ -H "Accept: application/link-format" \ "$doc_url") -echo "DEBUG: link-format status on the document itself: $status (expected 406)" [ "$status" = "406" ] diff --git a/http-tests/versioning/GET-version.sh b/http-tests/versioning/GET-version.sh index c4ad815997..bb5a34560b 100755 --- a/http-tests/versioning/GET-version.sh +++ b/http-tests/versioning/GET-version.sh @@ -17,7 +17,7 @@ purge_cache "$FRONTEND_VARNISH_SERVICE" # add agent to the writers group -ldh admin acl add-agent-to-group \ +ldh admin add agent \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ --agent "$AGENT_URI" \ @@ -72,15 +72,12 @@ ldh get \ -f "$AGENT_CERT_KEYSTORE" \ -p "$AGENT_CERT_PWD" \ --accept 'application/n-triples' \ - "${doc_url}?version=${sha1}") - -echo "DEBUG: Response body:" -echo "$response_body" + --version "$sha1" \ + "$doc_url") echo "$response_body" | grep -q "First version" if echo "$response_body" | grep -q "Second version"; then - echo "DEBUG: historical version response contains the updated title" exit 1 fi @@ -92,12 +89,10 @@ ldh get \ -p "$AGENT_CERT_PWD" \ --accept 'application/n-triples' \ --head \ - "${doc_url}?version=${sha1}" \ + --version "$sha1" \ + "$doc_url" \ | tr -d '\r') -echo "DEBUG: Response headers:" -echo "$response_headers" - echo "$response_headers" | grep -qi '^Memento-Datetime:' echo "$response_headers" | grep -qi '^Cache-Control:.*immutable' @@ -113,12 +108,10 @@ echo "$response_headers" | grep -q 'rel=timemap' echo "$response_headers" | grep -q 'acl#Read' if echo "$response_headers" | grep -q 'acl#Write'; then - echo "DEBUG: version response advertises acl:Write" exit 1 fi if echo "$response_headers" | grep -q 'acl#Append'; then - echo "DEBUG: version response advertises acl:Append" exit 1 fi diff --git a/http-tests/versioning/PUT-creates-commit.sh b/http-tests/versioning/PUT-creates-commit.sh index ce3693f20c..ef95e27096 100755 --- a/http-tests/versioning/PUT-creates-commit.sh +++ b/http-tests/versioning/PUT-creates-commit.sh @@ -17,7 +17,7 @@ purge_cache "$FRONTEND_VARNISH_SERVICE" # add agent to the writers group -ldh admin acl add-agent-to-group \ +ldh admin add agent \ -f "$OWNER_CERT_KEYSTORE" \ -p "$OWNER_CERT_PWD" \ --agent "$AGENT_URI" \ @@ -36,21 +36,24 @@ echo "<${doc_url}> /dev/null || true +} + +author="" + for i in $(seq 1 30); do - if gh api "repos/${VERSIONING_TEST_REPO}/contents/${path}?ref=${VERSIONING_TEST_BRANCH:-main}" > /dev/null 2>&1; then - break - fi + author=$(commit_author) + if [ -n "$author" ]; then break; fi sleep 1 done -gh api "repos/${VERSIONING_TEST_REPO}/contents/${path}?ref=${VERSIONING_TEST_BRANCH:-main}" > /dev/null - -# check that the commit author is the agent's WebID -author=$(gh api "repos/${VERSIONING_TEST_REPO}/commits?path=${path}&sha=${VERSIONING_TEST_BRANCH:-main}&per_page=1" --jq '.[0].commit.author.name') -echo "DEBUG: Expected author: $AGENT_URI" -echo "DEBUG: Got author: $author" [ "$author" = "$AGENT_URI" ] diff --git a/platform/namespace-ontology.trig.template b/platform/namespace-ontology.trig.template index b9042d5a61..6594834d3a 100644 --- a/platform/namespace-ontology.trig.template +++ b/platform/namespace-ontology.trig.template @@ -87,7 +87,7 @@ rdfs:label "Write/append access" ; rdfs:comment "Allows write access to all documents and containers" ; acl:accessToClass dh:Item, dh:Container, def:Root ; - acl:accessTo <${end_user_origin}/sparql>, <${end_user_origin}/importer>, <${end_user_origin}/add>, <${end_user_origin}/generate>, <${end_user_origin}/ns> ; + acl:accessTo <${end_user_origin}/sparql>, <${end_user_origin}/importer>, <${end_user_origin}/ns> ; acl:mode acl:Write, acl:Append ; acl:agentGroup , . @@ -107,7 +107,7 @@ rdfs:label "Full control" ; rdfs:comment "Allows full read/write access to all application resources" ; acl:accessToClass dh:Item, dh:Container, def:Root ; - acl:accessTo <${end_user_origin}/sparql>, <${end_user_origin}/importer>, <${end_user_origin}/add>, <${end_user_origin}/generate>, <${end_user_origin}/ns>, <${end_user_origin}/settings> ; + acl:accessTo <${end_user_origin}/sparql>, <${end_user_origin}/importer>, <${end_user_origin}/ns>, <${end_user_origin}/settings> ; acl:mode acl:Read, acl:Append, acl:Write, acl:Control ; acl:agentGroup . diff --git a/pom.xml b/pom.xml index 3337245c84..e486783211 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ com.atomgraph linkeddatahub - 5.10.0 + 6.0.0-SNAPSHOT ${packaging.type} AtomGraph LinkedDataHub @@ -46,7 +46,7 @@ https://github.com/AtomGraph/LinkedDataHub scm:git:git://github.com/AtomGraph/LinkedDataHub.git scm:git:git@github.com:AtomGraph/LinkedDataHub.git - linkeddatahub-5.10.0 + linkeddatahub-5.5.4 @@ -159,7 +159,7 @@ ${project.groupId} twirl - 2.0.0 + 2.0.1-SNAPSHOT org.slf4j @@ -169,13 +169,13 @@ ${project.groupId} client - 5.0.3 + 5.0.4 classes ${project.groupId} client - 5.0.3 + 5.0.4 war diff --git a/release.sh b/release.sh index 8e2166020a..addd5b7cfa 100755 --- a/release.sh +++ b/release.sh @@ -23,6 +23,9 @@ print_error() { # Track if release completed successfully RELEASE_SUCCESSFUL=false +# Set once release:perform has deployed. Publishing to Maven Central cannot be undone, so past this +# point the local git state is the only record of what was published and must not be thrown away. +PUBLISHED=false # Trap handler for automatic cleanup on failure cleanup_on_failure() { @@ -30,6 +33,17 @@ cleanup_on_failure() { # Only clean up if release failed (non-zero exit) and wasn't successful if [ $exit_code -ne 0 ] && [ "$RELEASE_SUCCESSFUL" = false ]; then + if [ "$PUBLISHED" = true ]; then + print_error "Release failed AFTER publishing $RELEASE_VERSION to Maven Central." + print_error "Not rolling back: the tag and commits are the only record of what was published." + print_error "Finish by hand from the current state:" + print_error " git checkout master && git merge --no-ff $RELEASE_TAG^{commit} -m \"Release version $RELEASE_VERSION\"" + print_error " git push origin master && git push origin $RELEASE_TAG" + print_error " git checkout develop && git merge --no-ff $RELEASE_BRANCH -m \"Post-release version bump\"" + print_error " git push origin develop" + exit $exit_code + fi + print_error "Release failed! Rolling back changes..." # Clean up Maven release artifacts @@ -48,7 +62,7 @@ cleanup_on_failure() { git branch -D "$RELEASE_BRANCH" 2>/dev/null || true fi - print_status "Rollback complete. You're back on develop branch." + print_status "Rollback complete. You're back on develop branch. Nothing was published." exit $exit_code fi } @@ -74,6 +88,24 @@ if ! git diff-index --quiet HEAD --; then exit 1 fi +# git diff-index above cannot see files marked skip-worktree or assume-unchanged: git reports them as +# matching the index no matter what is on disk. A branch switch is not so forgiving - it refuses to +# overwrite one whose content has diverged, which is how a release once died on `git checkout master` +# after Maven Central had already been published to. +DIVERGED_HIDDEN=$(git ls-files -v | grep -vE '^H ' | cut -c3- | while read -r file; do + [ -e "$file" ] || continue + [ "$(git hash-object "$file")" = "$(git rev-parse "HEAD:$file" 2>/dev/null)" ] || echo " $file" +done) + +# Only a warning: keeping local config out of commits this way is deliberate and usually harmless. +# It becomes fatal solely when such a file also differs between the branches being switched between, +# which assert_can_switch_to checks precisely, right before the irreversible step. +if [ -n "$DIVERGED_HIDDEN" ]; then + print_warning "Hidden from git status (skip-worktree/assume-unchanged) but differing from HEAD:" + echo "$DIVERGED_HIDDEN" >&2 + print_warning "Harmless unless one also differs between develop and master - checked before publishing." +fi + # Check if GPG is installed and configured if ! command -v gpg &> /dev/null; then print_error "GPG is not installed. Maven release requires GPG to sign artifacts." @@ -144,30 +176,58 @@ if git tag -l | grep -q "^$RELEASE_TAG$"; then fi fi -# Align the CLI before release:prepare, so its version is already correct in the commit that gets -# tagged - and so the two commits release:prepare adds stay the last two, which the merge below reads -# by position +# Align the CLI before release:prepare, so its version is already correct in the commit that gets tagged sync_cli_version "$RELEASE_VERSION" +# Anchor for deriving the commits release:prepare is about to add. Reading them back positionally +# (git log -2) breaks the moment anything else commits in between. +PREPARE_BASE=$(git rev-parse HEAD) + # Configure Maven release plugin to not push changes automatically mvn release:clean release:prepare -DpushChanges=false -DlocalCheckout=true -print_status "Performing Maven release (deploying to Sonatype)..." -mvn release:perform -DlocalCheckout=true - -# Capture the commit hashes -RELEASE_COMMIT=$(git log --oneline -2 --pretty=format:"%H" | tail -1) -SNAPSHOT_COMMIT=$(git log --oneline -1 --pretty=format:"%H") +# release:prepare adds the release commit and then the next-development commit on top of PREPARE_BASE +RELEASE_COMMIT=$(git rev-list --ancestry-path "$PREPARE_BASE"..HEAD | tail -1) +SNAPSHOT_COMMIT=$(git rev-parse HEAD) print_status "Release commit: $RELEASE_COMMIT" print_status "Development commit (SNAPSHOT bump): $SNAPSHOT_COMMIT" # Follow the platform onto the next development version. Deliberately after the two hashes are -# captured: it adds a commit on top, and both are read by position. Master merges $RELEASE_COMMIT -# alone so it does not go there, develop merges the whole branch so it does. +# captured, so it does not land between $PREPARE_BASE and the commits derived from it. Master merges +# $RELEASE_COMMIT alone so it does not go there, develop merges the whole branch so it does. NEXT_VERSION=$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout) sync_cli_version "$NEXT_VERSION" +# Everything below release:perform is unwindable; release:perform itself is not - Maven Central does +# not take artifacts back. So prove the branch switches it is followed by can actually happen while +# failing is still free. This is git's own checkout refusal condition: a file that differs between +# the branches, whose working-tree copy differs from HEAD. +assert_can_switch_to() { + local target="$1" blocked="" file + + # process substitution, not a pipe: a `while read` on the right of a pipe runs in a subshell, + # where the exit below would only kill the subshell and let the release carry on regardless + while read -r file; do + [ -e "$file" ] || continue + [ "$(git hash-object "$file")" = "$(git rev-parse "HEAD:$file" 2>/dev/null)" ] || blocked+=" $file"$'\n' + done < <(git diff --name-only HEAD "$target") + + if [ -n "$blocked" ]; then + print_error "Cannot switch to '$target' - these files would be overwritten:" + printf '%s' "$blocked" >&2 + print_error "Aborting before publishing to Maven Central, which cannot be undone." + exit 1 + fi +} + +assert_can_switch_to master +assert_can_switch_to develop + +print_status "Performing Maven release (deploying to Sonatype)..." +mvn release:perform -DlocalCheckout=true +PUBLISHED=true + # Switch to master and merge only the release commit print_status "Merging release commit to master branch..." git checkout master diff --git a/src/main/java/com/atomgraph/linkeddatahub/Application.java b/src/main/java/com/atomgraph/linkeddatahub/Application.java index 03a1f401f5..06b268dc7b 100644 --- a/src/main/java/com/atomgraph/linkeddatahub/Application.java +++ b/src/main/java/com/atomgraph/linkeddatahub/Application.java @@ -181,7 +181,6 @@ import java.io.UnsupportedEncodingException; import java.nio.charset.StandardCharsets; import java.security.MessageDigest; -import java.util.Arrays; import java.util.List; import java.util.Locale; import java.util.Objects; @@ -228,6 +227,7 @@ import org.apache.http.protocol.HttpCoreContext; import org.apache.jena.query.DatasetFactory; import org.apache.jena.rdf.model.ModelFactory; +import org.apache.jena.rdf.model.RDFNode; import org.apache.jena.rdf.model.ResIterator; import org.apache.jena.rdf.model.Resource; import org.apache.jena.riot.resultset.ResultSetLang; @@ -358,7 +358,6 @@ public Application(@Context ServletConfig servletConfig) throws URISyntaxExcepti System.getProperty("com.atomgraph.linkeddatahub.validateAfterInactivity") != null ? Integer.valueOf(System.getProperty("com.atomgraph.linkeddatahub.validateAfterInactivity")) : null, servletConfig.getServletContext().getInitParameter(LDHC.maxImportThreads.getURI()) != null ? Integer.valueOf(servletConfig.getServletContext().getInitParameter(LDHC.maxImportThreads.getURI())) : null, servletConfig.getServletContext().getInitParameter(LDHC.notificationAddress.getURI()) != null ? servletConfig.getServletContext().getInitParameter(LDHC.notificationAddress.getURI()) : null, - servletConfig.getServletContext().getInitParameter(LDHC.supportedLanguages.getURI()) != null ? servletConfig.getServletContext().getInitParameter(LDHC.supportedLanguages.getURI()) : null, servletConfig.getServletContext().getInitParameter(LDHC.enableWebIDSignUp.getURI()) != null ? Boolean.parseBoolean(servletConfig.getServletContext().getInitParameter(LDHC.enableWebIDSignUp.getURI())) : true, servletConfig.getServletContext().getInitParameter(LDHC.oidcRefreshTokens.getURI()), servletConfig.getServletContext().getInitParameter(LDHC.frontendProxy.getURI()) != null ? servletConfig.getServletContext().getInitParameter(LDHC.frontendProxy.getURI()) : null, @@ -414,7 +413,6 @@ public Application(@Context ServletConfig servletConfig) throws URISyntaxExcepti * @param maxRequestRetries maximum number of times that the HTTP client will retry a request * @param maxImportThreads maximum number of threads used for asynchronous imports * @param notificationAddressString email address used to send notifications - * @param supportedLanguageCodes comma-separated codes of supported languages * @param enableWebIDSignUp true if WebID signup is enabled * @param oidcRefreshTokensPropertiesPath path to the properties file with OIDC refresh tokens * @param mailUser username of the SMTP email server @@ -443,7 +441,7 @@ public Application(final ServletConfig servletConfig, final MediaTypes mediaType final Integer cookieMaxAge, final boolean enableLinkedDataProxy, final boolean allowInternalUrls, final Integer maxContentLength, final Integer maxConnPerRoute, final Integer maxTotalConn, final Integer maxRequestRetries, final Integer connectionRequestTimeout, final Integer socketTimeout, final Integer connectTimeout, final Long connectionTimeToLive, final Integer validateAfterInactivity, final Integer maxImportThreads, - final String notificationAddressString, final String supportedLanguageCodes, final boolean enableWebIDSignUp, final String oidcRefreshTokensPropertiesPath, + final String notificationAddressString, final boolean enableWebIDSignUp, final String oidcRefreshTokensPropertiesPath, final String frontendProxyString, final String backendProxyAdminString, final String backendProxyEndUserString, final String mailUser, final String mailPassword, final String smtpHost, final String smtpPort, final String googleClientID, final String googleClientSecret, @@ -561,12 +559,15 @@ public Application(final ServletConfig servletConfig, final MediaTypes mediaType this.importThreadPool = Executors.newFixedThreadPool(maxImportThreads); servletConfig.getServletContext().setAttribute(LDHC.maxImportThreads.getURI(), importThreadPool); // used in ImportListener to shutdown the thread pool - if (supportedLanguageCodes == null) + try { - if (log.isErrorEnabled()) log.error("Supported languages ({}) not configured", LDHC.supportedLanguages.getURI()); - throw new ConfigurationException(LDHC.supportedLanguages); + this.supportedLanguages = readBundleLanguages(servletConfig.getServletContext()); + } + catch (IOException ex) + { + if (log.isErrorEnabled()) log.error("Could not read UI translations: {}", XSLTWriterBase.TRANSLATIONS_PATH, ex); + throw new IllegalStateException(ex); } - this.supportedLanguages = Arrays.asList(supportedLanguageCodes.split(",")).stream().map(code -> Locale.forLanguageTag(code)).collect(Collectors.toList()); this.servletConfig = servletConfig; this.mediaTypes = mediaTypes; @@ -832,13 +833,14 @@ protected PasswordAuthentication getPasswordAuthentication() xsltProc.registerExtensionFunction(new DecodeURI()); xsltProc.registerExtensionFunction(new com.atomgraph.linkeddatahub.writer.function.URLDecode()); xsltProc.registerExtensionFunction(new com.atomgraph.linkeddatahub.writer.function.SendHTTPRequest(xsltProc, client)); + xsltProc.registerExtensionFunction(new com.atomgraph.linkeddatahub.writer.function.ParseQuery()); try { - for (String prefix : getRepository().getPrefixMappings().keySet()) + for (String prefix : repository.getPrefixMappings().keySet()) { // register mapped RDF documents in the XSLT processor so that document() returns them cached, throughout multiple transformations - TreeInfo doc = xsltProc.getUnderlyingConfiguration().buildDocumentTree(getResolver().resolve("", prefix)); + TreeInfo doc = xsltProc.getUnderlyingConfiguration().buildDocumentTree(resolver.resolve("", prefix)); xsltProc.getUnderlyingConfiguration().getGlobalDocumentPool().add(doc, prefix); } @@ -2517,13 +2519,55 @@ public MessageDigest getMessageDigest() /** * Returns list of locales for languages supported by the UI. - * + * * @return locale list */ public List getSupportedLanguages() { return supportedLanguages; } + + /** + * Reads the languages the UI translation bundle actually provides. + * + * Derived from the bundle rather than configured separately: a hand-maintained list can claim a language the bundle does + * not have, and the two drifted - the config said en,es while the bundle is tagged en-US,es-ES, + * and neither described the languages of the data being rendered. + * + * @param servletContext servlet context + * @return locales, ordered by language tag so variant selection is deterministic + * @throws IOException if the bundle cannot be read + */ + public static List readBundleLanguages(ServletContext servletContext) throws IOException + { + try (InputStream translations = servletContext.getResourceAsStream(XSLTWriterBase.TRANSLATIONS_PATH)) + { + if (translations == null) throw new IOException("UI translations not found: " + XSLTWriterBase.TRANSLATIONS_PATH); + + return readBundleLanguages(translations); + } + } + + /** + * Reads the languages present in a UI translation bundle. + * + * @param translations RDF/XML translation bundle + * @return locales, ordered by language tag so variant selection is deterministic + */ + public static List readBundleLanguages(InputStream translations) + { + Model model = ModelFactory.createDefaultModel(); + RDFParser.create().source(translations).lang(Lang.RDFXML).build().parse(model); + + return model.listObjects().toList().stream(). + filter(RDFNode::isLiteral). + map(node -> node.asLiteral().getLanguage()). + filter(lang -> !lang.isEmpty()). + distinct(). + sorted(). + map(Locale::forLanguageTag). + collect(Collectors.toList()); + } /** * Returns true if WebID signup is enabled. diff --git a/src/main/java/com/atomgraph/linkeddatahub/client/GitHubClient.java b/src/main/java/com/atomgraph/linkeddatahub/client/GitHubClient.java index 0883195cd5..aa0dfce2e0 100644 --- a/src/main/java/com/atomgraph/linkeddatahub/client/GitHubClient.java +++ b/src/main/java/com/atomgraph/linkeddatahub/client/GitHubClient.java @@ -61,6 +61,10 @@ public class GitHubClient public static final String AUTHOR_EMAIL = "noreply@linkeddatahub.invalid"; /** Maximum retries on rate-limited requests */ public static final int MAX_RETRIES = 2; + /** Maximum retries on conflicting writes */ + public static final int MAX_CONFLICT_RETRIES = 3; + /** Delay before the first conflict retry, multiplied by the attempt number on subsequent ones */ + public static final long CONFLICT_RETRY_DELAY_MILLIS = 200; /** Commits requested per page when walking a file's history */ public static final int COMMITS_PER_PAGE = 100; /** Maximum history pages retrieved, bounding the API calls a single TimeMap request can make */ @@ -94,7 +98,8 @@ public GitHubClient(Client client, URI apiBase, String token, String owner, Stri /** * Creates or updates a file on the branch. - * The current blob SHA is fetched first, as the Contents API requires it for updates (optimistic locking). + * The current blob SHA is fetched first, as the Contents API requires it for updates (optimistic locking); + * a conflicting write is retried with a re-read SHA, up to {@link #MAX_CONFLICT_RETRIES} times. * * @param path file path within the repository * @param content file content @@ -104,32 +109,43 @@ public GitHubClient(Client client, URI apiBase, String token, String owner, Stri */ public Commit putFile(String path, byte[] content, String message, String authorName) { - Optional sha = getFileSha(path); + for (int attempt = 0; ; attempt++) + { + Optional sha = getFileSha(path); // re-probed on every attempt: a conflict means this SHA has moved - JsonObjectBuilder json = Json.createObjectBuilder(). - add("message", message). - add("content", Base64.getEncoder().encodeToString(content)). - add("branch", branch). - add("author", author(authorName)); - sha.ifPresent(s -> json.add("sha", s)); + JsonObjectBuilder builder = Json.createObjectBuilder(). + add("message", message). + add("content", Base64.getEncoder().encodeToString(content)). + add("branch", branch). + add("author", author(authorName)); + sha.ifPresent(s -> builder.add("sha", s)); + JsonObject json = builder.build(); // built once: a builder yields its object only to the first build() - try (Response response = invoke(() -> contents(path). - request(GITHUB_JSON). - header(HttpHeaders.AUTHORIZATION, authorization). - buildPut(Entity.entity(json.build(), MediaType.APPLICATION_JSON_TYPE)))) - { - if (response.getStatus() == Response.Status.OK.getStatusCode() || response.getStatus() == Response.Status.CREATED.getStatusCode()) + try (Response response = invoke(() -> contents(path). + request(GITHUB_JSON). + header(HttpHeaders.AUTHORIZATION, authorization). + buildPut(Entity.entity(json, MediaType.APPLICATION_JSON_TYPE)))) { - JsonObject result = response.readEntity(JsonObject.class); - return new Commit(result.getJsonObject("commit").getString("sha"), result.getJsonObject("content").getString("sha")); - } + if (response.getStatus() == Response.Status.OK.getStatusCode() || response.getStatus() == Response.Status.CREATED.getStatusCode()) + { + JsonObject result = response.readEntity(JsonObject.class); + return new Commit(result.getJsonObject("commit").getString("sha"), result.getJsonObject("content").getString("sha")); + } - throw new RuntimeException("GitHub commit of '" + path + "' to " + owner + "/" + repo + " failed with status " + response.getStatus()); + if (response.getStatus() == Response.Status.CONFLICT.getStatusCode() && attempt < MAX_CONFLICT_RETRIES) + { + backOff(attempt, path); + continue; + } + + throw new RuntimeException("GitHub commit of '" + path + "' to " + owner + "/" + repo + " failed with status " + response.getStatus()); + } } } /** * Deletes a file from the branch. A no-op if the file does not exist. + * A conflicting write is retried with a re-read SHA, up to {@link #MAX_CONFLICT_RETRIES} times. * * @param path file path within the repository * @param message commit message @@ -137,28 +153,38 @@ public Commit putFile(String path, byte[] content, String message, String author */ public void deleteFile(String path, String message, String authorName) { - Optional sha = getFileSha(path); - if (sha.isEmpty()) + for (int attempt = 0; ; attempt++) { - if (log.isDebugEnabled()) log.debug("File '{}' not found in {}/{}, nothing to delete", path, owner, repo); - return; - } + Optional sha = getFileSha(path); // re-probed on every attempt: a conflict means this SHA has moved + if (sha.isEmpty()) + { + if (log.isDebugEnabled()) log.debug("File '{}' not found in {}/{}, nothing to delete", path, owner, repo); + return; + } - JsonObject json = Json.createObjectBuilder(). - add("message", message). - add("sha", sha.get()). - add("branch", branch). - add("author", author(authorName)). - build(); + JsonObject json = Json.createObjectBuilder(). + add("message", message). + add("sha", sha.get()). + add("branch", branch). + add("author", author(authorName)). + build(); + + try (Response response = invoke(() -> contents(path). + request(GITHUB_JSON). + property(ClientProperties.SUPPRESS_HTTP_COMPLIANCE_VALIDATION, true). // the Contents API requires a body on DELETE + header(HttpHeaders.AUTHORIZATION, authorization). + build("DELETE", Entity.entity(json, MediaType.APPLICATION_JSON_TYPE)))) + { + if (response.getStatus() == Response.Status.OK.getStatusCode()) return; + + if (response.getStatus() == Response.Status.CONFLICT.getStatusCode() && attempt < MAX_CONFLICT_RETRIES) + { + backOff(attempt, path); + continue; + } - try (Response response = invoke(() -> contents(path). - request(GITHUB_JSON). - property(ClientProperties.SUPPRESS_HTTP_COMPLIANCE_VALIDATION, true). // the Contents API requires a body on DELETE - header(HttpHeaders.AUTHORIZATION, authorization). - build("DELETE", Entity.entity(json, MediaType.APPLICATION_JSON_TYPE)))) - { - if (response.getStatus() != Response.Status.OK.getStatusCode()) throw new RuntimeException("GitHub deletion of '" + path + "' from " + owner + "/" + repo + " failed with status " + response.getStatus()); + } } } @@ -273,6 +299,36 @@ public Optional getFileSha(String path) } } + /** + * Returns the repository owner. + * + * @return owner name + */ + public String getOwner() + { + return owner; + } + + /** + * Returns the repository name. + * + * @return repository name + */ + public String getRepo() + { + return repo; + } + + /** + * Returns the branch that commits are made on. + * + * @return branch name + */ + public String getBranch() + { + return branch; + } + private WebTarget contents(String path) { return endpoint.path("repos/{owner}/{repo}/contents/{path}"). @@ -323,6 +379,24 @@ private boolean isRateLimited(Response response) (response.getHeaderString("Retry-After") != null || "0".equals(response.getHeaderString("x-ratelimit-remaining"))); } + /** + * Sleeps before retrying a write that conflicted, backing off linearly to give the competing writer time to finish. + */ + private void backOff(int attempt, String path) + { + if (log.isWarnEnabled()) log.warn("Conflicting write of '{}' to {}/{} on branch '{}', retrying", path, owner, repo, branch); + + try + { + Thread.sleep(CONFLICT_RETRY_DELAY_MILLIS * (attempt + 1)); + } + catch (InterruptedException ex) + { + Thread.currentThread().interrupt(); + throw new RuntimeException(ex); + } + } + private long retryAfter(Response response) { String retryAfter = response.getHeaderString("Retry-After"); diff --git a/src/main/java/com/atomgraph/linkeddatahub/server/filter/request/OntologyFilter.java b/src/main/java/com/atomgraph/linkeddatahub/server/filter/request/OntologyFilter.java index 40cf8efcda..782e0aab92 100644 --- a/src/main/java/com/atomgraph/linkeddatahub/server/filter/request/OntologyFilter.java +++ b/src/main/java/com/atomgraph/linkeddatahub/server/filter/request/OntologyFilter.java @@ -32,10 +32,15 @@ import jakarta.ws.rs.container.ContainerRequestContext; import jakarta.ws.rs.container.ContainerRequestFilter; import jakarta.ws.rs.container.PreMatching; +import org.apache.jena.graph.Graph; +import org.apache.jena.graph.Node; +import org.apache.jena.graph.NodeFactory; +import org.apache.jena.graph.Triple; import org.apache.jena.ontapi.OntModelFactory; import org.apache.jena.ontapi.OntSpecification; import org.apache.jena.ontapi.UnionGraph; import org.apache.jena.ontapi.model.OntModel; +import org.apache.jena.ontapi.utils.Graphs; import org.apache.jena.rdf.model.Model; import org.apache.jena.rdf.model.ModelFactory; import org.apache.jena.vocabulary.RDF; @@ -169,48 +174,94 @@ public OntModel getOntology(Application app, String uri) } /** - * Assembles the ontology's owl:imports closure as a union graph. ontapi resolves the closure through - * a scoped repository view: raw per-document graphs are read through (and cached in) the shared - * repository, while ontapi's union-graph bookkeeping stays local to the view — the shared repository - * keeps serving raw document graphs, and duplicate ontology IDs across applications cannot collide. - * No inference is applied: all consumers traverse class/property hierarchies explicitly. + * Assembles the ontology's owl:imports closure composed with the ontologies of the imported + * packages. Each package ontology is declared as an owl:imports of the application ontology, and + * ontapi resolves it — along with its own transitive imports — as part of the closure, through the + * same scoped repository view as every other import. The declaration is derived from the + * application's ldh:import data on every load and never persisted, so the ldh:import triples remain + * the single source of truth. A package ontology that cannot be resolved is skipped so a broken + * package cannot take the application ontology down. * * @param repository graph repository * @param uri ontology URI + * @param packageOntologies package ontology URIs * @return closure union graph */ + public static UnionGraph loadOntology(PrefixGraphRepository repository, String uri, List packageOntologies) + { + if (!packageOntologies.isEmpty()) declarePackageImports(repository, uri, packageOntologies); + + return loadOntology(repository, uri); + } + /** - * Assembles the ontology's owl:imports closure composed with the ontologies of the imported - * packages. Each package ontology is assembled as its own closure union (so its owl:imports - * resolve too) and added as a member of the application ontology's union — derived in memory, - * mirroring the stylesheet composition in {@code XsltExecutableFilter}; no owl:imports triple - * is materialized anywhere. A package ontology that fails to load is skipped so a broken - * package cannot take the application ontology down. + * Declares the package ontologies as owl:imports of the application ontology on its base graph, so + * that ontapi pulls them into the imports closure when the model is constructed. + *

+ * The declaration is additive: {@code ClearOntology} evicts the raw graph before every reload, so an + * uninstalled package's import cannot survive in the cached copy. A reload path that skipped that + * eviction would need this to reconcile against {@code packageOntologies} rather than only add. * * @param repository graph repository * @param uri ontology URI * @param packageOntologies package ontology URIs - * @return closure union graph */ - public static UnionGraph loadOntology(PrefixGraphRepository repository, String uri, List packageOntologies) + public static void declarePackageImports(PrefixGraphRepository repository, String uri, List packageOntologies) { - UnionGraph union = loadOntology(repository, uri); + Graph base = repository.get(uri); + Optional name = Graphs.findOntologyNameNode(base); + if (name.isEmpty()) + { + if (log.isErrorEnabled()) log.error("Ontology with URI '{}' carries no ontology header, cannot import packages {} into it", uri, packageOntologies); + return; + } for (URI packageOntology : packageOntologies) { - try + // the model is constructed with ignoreUnresolvedImports, which silently substitutes an empty + // graph for an import it cannot resolve — resolve it here so that a broken package is reported + // instead of composing as nothing + if (!isResolvable(repository, packageOntology.toString())) { - union.addSubGraph(loadOntology(repository, packageOntology.toString())); - } - catch (RuntimeException ex) - { - if (log.isErrorEnabled()) log.error("Could not load package ontology '{}', skipping it", packageOntology, ex); + if (log.isErrorEnabled()) log.error("Could not load package ontology '{}', skipping it", packageOntology); + continue; } + + base.add(Triple.create(name.get(), OWL.imports.asNode(), NodeFactory.createURI(packageOntology.toString()))); } + } - return union; + /** + * Returns true if the repository can supply a graph for the given ID. + * + * @param repository graph repository + * @param id graph ID + * @return true if resolvable + */ + public static boolean isResolvable(PrefixGraphRepository repository, String id) + { + try + { + return repository.get(id) != null; + } + catch (RuntimeException ex) // unmapped location, 404, connection refused, unparseable document... + { + if (log.isDebugEnabled()) log.debug("Could not resolve graph with ID '{}'", id, ex); + return false; + } } + /** + * Assembles the ontology's owl:imports closure as a union graph. ontapi resolves the closure through + * a scoped repository view: raw per-document graphs are read through (and cached in) the shared + * repository, while ontapi's union-graph bookkeeping stays local to the view — the shared repository + * keeps serving raw document graphs, and duplicate ontology IDs across applications cannot collide. + * No inference is applied: all consumers traverse class/property hierarchies explicitly. + * + * @param repository graph repository + * @param uri ontology URI + * @return closure union graph + */ public static UnionGraph loadOntology(PrefixGraphRepository repository, String uri) { if (log.isDebugEnabled()) log.debug("Started loading ontology with URI '{}'", uri); diff --git a/src/main/java/com/atomgraph/linkeddatahub/server/filter/response/ResponseHeadersFilter.java b/src/main/java/com/atomgraph/linkeddatahub/server/filter/response/ResponseHeadersFilter.java index 619f0806b7..a5a97df8d6 100644 --- a/src/main/java/com/atomgraph/linkeddatahub/server/filter/response/ResponseHeadersFilter.java +++ b/src/main/java/com/atomgraph/linkeddatahub/server/filter/response/ResponseHeadersFilter.java @@ -16,9 +16,11 @@ */ package com.atomgraph.linkeddatahub.server.filter.response; +import com.atomgraph.client.util.HTMLMediaTypePredicate; import com.atomgraph.client.vocabulary.AC; import com.atomgraph.client.vocabulary.LDT; import com.atomgraph.core.vocabulary.SD; +import com.atomgraph.linkeddatahub.server.util.LanguageNegotiator; import com.atomgraph.linkeddatahub.apps.model.Application; import com.atomgraph.linkeddatahub.apps.model.Dataset; import com.atomgraph.linkeddatahub.model.auth.Agent; @@ -65,6 +67,18 @@ public void filter(ContainerRequestContext request, ContainerResponseContext res if (response.getStatusInfo().equals(Response.Status.NO_CONTENT)) response.getHeaders().remove(HttpHeaders.CONTENT_TYPE); // needs to be explicitly unset for some reason + // Content-Language states the language of the representation, and must agree with the document's own . + // Both come from this one computation. Variant selection cannot supply it: it matched only when the reader's top + // preference happened to be an offered language, so it announced es on a page holding no Spanish and said nothing + // at all on a page that was entirely Lithuanian + // only where the rendering actually depends on language. An RDF representation is byte-identical for every reader - + // its literals carry their own tags and none is dropped - so it is intended for all language audiences, which RFC 9110 + // spells as no Content-Language at all. Labelling it would also contradict its own Vary, which carries no + // Accept-Language dimension for exactly the same reason + if (response.hasEntity() && response.getMediaType() != null && getApplication().isPresent() && new HTMLMediaTypePredicate().test(response.getMediaType())) + response.getHeaders().putSingle(HttpHeaders.CONTENT_LANGUAGE, + LanguageNegotiator.publishedTag(request.getAcceptableLanguages(), getSystem().getSupportedLanguages())); + if (request.getSecurityContext().getUserPrincipal() instanceof Agent) { Agent agent = ((Agent)(request.getSecurityContext().getUserPrincipal())); diff --git a/src/main/java/com/atomgraph/linkeddatahub/server/model/impl/DocumentHierarchyGraphStoreImpl.java b/src/main/java/com/atomgraph/linkeddatahub/server/model/impl/DocumentHierarchyGraphStoreImpl.java index f97d7d4ec3..8adcecbb27 100644 --- a/src/main/java/com/atomgraph/linkeddatahub/server/model/impl/DocumentHierarchyGraphStoreImpl.java +++ b/src/main/java/com/atomgraph/linkeddatahub/server/model/impl/DocumentHierarchyGraphStoreImpl.java @@ -746,7 +746,10 @@ public com.atomgraph.core.model.impl.Response getInternalResponse(Model model, U getWritableMediaTypes(Model.class), getLanguages(), getEncodings(), - new HTMLMediaTypePredicate()); + new HTMLMediaTypePredicate(), + // the HTML rendering negotiates over the whole accepted-language list, so two requests selecting the same + // variant can still differ; passing it makes the entity tag tell those representations apart + getHttpHeaders().getAcceptableLanguages()); } /** diff --git a/src/main/java/com/atomgraph/linkeddatahub/server/util/GraphVersioningService.java b/src/main/java/com/atomgraph/linkeddatahub/server/util/GraphVersioningService.java index f60a371854..87adb2995a 100644 --- a/src/main/java/com/atomgraph/linkeddatahub/server/util/GraphVersioningService.java +++ b/src/main/java/com/atomgraph/linkeddatahub/server/util/GraphVersioningService.java @@ -61,8 +61,9 @@ * Mirrors named graphs of versioning-enabled applications into GitHub repositories. * The unit of work is reconciliation: a task re-reads the graph from the application's * SPARQL service at execution time and makes the repository file match — including deleting - * the file when the graph is gone. Tasks for the same file are chained sequentially so - * commits never race the GitHub Contents API's SHA-based optimistic locking. + * the file when the graph is gone. Tasks for the same repository branch are chained sequentially: + * the GitHub Contents API takes its optimistic lock on the branch head rather than on the file, so + * commits to one branch conflict with each other no matter which files they touch. * Versioning is best-effort: task failures are logged, never propagated. * * @author Martynas Jusevičius {@literal } @@ -79,7 +80,7 @@ public class GraphVersioningService public record Repository(GitHubClient client, String pathPrefix) { } private final Map repositories; - private final Map> commitChains = new ConcurrentHashMap<>(); + private final Map> commitChains = new ConcurrentHashMap<>(); // keyed by repository branch private final ExecutorService executor = Executors.newFixedThreadPool(4); /** @@ -149,7 +150,7 @@ public Optional getRepository(String appURI) /** * Schedules asynchronous reconciliation of a graph's repository file with its current store state. - * Chained per file path; returns immediately. + * Chained per repository branch; returns immediately. * * @param serviceContext deployment context of the application's SPARQL service * @param appURI application URI @@ -165,8 +166,10 @@ public void commitAsync(ServiceContext serviceContext, String appURI, URI appBas String path = path(repository.pathPrefix(), appBase, graphURI); String message = method + " " + graphURI; + // all commits to a branch share one chain: two files committed concurrently would race the branch head + String repositoryBranch = repository.client().getOwner() + "/" + repository.client().getRepo() + "/" + repository.client().getBranch(); - commitChains.compute(path, (key, chain) -> + commitChains.compute(repositoryBranch, (key, chain) -> { CompletableFuture previous = chain != null ? chain : CompletableFuture.completedFuture(null); CompletableFuture next = previous.thenRunAsync(() -> reconcile(serviceContext, repository, path, graphURI, message, agentWebID), executor). diff --git a/src/main/java/com/atomgraph/linkeddatahub/server/util/LanguageNegotiator.java b/src/main/java/com/atomgraph/linkeddatahub/server/util/LanguageNegotiator.java new file mode 100644 index 0000000000..09d9d2cdbf --- /dev/null +++ b/src/main/java/com/atomgraph/linkeddatahub/server/util/LanguageNegotiator.java @@ -0,0 +1,93 @@ +/** + * Copyright 2026 Martynas Jusevičius + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.atomgraph.linkeddatahub.server.util; + +import java.util.List; +import java.util.Locale; + +/** + * Picks the language a representation is composed in from the languages a reader accepts. + * + * @author {@literal Martynas Jusevičius } + */ +public class LanguageNegotiator +{ + + /** + * Returns the language a representation is composed in: the highest-ranked language the reader accepts that the UI + * actually provides, falling back to the first supported language when the reader accepts none of them. + * + * This is deliberately not the reader's top preference. Asking for German does not make a page German, and reporting + * the request as the content's language is what put lang="de" on pages written entirely in English. + * + * Matching is RFC 4647 Basic Filtering through {@link Locale#filter}, not Lookup. The supported languages are derived + * from the UI translation bundle and carry region-qualified tags (en-US), while Accept-Language arrives as + * primary subtags (en). Lookup truncates the range and so returns only tags equal to or less specific than + * it, which does not match that direction; Basic Filtering does. + * + * @param acceptableLanguages the reader's languages, highest priority first + * @param supportedLanguages the languages the UI provides + * @return the composition language, never null + */ + public static Locale negotiate(List acceptableLanguages, List supportedLanguages) + { + if (supportedLanguages.isEmpty()) return Locale.ENGLISH; + + for (Locale acceptable : acceptableLanguages) + { + // JAX-RS reports a wildcard Accept-Language as a locale whose language is "*"; it expresses no preference + if (acceptable.getLanguage().isEmpty() || acceptable.getLanguage().equals("*")) continue; + + List matches = Locale.filter(List.of(new Locale.LanguageRange(acceptable.toLanguageTag())), supportedLanguages); + if (!matches.isEmpty()) return matches.get(0); + } + + return supportedLanguages.get(0); + } + + /** + * Returns the tag to publish for a representation: the negotiated language, carrying its region subtag only where the + * bundle actually distinguishes more than one variant of that language. + * + * BCP 47 puts it as "Region subtags MAY be omitted, as when it adds no distinguishing value to the tag" (RFC 5646 + * section 2.2.4), and asks in section 4.1 for a tag no more specific than is justified. The region in en-US + * is an artifact of how the translation bundle is keyed rather than a claim that the page is American English, and + * RFC 9110 section 8.5 reads Content-Language as the audience the representation is intended for - so publishing + * es-ES announces it is meant for readers of Castilian Spanish. + * + * Over-specifying also costs something measurable under RFC 4647: a reader asking for es-MX reaches a page + * tagged es by truncating their own range, and never reaches one tagged es-ES. Ship + * pt-BR alongside pt-PT and the region does distinguish, so there both keep it. + * + * The bundle's own xml:lang keys stay as they are. XPath's fn:lang() prefix-matches in the + * opposite direction - fn:lang('es') matches xml:lang="es-ES" but not the reverse - so a + * specific key is the more reachable one. Same rule, opposite optimum: the internal key and the published claim are not + * the same string. + * + * @param acceptableLanguages the reader's languages, highest priority first + * @param supportedLanguages the languages the UI provides + * @return a BCP 47 language tag, never null + */ + public static String publishedTag(List acceptableLanguages, List supportedLanguages) + { + Locale negotiated = negotiate(acceptableLanguages, supportedLanguages); + long variants = supportedLanguages.stream().filter(supported -> supported.getLanguage().equals(negotiated.getLanguage())).count(); + + return variants > 1 ? negotiated.toLanguageTag() : negotiated.getLanguage(); + } + +} diff --git a/src/main/java/com/atomgraph/linkeddatahub/writer/function/ParseQuery.java b/src/main/java/com/atomgraph/linkeddatahub/writer/function/ParseQuery.java new file mode 100644 index 0000000000..47f67434b4 --- /dev/null +++ b/src/main/java/com/atomgraph/linkeddatahub/writer/function/ParseQuery.java @@ -0,0 +1,139 @@ +/** + * Copyright 2026 Martynas Jusevičius + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.atomgraph.linkeddatahub.writer.function; + +import com.atomgraph.linkeddatahub.vocabulary.LDH; +import net.sf.saxon.s9api.ExtensionFunction; +import net.sf.saxon.s9api.ItemType; +import net.sf.saxon.s9api.OccurrenceIndicator; +import net.sf.saxon.s9api.QName; +import net.sf.saxon.s9api.SaxonApiException; +import net.sf.saxon.s9api.SequenceType; +import net.sf.saxon.s9api.XdmAtomicValue; +import net.sf.saxon.s9api.XdmValue; +import org.apache.jena.atlas.json.JsonArray; +import org.apache.jena.atlas.json.JsonObject; +import org.apache.jena.datatypes.xsd.XSDDatatype; +import org.apache.jena.graph.Node; +import org.apache.jena.graph.Triple; +import org.apache.jena.query.Query; +import org.apache.jena.query.QueryFactory; +import org.apache.jena.query.QueryParseException; +import org.apache.jena.sparql.syntax.Element; +import org.apache.jena.sparql.syntax.ElementGroup; + +/** + * Server-side counterpart of the browser's SPARQL.js 2.x Parser, backing the Saxon + * declaration of the ldh:parse-query stylesheet function so that + * ldh:construct-instance runs identically on both products. + *

+ * This is deliberately NOT a full SPARQL.js replacement: it emits only the subset of the parse + * tree that the stylesheets consume — a template array of triple maps and a + * where array whose non-emptiness marks a query that cannot be instantiated as a pure + * template (SPARQL.js puts the actual parsed patterns there; only the emptiness test is shared). + * Term strings follow the SPARQL.js 2.x serialization expected by + * ldh:triples-to-descriptions: bare URIs (no angle brackets), _:label + * blank nodes, ?name variables, and "lex" / + * "lex"@lang / "lex"^^datatypeURI literals with the datatype URI + * unwrapped — a 2.x quirk that the stylesheet regexes depend on. Divergences from SPARQL.js: + * blank node labels are Jena's parser-allocated ones, not the source labels (downstream only needs + * per-query consistency — ldh:instance-term prefixes them per constructor); variables + * are normalized to the ? sigil (SPARQL.js preserves $, and the + * stylesheets accept both); plain literals are emitted without ^^xsd:string to match + * SPARQL.js 2.x, although Jena types them as such per RDF 1.1. + *

+ * If SPARQL.js is ever upgraded to 3.x (see the TO-DO in ldh:triples-to-descriptions), + * this function, ldh:triples-to-descriptions and ldh:construct-instance + * must change in lockstep. + * + * @author {@literal Martynas Jusevičius } + */ +public class ParseQuery implements ExtensionFunction +{ + + @Override + public QName getName() + { + return new QName(LDH.NS, "parse-query"); + } + + @Override + public SequenceType getResultType() + { + return SequenceType.makeSequenceType(ItemType.STRING, OccurrenceIndicator.ONE); + } + + @Override + public SequenceType[] getArgumentTypes() + { + return new SequenceType[] + { + SequenceType.makeSequenceType(ItemType.STRING, OccurrenceIndicator.ONE), + }; + } + + @Override + public XdmValue call(XdmValue[] arguments) throws SaxonApiException + { + try + { + Query query = QueryFactory.create(arguments[0].itemAt(0).getStringValue()); + JsonObject json = new JsonObject(); + JsonArray template = new JsonArray(); + JsonArray where = new JsonArray(); + + if (query.isConstructType()) + for (Triple triple : query.getConstructTemplate().getBGP().getList()) + { + JsonObject map = new JsonObject(); + map.put("subject", term(triple.getSubject())); + map.put("predicate", term(triple.getPredicate())); + map.put("object", term(triple.getObject())); + template.add(map); + } + else + where.add(new JsonObject()); // not a CONSTRUCT: mark as non-instantiable + + Element pattern = query.getQueryPattern(); + if (pattern != null && !(pattern instanceof ElementGroup && ((ElementGroup)pattern).isEmpty())) + where.add(new JsonObject()); + + json.put("template", template); + json.put("where", where); + return new XdmAtomicValue(json.toString()); + } + catch (QueryParseException ex) + { + throw new SaxonApiException(ex); + } + } + + private String term(Node node) + { + if (node.isVariable()) return "?" + node.getName(); + if (node.isBlank()) return "_:" + node.getBlankNodeLabel(); + if (node.isLiteral()) + { + String lex = "\"" + node.getLiteralLexicalForm() + "\""; + if (!node.getLiteralLanguage().isEmpty()) return lex + "@" + node.getLiteralLanguage(); + if (node.getLiteralDatatypeURI() != null && !node.getLiteralDatatypeURI().equals(XSDDatatype.XSDstring.getURI())) return lex + "^^" + node.getLiteralDatatypeURI(); + return lex; + } + return node.getURI(); + } + +} diff --git a/src/main/java/com/atomgraph/server/mapper/SPINConstraintViolationExceptionMapper.java b/src/main/java/com/atomgraph/server/mapper/SPINConstraintViolationExceptionMapper.java index 5da173d19d..62e28f4eb5 100644 --- a/src/main/java/com/atomgraph/server/mapper/SPINConstraintViolationExceptionMapper.java +++ b/src/main/java/com/atomgraph/server/mapper/SPINConstraintViolationExceptionMapper.java @@ -25,6 +25,8 @@ import org.apache.jena.rdf.model.Resource; import org.apache.jena.rdf.model.ResourceFactory; import org.apache.jena.vocabulary.RDF; +import org.apache.jena.vocabulary.RDFS; +import com.atomgraph.spinrdf.constraints.ConstraintViolation; import com.atomgraph.spinrdf.constraints.SPINConstraints; import com.atomgraph.spinrdf.vocabulary.SPIN; import jakarta.inject.Inject; @@ -50,6 +52,19 @@ public Response toResponse(SPINConstraintViolationException ex) ex.getModel().add(exception.getModel()); SPINConstraints.addConstraintViolationsRDF(ex.getConstraintViolations(), ex.getModel(), true); + + // describe each violation's spin:violationSource within the response, so that clients can tell the + // constraint kind (e.g. ldh:MissingPropertyValue) and read its authored label without the ontology + for (ConstraintViolation cv : ex.getConstraintViolations()) + { + Resource source = cv.getSource(); + if (source != null) + { + ex.getModel().add(source.listProperties(RDF.type)); + ex.getModel().add(source.listProperties(RDFS.label)); + } + } + ResIterator it = ex.getModel().listSubjectsWithProperty(RDF.type, SPIN.ConstraintViolation); try { diff --git a/src/main/java/com/atomgraph/server/vocabulary/LDT.java b/src/main/java/com/atomgraph/server/vocabulary/LDT.java index 28cb52bcf9..e9bb82e871 100644 --- a/src/main/java/com/atomgraph/server/vocabulary/LDT.java +++ b/src/main/java/com/atomgraph/server/vocabulary/LDT.java @@ -94,8 +94,6 @@ public static String getURI() public static final Property cacheControl = m_model.createDataProperty( NS + "cacheControl" ); - public static final Property lang = m_model.createObjectProperty( NS + "lang" ); - public static final Property template = m_model.createObjectProperty( NS + "template" ); } diff --git a/src/main/resources/com/atomgraph/linkeddatahub/app/admin/lacl.ttl b/src/main/resources/com/atomgraph/linkeddatahub/app/admin/lacl.ttl index a45a58977a..b547724968 100644 --- a/src/main/resources/com/atomgraph/linkeddatahub/app/admin/lacl.ttl +++ b/src/main/resources/com/atomgraph/linkeddatahub/app/admin/lacl.ttl @@ -76,7 +76,7 @@ rdfs:isDefinedBy : . :MissingRequestMode a ldh:MissingPropertyValue ; - rdfs:label "Missing request agent" ; + rdfs:label "Missing request mode" ; sp:arg1 :requestMode ; rdfs:isDefinedBy : . diff --git a/src/main/resources/com/atomgraph/linkeddatahub/ldh.ttl b/src/main/resources/com/atomgraph/linkeddatahub/ldh.ttl index c6a2c85232..dcf00270b1 100644 --- a/src/main/resources/com/atomgraph/linkeddatahub/ldh.ttl +++ b/src/main/resources/com/atomgraph/linkeddatahub/ldh.ttl @@ -437,7 +437,7 @@ WHERE { rdfs:isDefinedBy : . :PropertyCardinality a spin:Template ; - rdfs:label "Missing property value" ; + rdfs:label "Property cardinality violation" ; spin:body :ConstructPropertyCardinality ; spin:labelTemplate "Instance violates the cardinality of RDF property {?arg1}: {?arg2}" ; spin:constraint [ a spl:Argument ; diff --git a/src/main/webapp/WEB-INF/web.xml b/src/main/webapp/WEB-INF/web.xml index decd5bcf9a..27c4a5440f 100644 --- a/src/main/webapp/WEB-INF/web.xml +++ b/src/main/webapp/WEB-INF/web.xml @@ -7,10 +7,6 @@ LinkedDataHub The low-code Knowledge Graph application platform - - https://w3id.org/atomgraph/linkeddatahub/config#supportedLanguages - en,es - https://w3id.org/atomgraph/linkeddatahub/config#authCacheControl max-age=69 diff --git a/src/main/webapp/static/com/atomgraph/linkeddatahub/css/app.css b/src/main/webapp/static/com/atomgraph/linkeddatahub/css/app.css new file mode 100644 index 0000000000..c9104dab57 --- /dev/null +++ b/src/main/webapp/static/com/atomgraph/linkeddatahub/css/app.css @@ -0,0 +1,3772 @@ +/* ========================================================================= + LinkedDataHub App UI kit — composition styles + Builds on ../../colors_and_type.css and the core primitives. Keep tokens here + to a minimum; this file is for layout / component skins. + ========================================================================= */ + +/* Core primitives (incl. the .msi icon base) — app pages link app.css directly, + so the dependency is declared here rather than assumed from styles.css. */ +@import url('core.css'); + +* { box-sizing: border-box; } +html, body { margin: 0; padding: 0; } +body { background: var(--bg-page); color: var(--fg-1); font-family: var(--font-sans); } + +a { color: var(--fg-link); text-decoration: none; } +a:hover { text-decoration: underline; text-underline-offset: 2px; } + +button { font-family: inherit; cursor: pointer; } + +/* Material Symbols (.msi) now lives in ui_kits/core/core.css — the icon + primitive is core-level. app.css inherits it via styles.css. */ + +/* ---------- App shell ---------- */ +.app { + min-height: 100vh; + display: flex; + flex-direction: column; +} + +/* Top header — dark accent gradient with knowledge-graph dot pattern overlay */ +.ldh-header { + position: relative; + height: 56px; + padding: 0 var(--sp-6); + display: grid; + grid-template-columns: 220px 1fr auto; + align-items: center; + gap: var(--sp-6); + background: var(--grad-deep); + color: #fff; + flex-shrink: 0; + isolation: isolate; /* contain ::before stacking without clipping descendants */ +} +.ldh-header::before { + content: ""; + position: absolute; inset: 0; + background-image: radial-gradient(circle at 1px 1px, rgba(255,255,255,0.10) 1px, transparent 1.4px); + background-size: 22px 22px; + pointer-events: none; + opacity: 0.55; + z-index: -1; /* sit under header children so dropdowns can overflow downward */ +} +.ldh-header > * { position: relative; } + +.ldh-wordmark { + display: flex; align-items: center; gap: var(--sp-2); + font-family: var(--font-sans); + font-weight: var(--fw-semibold); + font-size: 16px; + letter-spacing: var(--tracking-snug); + color: #fff; + text-decoration: none; +} +.ldh-wordmark .mark { + width: 24px; height: 24px; + border-radius: 6px; + background: linear-gradient(135deg, var(--ldh-blue-400) 0%, var(--ldh-violet-400) 100%); + position: relative; + box-shadow: inset 0 0 0 1px rgba(255,255,255,0.18); +} +.ldh-wordmark .mark::before, .ldh-wordmark .mark::after { + content: ""; position: absolute; border-radius: 50%; background: rgba(255,255,255,0.9); +} +.ldh-wordmark .mark::before { width: 6px; height: 6px; top: 4px; left: 4px; } +.ldh-wordmark .mark::after { width: 4px; height: 4px; bottom: 5px; right: 5px; } + +.ldh-address { + display: flex; align-items: center; + height: 36px; + min-width: 0; + width: 100%; + max-width: 720px; + padding: 0 12px; + background: rgba(255,255,255,0.08); + border: 1px solid rgba(255,255,255,0.14); + border-radius: var(--r-md); + color: #fff; + gap: 8px; + justify-self: center; +} +.ldh-address:focus-within { + background: rgba(255,255,255,0.16); + border-color: rgba(255,255,255,0.32); + box-shadow: 0 0 0 4px rgba(77, 148, 248, 0.18); +} +.ldh-address .msi { color: rgba(255,255,255,0.72); } +.ldh-address input { + flex: 1; + min-width: 0; + background: transparent; + border: 0; + outline: 0; + color: #fff; + font-family: var(--font-mono); + letter-spacing: 0.02em; + font-size: var(--fs-sm); +} +.ldh-address input::placeholder { color: rgba(255,255,255,0.55); font-family: var(--font-mono); letter-spacing: 0.02em; } + +.ldh-search { + display: flex; align-items: center; + height: 36px; + background: rgba(255,255,255,0.10); + border: 1px solid rgba(255,255,255,0.14); + border-radius: var(--r-md); + padding: 0 var(--sp-3); + gap: var(--sp-2); + color: rgba(255,255,255,0.92); + transition: background var(--dur-base) var(--ease-out-soft), border-color var(--dur-base) var(--ease-out-soft); + max-width: 720px; + width: 100%; + justify-self: center; +} +.ldh-search:focus-within { + background: rgba(255,255,255,0.16); + border-color: rgba(255,255,255,0.32); + box-shadow: 0 0 0 4px rgba(77, 148, 248, 0.18); +} +.ldh-search .msi { color: rgba(255,255,255,0.72); } +.ldh-search input { + flex: 1; + background: transparent; + border: 0; + outline: 0; + color: #fff; + font-family: var(--font-sans); + font-size: var(--fs-sm); +} +.ldh-search input::placeholder { color: rgba(255,255,255,0.55); font-family: var(--font-mono); letter-spacing: 0.02em; } +.ldh-search kbd { + font-family: var(--font-mono); + font-size: 10px; + color: rgba(255,255,255,0.55); + padding: 2px 6px; + border: 1px solid rgba(255,255,255,0.18); + border-radius: 4px; +} + +.ldh-header-actions { display: flex; align-items: center; gap: var(--sp-2); } +/* The header's icon buttons take the HEADER's ink by inheritance, never a + hardcoded literal: m3 makes the bar dark (light ink) while base44 makes it + light (dark ink), and `intent="inverse"` would pin white in both. Hover tints + with currentColor so it lands correctly on either. This is why the button + needs no per-skin colour patch at all. */ +.ldh-header .ldhc-iconbtn { color: inherit; } +.ldh-header .ldhc-iconbtn:hover { background: color-mix(in srgb, currentColor 12%, transparent); color: inherit; } +.ldh-header .ldhc-iconbtn:active { background: color-mix(in srgb, currentColor 18%, transparent); } + +/* Header icon buttons are core ; the + unread dot is a core . The old .ldh-icon-btn hardcoded + rgba(255,255,255,.10) and rgba(10,45,87,.95) — raw values, not tokens. */ + +.ldh-avatar-wrap { position: relative; display: inline-flex; } +.ldh-user-menu { + position: absolute; + top: calc(100% + 8px); + right: 0; + min-width: 248px; + background: var(--bg-card); + border: 1px solid var(--border-default); + border-radius: var(--r-md); + box-shadow: 0 16px 40px -12px rgba(15, 23, 42, 0.28), 0 4px 12px -4px rgba(15, 23, 42, 0.18); + padding: var(--sp-2); + z-index: 70; + animation: menu-pop var(--dur-fast) var(--ease-out-soft); + color: var(--fg-1); +} +@keyframes menu-pop { + from { opacity: 0; transform: translateY(-4px) scale(0.98); } + to { opacity: 1; transform: translateY(0) scale(1); } +} +.ldh-user-menu .umhead { + display: flex; align-items: center; gap: var(--sp-2); + padding: var(--sp-2); +} +.ldh-user-menu .umavatar { + width: 36px; height: 36px; + border-radius: 50%; + background: linear-gradient(135deg, var(--ldh-blue-400), var(--ldh-violet-400)); + color: #fff; + display: inline-flex; align-items: center; justify-content: center; + font-size: var(--fs-xs); + font-weight: var(--fw-semibold); + letter-spacing: 0.04em; + flex-shrink: 0; +} +.ldh-user-menu .umid { min-width: 0; } +.ldh-user-menu .umname { + font-weight: var(--fw-semibold); + font-size: var(--fs-sm); + color: var(--fg-1); + white-space: nowrap; overflow: hidden; text-overflow: ellipsis; +} +.ldh-user-menu .umemail { + font-size: 11px; + color: var(--fg-2); + font-family: var(--font-mono); + white-space: nowrap; overflow: hidden; text-overflow: ellipsis; +} +.ldh-user-menu .umdiv { + height: 1px; + background: var(--border-default); + margin: var(--sp-1) 0; +} +.ldh-user-menu .umitem { + display: flex; align-items: center; gap: var(--sp-2); + width: 100%; + padding: var(--sp-2); + border: 0; + background: transparent; + border-radius: var(--r-sm); + cursor: pointer; + font-size: var(--fs-sm); + color: var(--fg-1); + text-align: left; + font-family: inherit; +} +.ldh-user-menu .umitem:hover { background: var(--surface-2); } +.ldh-user-menu .umitem.is-danger { color: var(--danger-500); } +.ldh-user-menu .umitem.is-danger:hover { background: rgba(180, 35, 24, 0.08); } +.ldh-user-menu .umitem .msi { color: inherit; } + +/* ---------- Tabs row (dataspaces) ---------- */ +.ldh-tabs { + display: flex; + align-items: stretch; + gap: 2px; + padding: 0 var(--sp-6); + background: var(--surface-1); + border-bottom: 1px solid var(--border-default); + height: 40px; +} +.ldh-tab { + display: inline-flex; align-items: center; gap: var(--sp-2); + padding: 0 var(--sp-4); + font-size: var(--fs-sm); + font-weight: var(--fw-medium); + color: var(--fg-muted); + border: 0; + background: transparent; + border-bottom: 2px solid transparent; + transition: color var(--dur-fast) var(--ease-out-soft), border-color var(--dur-fast) var(--ease-out-soft); +} +.ldh-tab:hover { color: var(--fg-1); } +/* Active marker uses the semantic --fg-selected (not a raw ramp step), so it + matches core's .ldhc-tab.is-on and follows any skin without a per-skin patch. */ +.ldh-tab.is-active { color: var(--fg-selected); border-bottom-color: var(--fg-selected); } +.ldh-tab .tab-close { + width: 18px; height: 18px; display: inline-flex; align-items: center; justify-content: center; + border-radius: var(--r-xs); color: var(--fg-hint); +} +.ldh-tab .tab-close:hover { background: var(--bg-hover); color: var(--fg-1); } + +/* ---------- Action bar (Add / Breadcrumb / Toolbar) ---------- */ +/* Action bar Create dropdown — Container / Item picker. */ +.ldh-add-wrap { position: relative; display: inline-flex; } +.ldh-add-wrap .ldhc-btn.is-open .caret { transform: rotate(180deg); } +.ldh-add-wrap .ldhc-btn .caret { transition: transform var(--dur-fast) var(--ease-out-soft); } +.ldh-add-menu { + position: absolute; + top: calc(100% + 6px); + left: 0; + min-width: 280px; + background: var(--bg-card); + border: 1px solid var(--border-default); + border-radius: var(--r-md); + box-shadow: 0 16px 40px -12px rgba(15, 23, 42, 0.28), 0 4px 12px -4px rgba(15, 23, 42, 0.18); + padding: var(--sp-2); + z-index: 50; + animation: menu-pop var(--dur-fast) var(--ease-out-soft); + color: var(--fg-1); +} +.ldh-add-menu .hd { + padding: 6px 8px 4px; + font-family: var(--font-mono); + font-size: 10px; + letter-spacing: 0.04em; + text-transform: uppercase; + color: var(--fg-muted); +} +.ldh-add-menu .it { + display: grid; + grid-template-columns: 32px 1fr; + align-items: center; + gap: 10px; + width: 100%; + padding: 8px; + border: 0; + background: transparent; + border-radius: var(--r-sm); + cursor: pointer; + font: inherit; + text-align: left; + color: var(--fg-1); +} +.ldh-add-menu .it:hover { background: var(--surface-1); } +.ldh-add-menu .it .ico { + width: 32px; height: 32px; + display: inline-flex; align-items: center; justify-content: center; + border-radius: var(--r-sm); + background: var(--bg-accent-quiet); + color: var(--ldh-blue-700); +} +.ldh-add-menu .it .body { display: flex; flex-direction: column; min-width: 0; } +.ldh-add-menu .it .body .lbl { + font-size: var(--fs-sm); + font-weight: var(--fw-medium); + color: var(--fg-1); +} +.ldh-add-menu .it .body .sub { + font-size: var(--fs-xs); + color: var(--fg-2); +} + +.ldh-actionbar { + display: grid; + grid-template-columns: auto 1fr auto; + align-items: center; + gap: var(--sp-6); + padding: var(--sp-3) var(--sp-6); + background: var(--surface-0); + border-bottom: 1px solid var(--border-default); + flex-shrink: 0; +} + +/* Buttons live in ui_kits/core/core.css as .ldhc-btn — the ONE button system. + The app kit's legacy .ldh-btn is gone; markup uses the core class directly. */ + +/* ---------- Breadcrumb · Drive (Google-Drive-flavored, default) ---------- + Roomy horizontal path. Ancestors are quiet medium-weight links with a hover + pill; the current resource is large + bold with a dropdown caret. */ +.ldh-bc-drive { + display: flex; align-items: center; + gap: 2px; + min-width: 0; +} +.ldh-bc-drive .bc-d-up { + display: inline-flex; align-items: center; + padding: 4px 9px; + border-radius: var(--r-md); + color: var(--fg-2); + font-size: var(--fs-base); + font-weight: var(--fw-medium); + white-space: nowrap; + transition: background var(--dur-fast) var(--ease-out-soft), color var(--dur-fast) var(--ease-out-soft); +} +.ldh-bc-drive .bc-d-up:hover { + background: var(--surface-2); + color: var(--fg-1); + text-decoration: none; +} +.ldh-bc-drive .bc-d-sep { + color: var(--fg-hint); + font-size: 18px; + opacity: 0.7; + flex-shrink: 0; +} +.ldh-bc-drive .bc-d-here { + display: inline-flex; align-items: center; + padding: 4px 9px; + border-radius: var(--r-md); + font: inherit; + color: var(--fg-1); + min-width: 0; +} +.ldh-bc-drive .bc-d-name { + font-size: var(--fs-base); + font-weight: var(--fw-medium); + color: var(--fg-1); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + min-width: 0; +} + +/* ---------- Breadcrumb · Stack (modern hero) ---------- + Two stacked lines in the middle of the actionbar: + · Parent path — tiny uppercase mono "you came from here" trail. + · Current — large bold name + icon chip + optional class badge. + Reads as a "you are here" hero rather than a list of equal links. */ +.ldh-bc-stack { + display: flex; flex-direction: column; + justify-content: center; + gap: 2px; + min-width: 0; + line-height: 1.2; +} +.ldh-bc-stack .bc-path { + display: flex; align-items: center; gap: 4px; + font-family: var(--font-mono); + font-size: 10px; + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--fg-muted); + min-width: 0; +} +.ldh-bc-stack .bc-up { + display: inline-flex; align-items: center; gap: 4px; + padding: 1px 4px; + border-radius: 4px; + color: inherit; +} +.ldh-bc-stack .bc-up:hover { + background: var(--surface-2); + color: var(--fg-2); + text-decoration: none; +} +.ldh-bc-stack .bc-up .msi { font-size: 13px; opacity: 0.7; } +.ldh-bc-stack .bc-sep { + color: var(--fg-hint); + opacity: 0.55; + font-size: 11px; +} +.ldh-bc-stack .bc-here { + display: inline-flex; align-items: center; gap: 8px; + min-width: 0; +} +.ldh-bc-stack .bc-here-ico { + width: 24px; height: 24px; + border-radius: 6px; + background: var(--bg-accent-quiet); + color: var(--ldh-blue-700); + display: inline-flex; align-items: center; justify-content: center; + flex-shrink: 0; +} +.ldh-bc-stack .bc-here-ico .msi { font-size: 16px; } +.ldh-bc-stack .bc-here-name { + font-size: 15px; + font-weight: var(--fw-semibold); + color: var(--fg-1); + letter-spacing: -0.005em; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + min-width: 0; +} +.ldh-bc-stack .bc-here-badge { + font-family: var(--font-mono); + font-size: 10px; + letter-spacing: 0.02em; + color: var(--ldh-blue-700); + background: var(--bg-accent-quiet); + border: 1px solid var(--ldh-blue-100); + padding: 1px 6px; + border-radius: var(--r-pill); + flex-shrink: 0; +} + +/* ---------- Breadcrumb · Pills ---------- */ +.ldh-bc-pills { + display: flex; align-items: center; gap: 4px; + /* NOWRAP on purpose: see .ab-mid above. A wrapping breadcrumb stops reading + as a path. Overflow is absorbed by truncating pill labels instead. */ + flex-wrap: nowrap; + min-width: 0; +} +.ldh-bc-pills .bc-pill { + display: inline-flex; align-items: center; gap: 6px; + min-width: 0; + padding: 4px 10px; + height: 26px; + border-radius: var(--r-pill); + background: var(--surface-1); + border: 1px solid transparent; + color: var(--fg-2); + font-size: var(--fs-sm); + white-space: nowrap; + transition: background var(--dur-fast) var(--ease-out-soft), color var(--dur-fast) var(--ease-out-soft); +} +.ldh-bc-pills .bc-pill:hover { background: var(--bg-hover); color: var(--fg-1); text-decoration: none; } +.ldh-bc-pills .bc-pill.is-current { + background: var(--bg-accent-quiet); + border-color: var(--ldh-blue-100); + color: var(--ldh-blue-700); + font-weight: var(--fw-medium); +} +.ldh-bc-pills .bc-pill .msi { font-size: 14px; opacity: 0.85; flex-shrink: 0; } +/* The label truncates; the icon and the current pill do not. Keeping the + current step whole matters most — it is where you are. */ +.ldh-bc-pills .bc-pill > span:not(.msi) { min-width: 0; overflow: hidden; text-overflow: ellipsis; } +.ldh-bc-pills .bc-pill.is-current { flex-shrink: 0; } +.ldh-bc-pills .bc-sep { flex-shrink: 0; } +.ldh-bc-pills .bc-sep { + color: var(--fg-hint); + font-size: 14px; + opacity: 0.7; +} + +/* ---------- Breadcrumb · Compact (iOS-style back + current) ---------- */ +.ldh-bc-compact { + display: inline-flex; align-items: center; gap: 8px; + min-width: 0; +} +.ldh-bc-compact .bc-back { + display: inline-flex; align-items: center; gap: 2px; + padding: 4px 8px 4px 4px; + border-radius: var(--r-sm); + color: var(--fg-link); + font-size: var(--fs-sm); + font-weight: var(--fw-medium); + white-space: nowrap; +} +.ldh-bc-compact .bc-back:hover { background: var(--surface-2); text-decoration: none; } +.ldh-bc-compact .bc-back .msi { font-size: 14px; } +.ldh-bc-compact .bc-divider { + width: 1px; height: 20px; + background: var(--border-default); +} +.ldh-bc-compact .bc-here-compact { + display: inline-flex; align-items: center; gap: 6px; + font-size: var(--fs-sm); + font-weight: var(--fw-semibold); + color: var(--fg-1); + white-space: nowrap; +} +.ldh-bc-compact .bc-here-compact .msi { + color: var(--ldh-blue-700); +} + +/* The Stack style is taller than the rest. Adjust actionbar vertical padding + to keep the row visually balanced regardless of style. */ +.ldh-actionbar--bc-stack { padding-top: var(--sp-2); padding-bottom: var(--sp-2); } +.ldh-actionbar--bc-drive { padding-top: var(--sp-2); padding-bottom: var(--sp-2); } + +/* ───────────────────────────────────────────────────────────────────────── + Tweak-driven overrides — toggled by data-attributes on . + Set by index.html based on the Tweaks panel state. + ───────────────────────────────────────────────────────────────────────── */ + + +/* Density — compact tightens row padding across the property editor. */ +html[data-tweak-density="compact"] .ldh-prop-row > .value, +html[data-tweak-density="compact"] .ldh-prop-row > .row-actions { + padding-top: 4px; + padding-bottom: 4px; +} +html[data-tweak-density="compact"] .ldh-prop-group > .label { + padding-top: 4px; + padding-bottom: 4px; +} +html[data-tweak-density="compact"] .ldh-actionbar { + padding-top: 4px; + padding-bottom: 4px; +} +html[data-tweak-density="compact"] .ldh-block-body { padding: var(--sp-3) 0; } + +/* Header style — subtle = solid dark navy, no gradient. Flat = light card. */ +html[data-tweak-header="subtle"] .ldh-header { + background: var(--ldh-blue-900, #0a1230); +} +html[data-tweak-header="subtle"] .ldh-header::before { opacity: 0; } +html[data-tweak-header="flat"] .ldh-header { + background: var(--bg-card); + color: var(--fg-1); + border-bottom: 1px solid var(--border-default); +} +html[data-tweak-header="flat"] .ldh-header::before { opacity: 0; } +html[data-tweak-header="flat"] .ldh-wordmark { color: var(--fg-1); } +html[data-tweak-header="flat"] .ldh-wordmark .mark { + background: linear-gradient(135deg, var(--ldh-blue-400), var(--ldh-violet-400)); +} +html[data-tweak-header="flat"] .ldh-address { + background: var(--surface-1); + border-color: var(--border-default); + color: var(--fg-1); +} +html[data-tweak-header="flat"] .ldh-address input { color: var(--fg-1); } +html[data-tweak-header="flat"] .ldh-address input::placeholder { color: var(--fg-muted); } +html[data-tweak-header="flat"] .ldh-address .msi { color: var(--fg-muted); } +html[data-tweak-header="flat"] .ldh-avatar { border: 2px solid var(--border-default); } + +/* Block edges — sharp = no radius, borderless = no border + no radius. */ +html[data-tweak-block-edges="sharp"] .ldh-block, +html[data-tweak-block-edges="sharp"] .ldh-view { + border-radius: 0; +} +html[data-tweak-block-edges="borderless"] .ldh-block, +html[data-tweak-block-edges="borderless"] .ldh-view { + border: 0; + border-radius: 0; + box-shadow: none; +} +html[data-tweak-block-edges="borderless"] .ldh-block-head { + border-bottom: 1px solid var(--border-default); +} + +/* Flow — the document treatment: blocks dissolve their card frame (no fill, + border, radius or shadow); the data they contain (grid cards, chart plots, + statement rows) keeps its own surfaces. Consecutive blocks are separated by + a hairline + even vertical rhythm, so prose and structured views interleave + as one continuous reading surface rather than a stack of widgets. */ +html[data-tweak-block-edges="flow"] .ldh-block, +html[data-tweak-block-edges="flow"] .ldh-view { + background: transparent; + border: 0; + border-radius: 0; + box-shadow: none; +} +html[data-tweak-block-edges="flow"] .ldh-block:hover { + box-shadow: none; + transform: none; +} +html[data-tweak-block-edges="flow"] .ldh-content { gap: 0; } +html[data-tweak-block-edges="flow"] .ldh-block-row { padding: var(--sp-6) 0; } +html[data-tweak-block-edges="flow"] .ldh-block-row + .ldh-block-row { + border-top: 1px solid var(--border-default); +} +/* …but data-visualisation bodies still want a white ground — a chart plotted + straight onto the tinted floor reads as unanchored. Plate the chart body + (its own controls/canvas padding supplies the inset) so the plot sits on a + clean surface while prose blocks stay transparent. */ +html[data-tweak-block-edges="flow"] .ldh-block-body:has(> .chart-canvas), +html[data-tweak-block-edges="flow"] .ldh-block-body:has(> .chart-controls) { + background: var(--surface-0); + border: 1px solid var(--border-default); + border-radius: var(--r-md); + padding: 0; + overflow: hidden; +} +/* Resource blocks are structured data surfaces too: the statement list — and + especially a media-header block, whose full-bleed image must keep its corners + clipped — stay on a white card in flow mode (like charts), so the description + doesn't float transparent over the tinted floor. */ +html[data-tweak-block-edges="flow"] .ldh-block:has(.ldh-edit-form) { + background: var(--bg-card); + border: 1px solid var(--border-default); + border-radius: var(--r-lg); + overflow: hidden; +} + +/* Action toolbar (right cluster) */ +.ldh-toolbar { + display: flex; align-items: center; gap: var(--sp-1); + color: var(--fg-muted); +} +.ldh-toolbar .ts { + font-family: var(--font-mono); + font-size: var(--fs-xs); + color: var(--fg-hint); + margin-right: var(--sp-3); + white-space: nowrap; +} +.ldh-toolbar .tb { + width: 36px; height: 36px; + display: inline-flex; align-items: center; justify-content: center; + background: transparent; + border: 0; border-radius: var(--r-sm); + color: var(--fg-2); + transition: background var(--dur-fast) var(--ease-out-soft), color var(--dur-fast) var(--ease-out-soft); +} +.ldh-toolbar .tb:hover { background: var(--bg-hover); color: var(--fg-1); } +.ldh-toolbar .tb.is-danger:hover { background: var(--danger-50); color: var(--danger-500); } +.ldh-toolbar .divider { + width: 1px; height: 20px; background: var(--border-default); margin: 0 var(--sp-2); +} + +/* ─── Redesigned action cluster (declutter): mode · contextual primary · ⋯ ── */ +.ldh-actions { display: flex; align-items: center; gap: var(--sp-1); color: var(--fg-muted); } +.ldh-actions-div { width: 1px; height: 20px; background: var(--border-default); margin: 0 var(--sp-2); } +.ldh-actions .ldhc-btn .msi { font-size: 18px; } + +/* Document dateTime as quiet metadata — never a control */ +.ldh-ab-ts { + display: inline-flex; align-items: center; gap: 4px; + font-family: var(--font-mono); font-size: var(--fs-xs); + color: var(--fg-hint); white-space: nowrap; flex-shrink: 0; +} +.ldh-ab-ts .msi { font-size: 14px; opacity: 0.7; } + +/* Overflow kebab */ +.ldh-of-wrap { position: relative; display: inline-flex; } +.ldh-of-wrap .tb { + width: 36px; height: 36px; + display: inline-flex; align-items: center; justify-content: center; + border: 0; background: transparent; border-radius: var(--r-md); + color: var(--fg-muted); cursor: pointer; + transition: background var(--dur-fast) var(--ease-out-soft), color var(--dur-fast) var(--ease-out-soft); +} +.ldh-of-wrap .tb:hover, .ldh-of-wrap.is-open .tb { background: var(--bg-hover); color: var(--fg-1); } +.ldh-of-menu { + position: absolute; top: calc(100% + 6px); right: 0; z-index: 50; + min-width: 184px; padding: 6px; + background: var(--surface-0); border: 1px solid var(--border-default); + border-radius: var(--r-md); box-shadow: var(--shadow-lg); + display: flex; flex-direction: column; gap: 2px; + color: var(--fg-1); +} +.ldh-of-menu .it { + display: flex; align-items: center; gap: var(--sp-2); + padding: 8px 10px; border: 0; background: transparent; cursor: pointer; + border-radius: var(--r-sm); font: inherit; font-size: var(--fs-sm); + color: var(--fg-2); text-align: left; +} +.ldh-of-menu .it:hover { background: var(--bg-hover); color: var(--fg-1); } +.ldh-of-menu .it.is-danger { color: var(--danger-500); } +.ldh-of-menu .it.is-danger:hover { background: var(--danger-50); } +.ldh-of-menu .it .msi { font-size: 18px; } +.ldh-of-menu .it-txt { display: flex; flex-direction: column; gap: 1px; min-width: 0; } +.ldh-of-menu .it-hint { font-size: var(--fs-xs); color: var(--ink-4); } +.ldh-of-menu .it:hover .it-hint { color: var(--fg-hint); } +.ldh-of-menu .it.is-danger .it-hint { color: var(--ink-4); } +.ldh-of-div { height: 1px; background: var(--border-default); margin: 4px 2px; } + +/* Mid zone (breadcrumb + timestamp) for zoned/contextual single-row layouts. + The timestamp pushes to the right edge of the zone (just before the actions), + so the "last modified" stamp reads as right-aligned metadata. */ +.ldh-actionbar--zoned .ab-mid, +.ldh-actionbar--contextual .ab-mid { + display: flex; align-items: center; gap: var(--sp-4); min-width: 0; + /* The mid zone holds two different KINDS of thing: the breadcrumb, which is + navigation and must stay legible, and the timestamp, which is metadata. + When they don't both fit, the METADATA yields — it drops to a second line. + This was nowrap, so the only thing that could give was the breadcrumb's own + wrap, which stacked the pills one per row and doubled the bar's height. */ + flex-wrap: wrap; + row-gap: 2px; +} +.ldh-actionbar--zoned .ab-mid .ldh-ab-ts, +.ldh-actionbar--contextual .ab-mid .ldh-ab-ts { margin-left: auto; padding-left: var(--sp-4); } + +/* TWO-TIER — location line above, controls line below */ +.ldh-actionbar--twotier { + display: flex; flex-direction: column; align-items: stretch; gap: var(--sp-2); +} +.ldh-actionbar--twotier .ab-context { + display: flex; align-items: center; justify-content: space-between; gap: var(--sp-4); min-width: 0; +} +.ldh-actionbar--twotier .ab-controls { display: flex; align-items: center; gap: var(--sp-4); } +.ldh-actionbar--twotier .ab-spacer { flex: 1; } + +/* CONTEXTUAL — minimal at rest; secondary actions (Edit + ⋯) reveal on hover/ + focus of the bar. Mode switcher stays visible as the one persistent control. */ +.ldh-actionbar--contextual .ldh-actions .ldhc-btn, +.ldh-actionbar--contextual .ldh-actions .ldh-of-wrap, +.ldh-actionbar--contextual .ldh-actions .ldh-actions-div { + opacity: 0; transition: opacity var(--dur-base) var(--ease-out-soft); +} +.ldh-actionbar--contextual:hover .ldh-actions .ldhc-btn, +.ldh-actionbar--contextual:focus-within .ldh-actions .ldhc-btn, +.ldh-actionbar--contextual:hover .ldh-actions .ldh-of-wrap, +.ldh-actionbar--contextual:focus-within .ldh-actions .ldh-of-wrap, +.ldh-actionbar--contextual:hover .ldh-actions .ldh-actions-div, +.ldh-actionbar--contextual:focus-within .ldh-actions .ldh-actions-div { opacity: 1; } + +/* Mode switcher — segmented dropdown */ +.ldh-mode { + position: relative; + display: inline-flex; align-items: center; gap: var(--sp-2); + height: 36px; + padding: 0 var(--sp-3); + background: var(--surface-2); + border: 1px solid var(--border-default); + border-radius: var(--r-md); + color: var(--fg-2); + font-size: var(--fs-sm); + font-weight: var(--fw-medium); + margin-right: var(--sp-2); +} +.ldh-mode .label-row { display: inline-flex; align-items: center; gap: var(--sp-2); } +.ldh-mode .label { letter-spacing: 0.02em; } +.ldh-mode .modes-pop { + position: absolute; top: calc(100% + 6px); right: 0; + min-width: 220px; + background: var(--surface-0); + border: 1px solid var(--border-default); + border-radius: var(--r-md); + box-shadow: var(--shadow-lg); + padding: var(--sp-1); + display: none; + z-index: 50; + color: var(--fg-1); +} +.ldh-mode.is-open .modes-pop { display: block; } +.ldh-mode .mi { + display: flex; align-items: center; gap: var(--sp-3); + padding: var(--sp-2) var(--sp-3); + font-size: var(--fs-sm); color: var(--fg-2); + border-radius: var(--r-sm); + width: 100%; + background: transparent; border: 0; + cursor: pointer; + text-align: left; +} +.ldh-mode .mi:hover { background: var(--bg-hover); color: var(--fg-1); } +.ldh-mode .mi.is-active { background: var(--ldh-blue-50); color: var(--ldh-blue-700); font-weight: var(--fw-medium); } +.ldh-mode .mi .msi { color: var(--fg-muted); } +.ldh-mode .mi.is-active .msi { color: var(--ldh-blue-600); } +.ldh-mode .mi .desc { display: block; font-size: 11px; color: var(--fg-hint); font-weight: 400; margin-top: 1px; } +.ldh-mode .mi .label-col { flex: 1; } + +/* ---------- Main layout ---------- */ +.ldh-main { + flex: 1; + display: grid; + grid-template-columns: minmax(0, 1fr); + gap: var(--sp-5); + padding: var(--sp-5) var(--sp-6); + max-width: var(--container-xl); + width: 100%; + margin: 0 auto; + align-items: start; +} +.ldh-main.is-flush { padding: var(--sp-3) var(--sp-3); max-width: none; } +.ldh-content { + display: flex; flex-direction: column; gap: var(--sp-6); + min-width: 0; +} + +/* Per-view-block row: the block-scoped drawer (backlinks/related/parallax) + slides in from the right edge of the block and overlays its right portion. + The drawer is scoped to the block's height — no global push, no body scroll + hijack. The row stays display:block so its content keeps full width when + the drawer is closed AND when it's open. */ +.ldh-block-row { position: relative; } +.ldh-block-row > .row-main { min-width: 0; } + +/* Backlinks / related panel */ +.ldh-panel { + background: var(--bg-card); + border: 1px solid var(--border-default); + border-radius: var(--r-lg); + overflow: hidden; +} +.ldh-panel .ph { + display: flex; align-items: center; justify-content: space-between; + padding: var(--sp-3) var(--sp-4); + background: var(--surface-1); + border-bottom: 1px solid var(--border-default); +} +.ldh-panel .ph .title { + font-family: var(--font-mono); + font-size: 11px; + letter-spacing: var(--tracking-wide); + text-transform: uppercase; + color: var(--fg-muted); +} +.ldh-panel.is-collapsed .pb { display: none; } +.ldh-panel.is-collapsed .ph { border-bottom: 0; } +.ldh-panel .pb { padding: var(--sp-3) var(--sp-4); display: flex; flex-direction: column; gap: var(--sp-2); } +.ldh-panel .pb a { display: block; font-size: var(--fs-sm); padding: 4px 0; color: var(--fg-2); } +.ldh-panel .pb a:hover { color: var(--fg-link); } +.ldh-panel .pb .empty { font-size: var(--fs-sm); color: var(--fg-hint); padding: 4px 0; } + +/* ---------- Footer ---------- */ +.ldh-footer { + margin-top: var(--sp-12); + padding: var(--sp-10) var(--sp-6) var(--sp-8); + background: var(--surface-1); + border-top: 1px solid var(--border-default); +} +.ldh-footer .cols { + max-width: var(--container-xl); + margin: 0 auto; + display: grid; + grid-template-columns: 1.4fr repeat(4, 1fr); + gap: var(--sp-8); +} +.ldh-footer .col .ftitle { + font-family: var(--font-mono); + font-size: 11px; + letter-spacing: var(--tracking-wide); + text-transform: uppercase; + color: var(--fg-muted); + margin: 0 0 var(--sp-3); +} +.ldh-footer .col a { display: block; padding: 4px 0; font-size: var(--fs-sm); color: var(--fg-2); } +.ldh-footer .col a:hover { color: var(--fg-link); } +.ldh-footer .brand-col p { + font-size: var(--fs-sm); color: var(--fg-muted); line-height: var(--lh-base); max-width: 280px; margin: var(--sp-3) 0 0; +} +.ldh-footer .legal { + max-width: var(--container-xl); + margin: var(--sp-8) auto 0; + padding-top: var(--sp-4); + border-top: 1px solid var(--border-default); + display: flex; justify-content: space-between; + font-size: var(--fs-xs); + color: var(--fg-hint); + font-family: var(--font-mono); +} + +/* ---------- Generic Block (view block wrapper) ---------- */ +.ldh-block { + background: var(--bg-card); + border: 1px solid var(--border-default); + border-radius: var(--r-lg); + overflow: hidden; + transition: box-shadow var(--dur-base) var(--ease-out-soft), border-color var(--dur-base) var(--ease-out-soft); +} +.ldh-block:hover { box-shadow: var(--shadow-sm); } +.ldh-block.is-quiet { border: 0; background: transparent; } +.ldh-block.is-quiet:hover { box-shadow: none; } +.ldh-block-head { + display: flex; align-items: flex-start; justify-content: space-between; + padding: var(--sp-5) var(--sp-6) var(--sp-3); + gap: var(--sp-4); +} +.ldh-block-head .ttl { font-size: var(--fs-xl); font-weight: var(--fw-semibold); color: var(--fg-1); margin: 0; line-height: var(--lh-snug); letter-spacing: var(--tracking-snug); } +/* Hero text block — the lead resource's title reads at page-title scale, not + block-heading scale, so the top of a document anchors the page. */ +.ldh-block-head .ttl--hero { font-size: var(--fs-3xl); font-weight: var(--fw-bold); line-height: var(--lh-tight); letter-spacing: var(--tracking-tight); } +.ldh-block-head .sub { display: block; font-size: var(--fs-sm); color: var(--fg-muted); margin-top: 4px; font-weight: 400; } +.ldh-block-head .actions { display: flex; gap: var(--sp-1); } +.ldh-block-head .actions .tb { + width: 32px; height: 32px; + display: inline-flex; align-items: center; justify-content: center; + background: transparent; border: 0; border-radius: var(--r-sm); + color: var(--fg-muted); +} +.ldh-block-head .actions .tb:hover { background: var(--bg-hover); color: var(--fg-1); } +/* Delete (×) — quiet at rest, danger on hover. The block-removal affordance. */ +.ldh-block-head .actions .tb-danger:hover { background: var(--danger-50); color: var(--danger-500); } + +/* Links button — opens block-scoped drawer (backlinks / related / parallax) */ +.ldh-block-head .actions .tb-links, +.hero-actions .tb-links { + width: auto; + min-width: 32px; + padding: 0 8px 0 6px; + gap: 4px; + background: var(--bg-accent-quiet); + color: var(--ldh-blue-700); + border: 1px solid var(--ldh-blue-100); +} +.ldh-block-head .actions .tb-links:hover, +.hero-actions .tb-links:hover { + background: var(--ldh-blue-100); + color: var(--ldh-blue-700); +} +.ldh-block-head .actions .tb-links.is-on, +.hero-actions .tb-links.is-on { + background: var(--ldh-blue-600); + color: #fff; + border-color: var(--ldh-blue-600); +} + +.hero-actions { + position: absolute; + top: var(--sp-4); + right: var(--sp-4); + display: flex; + gap: var(--sp-1); + z-index: 2; +} +.hero-actions .tb-links { + height: 32px; + display: inline-flex; align-items: center; + border-radius: var(--r-sm); + background: rgba(255,255,255,0.72); + backdrop-filter: blur(6px); + border-color: rgba(255,255,255,0.85); +} + +/* Block drawer — fixed to the viewport's right edge so it sits FLUSH against + the page edge regardless of where the block lives inside the centered + .ldh-main container. Top/height are set inline by the React component to + match the anchor block's bounding rect, so the drawer stays block-scoped + vertically. */ +.ldh-drawer { + position: fixed; + right: 0; + width: min(360px, 92vw); + color: var(--fg-1); + background: var(--surface-0); + border: 0; + border-left: 1px solid var(--border-default); + border-radius: 0; + box-shadow: -8px 0 24px -22px rgba(15, 23, 42, 0.16); + display: flex; flex-direction: column; + z-index: 25; + animation: drawer-slide var(--dur-base) var(--ease-out-soft); + overflow: hidden; +} +@keyframes drawer-slide { + from { transform: translateX(100%); opacity: 0; } + to { transform: translateX(0); opacity: 1; } +} + +/* Edge-hover sensor — invisible hot-zone flush to the viewport's right edge, + vertically scoped to its block (top/height set inline). Reaching it slides + the drawer out, no click needed. A faint handle peeks in so the gesture is + discoverable. */ +.ldh-edge-sensor { + position: fixed; + right: 0; + width: 20px; + z-index: 24; + display: flex; align-items: center; justify-content: flex-end; + cursor: pointer; +} +.ldh-edge-handle { + display: flex; align-items: center; justify-content: center; + width: 17px; height: 54px; + border-radius: var(--r-pill) 0 0 var(--r-pill); + background: var(--surface-0); + border: 1px solid var(--border-default); + border-right: 0; + color: var(--fg-hint); + box-shadow: -8px 0 18px -14px rgba(15, 23, 42, 0.28); + opacity: 0.5; + transform: translateX(7px); + transition: opacity var(--dur-base) var(--ease-out-soft), + transform var(--dur-base) var(--ease-out-soft), + color var(--dur-fast) var(--ease-out-soft), + border-color var(--dur-fast) var(--ease-out-soft); +} +.ldh-edge-handle .msi { font-size: 18px; } +.ldh-edge-sensor:hover .ldh-edge-handle { + opacity: 1; + transform: translateX(0); + /* --accent is set only at RUNTIME by index.html's accent tweak, so on every + other surface (preview cards, templates, screen-*.html) it is undefined and + the hover colour silently fell back to inherited body ink. The tweak stays + as an override; --fg-link is the static default because it is the system's + interactive colour and remaps per skin (base44 → lime). */ + color: var(--accent, var(--fg-link)); + border-color: var(--accent, var(--fg-link)); +} +.ldh-drawer .dh { + display: flex; align-items: flex-start; justify-content: space-between; + padding: var(--sp-5) var(--sp-5) var(--sp-4); + border-bottom: 1px solid var(--border-default); +} +.ldh-drawer .dh .eyebrow { + font-family: var(--font-mono); font-size: 10px; + letter-spacing: var(--tracking-wide); text-transform: uppercase; + color: var(--fg-muted); +} +.ldh-drawer .dh h3 { + margin: 4px 0 0; + font-size: var(--fs-lg); + font-weight: var(--fw-semibold); + letter-spacing: var(--tracking-snug); + color: var(--fg-1); +} +/* Drawer close is core — no bespoke rule needed. */ + +.ldh-drawer .db { + flex: 1; + overflow: auto; + padding: var(--sp-3) var(--sp-3) var(--sp-5); + display: flex; flex-direction: column; gap: var(--sp-4); +} +.ldh-drawer .dgroup { display: flex; flex-direction: column; gap: 2px; } +.ldh-drawer .dh2 { + display: flex; align-items: baseline; justify-content: space-between; + padding: var(--sp-2) var(--sp-2) 4px; + font-family: var(--font-mono); font-size: 10px; + letter-spacing: var(--tracking-wide); text-transform: uppercase; + color: var(--fg-muted); +} +.ldh-drawer .dh2 em { + font-style: normal; + font-family: var(--font-mono); font-size: 10px; + color: var(--fg-hint); + background: var(--surface-2); + padding: 1px 6px; border-radius: var(--r-pill); + letter-spacing: 0; +} +.ldh-drawer .drow { + display: flex; align-items: center; gap: 8px; + padding: 6px 8px; + border-radius: var(--r-sm); + text-decoration: none; + color: var(--fg-2); + font-size: var(--fs-sm); + transition: background var(--dur-fast) var(--ease-out-soft); +} +.ldh-drawer .drow:hover { background: var(--bg-hover); color: var(--fg-1); text-decoration: none; } +.ldh-drawer .drow .msi { color: var(--fg-hint); flex-shrink: 0; } +.ldh-drawer .drow .lbl { flex: 1; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } +.ldh-drawer .drow .tag { + font-family: var(--font-mono); font-size: 9.5px; color: var(--fg-hint); + background: var(--surface-2); padding: 1px 6px; border-radius: var(--r-pill); +} +.ldh-drawer .drow.is-current { + background: var(--bg-accent-quiet); + color: var(--ldh-blue-700); + font-weight: var(--fw-medium); +} +.ldh-drawer .drow.is-current .msi { color: var(--ldh-blue-600); } +.ldh-drawer .drow.is-current .tag { background: var(--surface-0); color: var(--ldh-blue-600); } + +.ldh-drawer .df { + display: flex; align-items: center; justify-content: space-between; + padding: var(--sp-3) var(--sp-4); + border-top: 1px solid var(--border-default); + background: var(--surface-1); + font-family: var(--font-mono); + font-size: var(--fs-xs); + color: var(--fg-muted); +} +.ldh-block-body { padding: 0 var(--sp-6) var(--sp-6); } +.ldh-block-body.is-flush { padding: 0; } +.ldh-block-foot { + display: flex; align-items: center; justify-content: space-between; + padding: var(--sp-3) var(--sp-6); + border-top: 1px solid var(--border-default); + background: var(--surface-1); +} + +/* Section header — for grouping blocks in ContentMode */ +.ldh-section { + display: flex; flex-direction: column; gap: var(--sp-2); + margin-top: var(--sp-4); +} +.ldh-section .eyebrow { font-family: var(--font-mono); font-size: 11px; letter-spacing: var(--tracking-wide); text-transform: uppercase; color: var(--ldh-blue-600); } +.ldh-section h2 { margin: 0; font-size: var(--fs-2xl); font-weight: var(--fw-semibold); letter-spacing: var(--tracking-snug); color: var(--fg-1); } +.ldh-section p { margin: 4px 0 0; color: var(--fg-2); max-width: 56ch; } + +/* Hero block — uses pastel gradient for the document intro */ +.ldh-hero { + position: relative; + background: var(--grad-aurora); + border: 1px solid var(--border-default); + border-radius: var(--r-lg); + padding: var(--sp-10) var(--sp-8); + overflow: hidden; +} +.ldh-hero::before { + content: ""; position: absolute; inset: 0; + background-image: radial-gradient(circle at 1px 1px, rgba(11,14,20,0.06) 1px, transparent 1.4px); + background-size: 24px 24px; + opacity: 0.7; + pointer-events: none; +} +.ldh-hero > * { position: relative; } + +/* Dark theme: pastel hero would strand light text on a light wash — swap to the + dark accent gradient (governance: pastels/gradients are hero-only anyway). */ +:root[data-theme="dark"] .ldh-hero { + background: var(--grad-deep); + border-color: var(--line-2); +} +:root[data-theme="dark"] .ldh-hero::before { + background-image: radial-gradient(circle at 1px 1px, rgba(255,255,255,0.08) 1px, transparent 1.4px); +} +.ldh-hero .eyebrow { font-family: var(--font-mono); font-size: 11px; letter-spacing: var(--tracking-wide); text-transform: uppercase; color: var(--ldh-violet-700); } +.ldh-hero h1 { + margin: var(--sp-2) 0 var(--sp-3); + font-size: var(--fs-4xl); + font-weight: var(--fw-semibold); + letter-spacing: var(--tracking-tight); + color: var(--fg-1); + line-height: var(--lh-tight); +} +.ldh-hero h1 em { font-style: italic; font-family: var(--font-display); font-weight: 400; } +.ldh-hero .lede { font-size: var(--fs-md); color: var(--ink-2); max-width: 56ch; line-height: var(--lh-base); margin: 0; } +.ldh-hero .lede + p { margin-top: var(--sp-3); } + +/* Chart controls (re-styled . */ +.chart-canvas { padding: var(--sp-6); } +/* ChartMode hero canvas — the chart fills the full block width (fluid SVG), + so it never gets letterboxed inside a fixed-height box like the small + dashboard charts do. */ +.chart-canvas--hero { padding: var(--sp-7) var(--sp-6) var(--sp-8); } + +/* Resource media header — full-bleed depiction image with a title overlay. + Generic + data-driven: rendered for ANY resource whose description has a + depiction predicate (foaf:depiction, schema:image, …), never tied to a class. */ +.ldh-media-block { overflow: hidden; } +.ldh-media-head { + position: relative; + width: 100%; + min-height: 320px; + background: linear-gradient(135deg, var(--ldh-blue-400) 0%, var(--ldh-violet-400) 100%); +} +.ldh-media-slot { + position: absolute; inset: 0; + width: 100%; height: 100%; + display: block; + border-radius: 0 !important; +} +.ldh-media-scrim { + position: absolute; inset: 0; pointer-events: none; + background: linear-gradient(180deg, rgba(0,0,0,0.05) 0%, rgba(0,0,0,0.15) 35%, rgba(0,0,0,0.65) 100%); + z-index: 1; +} +.ldh-media-text { + position: absolute; left: var(--sp-6); right: var(--sp-6); bottom: var(--sp-5); + color: #fff; text-shadow: 0 2px 12px rgba(0,0,0,0.45); + z-index: 2; +} +.ldh-media-text .overline { + display: inline-block; margin-bottom: 8px; + font-family: var(--font-mono); font-size: 11px; letter-spacing: 0.06em; text-transform: uppercase; + color: rgba(255,255,255,0.95); + background: rgba(255,255,255,0.16); backdrop-filter: blur(6px); + padding: 3px 9px; border-radius: var(--r-pill); +} +.ldh-media-text h3 { + margin: 0; font-size: var(--fs-2xl); font-weight: var(--fw-semibold); + letter-spacing: var(--tracking-snug); line-height: var(--lh-tight); +} +.ldh-media-actions { + position: absolute; top: var(--sp-3); right: var(--sp-3); + display: flex; gap: 6px; z-index: 3; +} +.ldh-media-actions .tb-onmedia, +.ldh-media-actions .tb-links { + width: 32px; height: 32px; + display: inline-flex; align-items: center; justify-content: center; + background: rgba(255,255,255,0.18); backdrop-filter: blur(8px); + border: 1px solid rgba(255,255,255,0.28); border-radius: var(--r-sm); + color: #fff; cursor: pointer; + transition: background var(--dur-fast) var(--ease-out-soft); +} +.ldh-media-actions .tb-onmedia:hover, +.ldh-media-actions .tb-links:hover { background: rgba(255,255,255,0.32); } +.ldh-media-actions .tb-onmedia.is-on { background: rgba(255,255,255,0.45); } +.ldh-media-actions .tb-links { width: auto; padding: 0 8px 0 6px; gap: 4px; } +.ldh-media-actions .tb-links .tbnum { font-family: var(--font-mono); font-size: 10px; font-weight: var(--fw-medium); letter-spacing: 0.02em; } + +/* Resource list — grid of category cards */ +.ldh-grid { + display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); + gap: var(--sp-4); +} +.ldh-grid-card { + background: var(--bg-card); + border: 1px solid var(--border-default); + border-radius: var(--r-md); + padding: var(--sp-4); + display: flex; align-items: center; gap: var(--sp-3); + transition: border-color var(--dur-fast) var(--ease-out-soft), box-shadow var(--dur-fast) var(--ease-out-soft), transform var(--dur-fast) var(--ease-out-soft); + text-decoration: none; +} +.ldh-grid-card:hover { + border-color: var(--ldh-blue-300); + box-shadow: var(--shadow-sm); + transform: translateY(-1px); + text-decoration: none; +} +.ldh-grid-card .gi { + width: 36px; height: 36px; + border-radius: var(--r-sm); + background: var(--surface-2); + display: inline-flex; align-items: center; justify-content: center; + color: var(--fg-muted); + flex-shrink: 0; +} +.ldh-grid-card.lavender .gi { background: var(--surf-lavender); color: var(--ldh-violet-600); } +.ldh-grid-card.mint .gi { background: var(--surf-mint); color: var(--success-500); } +.ldh-grid-card.peach .gi { background: var(--surf-peach); color: #c25c00; } +.ldh-grid-card.sky .gi { background: var(--surf-sky); color: var(--ldh-blue-600); } +.ldh-grid-card.blush .gi { background: var(--surf-blush); color: #b03060; } +.ldh-grid-card.sand .gi { background: var(--surf-sand); color: #7a5b0c; } +.ldh-grid-card .gt { font-weight: var(--fw-medium); color: var(--fg-1); font-size: var(--fs-sm); } +.ldh-grid-card .gc { font-family: var(--font-mono); font-size: 11px; color: var(--fg-hint); margin-top: 2px; display: block; } + +/* ---------- Property editor (read + edit on a row level) ---------- + Each row reads as a clean label/value pair by default. Click the value to + edit it; the visual chrome (input border + bg) only appears while editing. + No block-wide editing flag; saves are optimistic per-row. */ +.ldh-prop-form { display: flex; flex-direction: column; } + +/* One predicate (with N values) lives in a .ldh-prop-group grid. The label + cell spans N rows; each value's .ldh-prop-row uses display:contents so its + value/type/actions cells slot into columns 2/3/4 of the parent grid. */ +.ldh-prop-group { + display: grid; + grid-template-columns: 200px 1fr 64px; + /* MUST stay 0 — see the statement-delimiter block below. */ + column-gap: 0; +} + +/* Shared label cell. Spans all value rows when there are >1; align text to + the right and vertically center across the spanned rows. */ +.ldh-prop-group > .label { + position: relative; + padding: var(--sp-3) var(--sp-3) var(--sp-3) var(--sp-6); + text-align: right; + line-height: 1.25; + display: flex; + flex-direction: column; + justify-content: center; + align-self: stretch; +} +/* No predicate CURIE in the label column. The label column shows the human + label only; the machine name is the group's identity in the data, reachable + through the rdfs:comment toggletip and the row's own actions. */ +.ldh-prop-group > .label .pred { + color: var(--fg-2); + font-family: var(--font-sans); + font-weight: var(--fw-medium); + font-size: var(--fs-sm); +} + +/* Value row — display:contents puts its cells (value, row-actions) + directly into the parent grid's columns 2, 3. */ +.ldh-prop-row { display: contents; } +.ldh-prop-row > .value, +.ldh-prop-row > .row-actions { + /* MUST stretch: these share a grid row with the (spanning) label cell, and + the statement hairline + hover band are drawn on all three. A cell that + sizes to its content draws its rule at a different y than its neighbours. */ + align-self: stretch; + padding-top: var(--sp-3); + padding-bottom: var(--sp-3); + padding-left: var(--sp-3); + transition: background var(--dur-fast) var(--ease-out-soft); +} +.ldh-prop-row > .row-actions { padding-right: var(--sp-6); } + +/* ========================================================================= + STATEMENT DELIMITERS — a hairline under each statement, as in the product. + .ldh-prop-row is display:contents, so the rule has to go on the CELLS; put + it on all three and they line up into one continuous rule. + + The label cell SPANS a multi-value group, so its border draws once at the + bottom of the whole span while .value / .row-actions draw per value. That + falls out exactly right: a single-value statement gets one full-width rule, + and extra values of the same predicate are separated only across columns + 2–3 — the unbroken label column reads as "still the same predicate". + + The final statement has no rule: the card edge already ends the list, and a + trailing hairline reads as a dangling separator. + ========================================================================= */ +.ldh-prop-form .ldh-prop-group > .label, +.ldh-prop-form .ldh-prop-row > .value, +.ldh-prop-form .ldh-prop-row > .row-actions { border-bottom: 1px solid var(--border-default); } +.ldh-prop-form > .ldh-prop-group:last-child > .label, +.ldh-prop-form > .ldh-prop-group:last-child .ldh-prop-row:last-child > .value, +.ldh-prop-form > .ldh-prop-group:last-child .ldh-prop-row:last-child > .row-actions { border-bottom: 0; } +.ldh-prop-group:hover > .label, +.ldh-prop-row:hover > .value, +.ldh-prop-row:hover > .row-actions { + background: var(--surface-1); +} +/* Bridge the grid column-gaps so the hover highlight reads as one continuous + band rather than three segments separated by white gaps. Each end cell + extends only toward the value; the value extends both ways. */ +/* No bridging shadows needed now that column-gap is 0 — the three cells are + adjacent, so their hover backgrounds already form one continuous band. */ + +/* Value cell — looks like plain text in read mode. Clickable interactive rows + get a subtle hover affordance. Inputs only appear when editing. */ +.ldh-prop-row .value { + min-height: 32px; + font-size: var(--fs-sm); + color: var(--fg-1); + border: 1px solid transparent; + background: transparent; + display: flex; align-items: center; flex-wrap: wrap; gap: 6px; + min-width: 0; + transition: background var(--dur-fast) var(--ease-out-soft), border-color var(--dur-fast) var(--ease-out-soft); + cursor: text; + word-break: break-word; +} +.ldh-prop-row.is-interactive .value:hover { + background: var(--surface-1); +} +.ldh-prop-row.is-interactive .value:focus-visible { + outline: 0; + background: var(--surface-2); + border-color: var(--ldh-blue-300); + box-shadow: var(--shadow-glow-blue); +} +.ldh-prop-row .value .long { white-space: pre-wrap; } + +/* Edit state — input fills the value cell with a distinct (recessed) surface so + the active field reads as separate from the white card around it. */ +.ldh-prop-row.is-editing .value { + background: var(--surface-2); + border-color: var(--ldh-blue-300); + /* the delimiter is the list's, not the field's — keep it through editing */ + border-bottom-color: var(--border-default); + box-shadow: var(--shadow-glow-blue); + padding: 4px 6px; +} +.ldh-prop-row.is-editing .value input, +.ldh-prop-row.is-editing .value textarea, +.ldh-prop-row.is-editing .value select { + flex: 1; min-width: 0; + background: transparent; + border: 0; + outline: 0; + padding: 4px 4px; + font: inherit; + color: inherit; + resize: vertical; + line-height: 1.5; +} +.ldh-prop-row.is-editing .value textarea { + min-height: 80px; +} + +/* Resource typeahead picker — used in place of the plain input when editing + a resource-valued cell. Composed of a small search input followed by an + inline dropdown list of dataspace matches. */ +.ldh-resource-pick { + position: relative; + flex: 1; + min-width: 0; + display: flex; + flex-direction: column; + gap: 6px; +} +.ldh-resource-pick-input { + display: flex; align-items: center; gap: 6px; + height: 32px; + padding: 0 10px; + border-radius: var(--r-sm); + border: 1px solid var(--ldh-blue-400); + background: var(--surface-0); + box-shadow: 0 0 0 3px var(--bg-accent-quiet); +} +.ldh-resource-pick-input .msi { color: var(--fg-2); flex-shrink: 0; } +.ldh-resource-pick-input input { + flex: 1; + min-width: 0; + border: 0; + outline: 0; + background: transparent; + font: inherit; + color: var(--fg-1); +} +.ldh-resource-pick-input input::placeholder { color: var(--fg-hint); } + +.ldh-resource-pick-list { + position: absolute; + top: calc(32px + 6px); + left: 0; + right: 0; + background: var(--bg-card); + border: 1px solid var(--border-default); + border-radius: var(--r-md); + box-shadow: 0 16px 40px -12px rgba(15, 23, 42, 0.28), 0 4px 12px -4px rgba(15, 23, 42, 0.18); + padding: 4px; + max-height: 320px; + overflow: auto; + z-index: 20; + animation: menu-pop var(--dur-fast) var(--ease-out-soft); +} +.ldh-resource-pick-section { + padding: 6px 10px 4px; + font-family: var(--font-mono); + font-size: 10px; + letter-spacing: 0.04em; + text-transform: uppercase; + color: var(--fg-muted); +} +.ldh-resource-pick-item { + display: grid; + grid-template-columns: 22px 1fr auto; + align-items: center; + gap: 8px; + width: 100%; + padding: 6px 10px; + border: 0; + background: transparent; + border-radius: var(--r-sm); + cursor: pointer; + font: inherit; + text-align: left; + color: var(--fg-1); +} +.ldh-resource-pick-item:hover, +.ldh-resource-pick-item.is-active { + background: var(--surface-1); +} +.ldh-resource-pick-item .ico { + width: 22px; height: 22px; + display: inline-flex; align-items: center; justify-content: center; + border-radius: var(--r-sm); + background: var(--bg-accent-quiet); + color: var(--ldh-blue-700); + flex-shrink: 0; +} +.ldh-resource-pick-item .body { + display: flex; flex-direction: column; + min-width: 0; +} +.ldh-resource-pick-item .body .label { + font-size: var(--fs-sm); + color: var(--fg-1); + white-space: nowrap; overflow: hidden; text-overflow: ellipsis; +} +.ldh-resource-pick-item .body .iri { + font-family: var(--font-mono); + font-size: 10px; + color: var(--fg-hint); + white-space: nowrap; overflow: hidden; text-overflow: ellipsis; +} +.ldh-resource-pick-item .cls { + font-family: var(--font-mono); + font-size: 10px; + color: var(--ldh-blue-700); + background: var(--bg-accent-quiet); + padding: 1px 6px; + border-radius: var(--r-pill); + white-space: nowrap; + flex-shrink: 0; +} +.ldh-resource-pick-empty { + display: flex; align-items: flex-start; gap: 8px; + padding: 12px 10px; + color: var(--fg-2); + font-size: var(--fs-sm); +} +.ldh-resource-pick-empty .hint { + font-size: 11px; + color: var(--fg-hint); + margin-top: 2px; +} + +/* Non-interactive value display variants */ +.ldh-prop-row .value .chip-inline { + display: inline-flex; align-items: center; gap: 4px; + padding: 2px 8px; border-radius: var(--r-pill); + background: var(--ldh-violet-50); + color: var(--ldh-violet-700); + font-family: var(--font-sans); font-size: 12px; font-weight: var(--fw-medium); +} +.ldh-prop-row .value .image-mini { + display: inline-flex; align-items: center; gap: 8px; +} +.ldh-prop-row .value .image-mini .thumb { + width: 24px; height: 24px; border-radius: 4px; + background: var(--surf-peach); + display: inline-flex; align-items: center; justify-content: center; + color: #c25c00; +} +.ldh-prop-row .value .iri-link { + font-family: var(--font-mono); + color: var(--ldh-blue-700); +} + +/* Right-side row actions — edit (interactive rows) + remove. + The reveal is on the BUTTONS, not on the cell: the cell carries the statement + delimiter, and fading the cell faded its border too, so the hairline stopped + short of the actions column. Fading the controls keeps the cell (and its + rule) present while the row is at rest, and the footprint never shifts. */ +.ldh-prop-row .row-actions { + display: flex; gap: 2px; + justify-content: flex-end; + align-items: center; +} +.ldh-prop-row .row-actions > * { + opacity: 0; + transition: opacity var(--dur-fast) var(--ease-out-soft); +} +.ldh-prop-row:hover .row-actions > *, +.ldh-prop-row.is-editing .row-actions > *, +.ldh-prop-row:focus-within .row-actions > * { opacity: 1; } +/* ALWAYS-ON API. Because the reveal moved from the cell to its children, the + old `.row-actions { opacity: 1 }` override silently stopped working — it now + sets a cell that is already opaque and never reaches the buttons. Anything + that needs the affordances permanently visible (documentation specimens, a + touch-only surface with no hover) must use one of these instead of setting + opacity on the cell: + · .row-actions.is-always-on — one row + · .ldh-prop-form.is-specimen — every row in the form + Cell-level opacity is deliberately no longer a knob: it hides the statement + delimiter drawn on that cell along with the buttons. */ +.ldh-prop-row .row-actions.is-always-on > *, +.ldh-prop-form.is-specimen .ldh-prop-row .row-actions > * { opacity: 1; } + +.ldh-prop-row .row-actions .rb { + width: 28px; height: 28px; + display: inline-flex; align-items: center; justify-content: center; + background: transparent; border: 0; border-radius: var(--r-sm); + color: var(--fg-muted); + cursor: pointer; + transition: background var(--dur-fast) var(--ease-out-soft), color var(--dur-fast) var(--ease-out-soft); +} +.ldh-prop-row .row-actions .rb.edit:hover { + background: var(--bg-accent-quiet); + color: var(--ldh-blue-700); +} +.ldh-prop-row .row-actions .rb.remove:hover { + background: var(--danger-50); + color: var(--danger-500); +} + +/* Add-row footer — single block-level affordance that survived the row-level refactor */ +.ldh-prop-addrow { + display: flex; align-items: center; gap: var(--sp-2); + padding: var(--sp-3) var(--sp-6); + background: var(--surface-1); + border-top: 1px dashed var(--border-default); +} +.ldh-prop-addrow .pred-pick { + display: inline-flex; align-items: center; gap: var(--sp-2); + height: 32px; padding: 0 var(--sp-3); + background: var(--surface-0); + border: 1px solid var(--border-default); + border-radius: var(--r-sm); + font-size: var(--fs-sm); + color: var(--fg-2); + font-family: var(--font-mono); +} +.ldh-prop-addrow .add-prop { + width: 32px; height: 32px; + display: inline-flex; align-items: center; justify-content: center; + background: var(--ldh-blue-50); color: var(--ldh-blue-700); + border: 1px solid var(--ldh-blue-100); + border-radius: var(--r-sm); + cursor: pointer; + transition: background var(--dur-fast) var(--ease-out-soft); +} +.ldh-prop-addrow .add-prop:hover { background: var(--ldh-blue-100); } + +/* RDF type tag — small pill at right of input */ +.ldh-rdf-type { + display: inline-flex; align-items: center; gap: 4px; + padding: 2px 8px; + border-radius: var(--r-pill); + font-family: var(--font-mono); + font-size: 10px; + font-weight: var(--fw-medium); + letter-spacing: 0.02em; + text-transform: lowercase; + border: 1px solid transparent; + justify-self: start; + white-space: nowrap; +} + +/* In a property row the rdf-type pill is no longer shown — the term type is + conveyed by the value rendering itself (links for resources, plain text for + literals). The .ldh-rdf-type component below is still used by the RDF-tags + card and the graph detail panel. */ +.ldh-rdf-type.class { color: var(--ldh-violet-700); background: var(--ldh-violet-50); border-color: var(--ldh-violet-100); } +.ldh-rdf-type.resource { color: var(--ldh-blue-700); background: var(--ldh-blue-50); border-color: var(--ldh-blue-100); } +.ldh-rdf-type.literal { color: var(--success-500); background: var(--success-50); border-color: #c8eedb; } +.ldh-rdf-type.blank { color: var(--warning-500); background: var(--warning-50); border-color: #f6e1ad; } +.ldh-rdf-type.named { color: var(--ldh-violet-700); background: #f4ecfb; border-color: #e1cdf6; } +.ldh-rdf-type::before { + content: ""; width: 6px; height: 6px; border-radius: 50%; background: currentColor; opacity: 0.85; +} + +/* ---------- Text block (XHTML embedded content) ---------- */ +/* No own padding: it sits inside .ldh-block-body, which already provides the + block's content inset. Re-padding here double-indented the prose past the + block header and the other blocks (charts, etc.). */ +.ldh-text-block { padding: 0; } +.ldh-text-block p { margin: 0 0 var(--sp-3); color: var(--ink-2); line-height: var(--lh-base); font-size: var(--fs-base); } +.ldh-text-block p:last-child { margin-bottom: 0; } +.ldh-text-block a { color: var(--fg-link); } + +/* ---------- Empty state for misc ---------- */ +.ldh-empty { + padding: var(--sp-6); text-align: center; color: var(--fg-hint); font-size: var(--fs-sm); +} + +/* ========================================================================= + BLOCK STATES — failed / empty / loading. + + DECISION: a block KEEPS its card chrome and header in every state; only the + BODY swaps. It does not collapse to a bare alert. Three reasons: + 1. A block is a persistent addressable object — it has an IRI and block + actions (copy IRI, edit query, delete). Collapsing to an alert strips + exactly the affordances the failure case needs most. + 2. The title comes from RDF and is known before the query resolves, so + there is nothing to gain by hiding it while loading. + 3. Layout stability — the card holds its footprint, so the document does + not reflow as blocks resolve. + The header gains one status marker so the state is legible when the block is + scrolled past or collapsed. + ========================================================================= */ + +/* Header status marker — sits after the title, before the actions. */ +.ldh-block-state { + display: inline-flex; align-items: center; gap: 4px; + margin-left: var(--sp-2); padding: 1px 8px; + border-radius: var(--r-pill); flex-shrink: 0; + font-family: var(--font-mono); font-size: var(--fs-xs); font-weight: var(--fw-medium); + white-space: nowrap; vertical-align: middle; +} +.ldh-block-state .msi { font-size: 13px; } +.ldh-block-state.is-failed { background: var(--danger-50); color: var(--danger-500); } +.ldh-block-state.is-empty { background: var(--surface-2); color: var(--fg-muted); } +.ldh-block-state.is-loading { background: var(--surface-2); color: var(--fg-muted); } +.ldh-block-state.is-loading .msi { animation: spin 1.1s linear infinite; } +/* A failed block gets a warm ring so it's findable in a long document. This is + an `outline`, not a border: skins differ on whether cards even have borders + (m3 uses box-shadow elevation and sets border-width: 0), and an outline + survives both without fighting the skin's elevation shadow. Offset inward so + it lands on the card edge rather than outside it. */ +.ldh-block.is-failed { + outline: 1px solid color-mix(in srgb, var(--danger-500) 38%, transparent); + outline-offset: -1px; + border-color: color-mix(in srgb, var(--danger-500) 28%, var(--border-default)); +} + +/* Error body: the alert owns the message; the raw detail is demoted. */ +.ldh-block-error { display: flex; flex-direction: column; gap: var(--sp-3); } +/* Technical detail — collapsed by default, mono and muted when open. Never the + first thing read, but never hidden from the person who needs it either. */ +.ldh-block-detail { border-top: 1px dashed var(--border-default); padding-top: var(--sp-3); } +.ldh-block-detail > summary { + display: inline-flex; align-items: center; gap: 5px; + cursor: pointer; list-style: none; + font-family: var(--font-sans); font-size: var(--fs-xs); font-weight: var(--fw-medium); + color: var(--fg-muted); border-radius: var(--r-xs); +} +.ldh-block-detail > summary::-webkit-details-marker { display: none; } +.ldh-block-detail > summary:hover { color: var(--fg-1); } +.ldh-block-detail > summary:focus-visible { outline: 0; box-shadow: var(--focus-ring); } +.ldh-block-detail > summary .msi { font-size: 15px; transition: transform var(--dur-fast) var(--ease-out-soft); } +.ldh-block-detail[open] > summary .msi { transform: rotate(90deg); } +.ldh-block-detail pre { + margin: var(--sp-2) 0 0; padding: var(--sp-3); overflow-x: auto; + background: var(--surface-2); border-radius: var(--r-sm); + font-family: var(--font-mono); font-size: var(--fs-xs); line-height: 1.6; + color: var(--fg-muted); white-space: pre-wrap; word-break: break-word; +} + +/* Empty body: literal, quiet, with the one action that resolves it. Per the + copy rules — "No results." not "Nothing here yet 👀". */ +.ldh-block-blank { + display: flex; flex-direction: column; align-items: center; gap: var(--sp-2); + padding: var(--sp-8) var(--sp-4); text-align: center; +} +.ldh-block-blank > .msi { font-size: 28px; color: var(--ink-5); } +.ldh-block-blank .bb-msg { font-size: var(--fs-base); color: var(--fg-2); font-weight: var(--fw-medium); } +.ldh-block-blank .bb-sub { font-size: var(--fs-sm); color: var(--fg-hint); max-width: 46ch; line-height: var(--lh-base); } +.ldh-block-blank .bb-act { margin-top: var(--sp-2); } + +/* Loading body: skeleton shaped like the content it replaces. */ +.ldh-block-loading { display: flex; flex-direction: column; gap: var(--sp-3); } +.ldh-block-loading .bl-row { display: flex; align-items: center; gap: var(--sp-3); } +/* Core's .ldhc-skel-group is width:100% (it's normally a block-level stack), so + a fixed-width skeleton used as a flex child still claims a 100% flex base and + over-subscribes the row — leaving no free space for a flex:1 sibling to grow + into, which collapses it to 0. Pin the fixed columns to their content width so + the fluid column keeps the remainder. */ +.ldh-block-loading .bl-row > .ldhc-skel-group { flex: 0 0 auto; width: auto; } + +/* ---------- View-block toolbar (facets / sort / count, sits inside .ldh-block between head and body) ---------- */ +.ldh-view-toolbar { + display: flex; align-items: center; gap: var(--sp-2); + padding: var(--sp-3) var(--sp-6); + border-bottom: 1px dashed var(--border-default); + background: var(--surface-1); + flex-wrap: wrap; +} +.ldh-view-toolbar .left { display: flex; align-items: center; gap: var(--sp-2); flex-wrap: wrap; } +.ldh-view-toolbar .right { display: flex; align-items: center; gap: var(--sp-2); margin-left: auto; } +.ldh-view-toolbar .count { + font-family: var(--font-mono); font-size: var(--fs-xs); + color: var(--fg-muted); white-space: nowrap; +} +.ldh-view-toolbar .count b { color: var(--fg-1); font-weight: var(--fw-semibold); } + +/* Facet pills — read like Linear/Notion filter chips. Compact, removable. */ +.facet-pill { + display: inline-flex; align-items: center; gap: 6px; + height: 28px; padding: 0 10px; + background: var(--surface-0); + border: 1px solid var(--border-default); + border-radius: var(--r-pill); + font-family: var(--font-sans); font-size: var(--fs-sm); + color: var(--fg-2); + cursor: pointer; + transition: background var(--dur-fast) var(--ease-out-soft), border-color var(--dur-fast) var(--ease-out-soft); + white-space: nowrap; +} +.facet-pill:hover { border-color: var(--border-strong); background: var(--surface-2); } +.facet-pill .pred { + font-family: var(--font-mono); font-size: 10px; letter-spacing: 0.02em; + color: var(--fg-hint); text-transform: lowercase; +} +.facet-pill .val { font-weight: var(--fw-medium); color: var(--fg-1); } +.facet-pill.is-active { background: var(--ldh-blue-50); color: var(--ldh-blue-700); border-color: var(--ldh-blue-200); } +.facet-pill.is-active .pred { color: var(--ldh-blue-600); } +.facet-pill.is-active .val { color: var(--ldh-blue-700); } +.facet-pill .x { + display: inline-flex; align-items: center; justify-content: center; + width: 16px; height: 16px; border-radius: 50%; + color: var(--fg-muted); + margin-left: 2px; +} +.facet-pill .x:hover { background: rgba(0,0,0,0.06); color: var(--fg-1); } +.facet-pill .caret { color: var(--fg-muted); margin-left: 1px; } +.facet-pill.is-open { border-color: var(--ldh-blue-300); background: var(--surface-2); } +.facet-pill.add { + border-style: dashed; color: var(--fg-muted); background: transparent; +} +.facet-pill.add:hover { color: var(--fg-1); border-color: var(--border-strong); } + +/* Filters lead icon + clear-all */ +.ldh-view-toolbar .facet-lead { display: inline-flex; align-items: center; color: var(--fg-hint); margin-right: 2px; } +.facet-clear-all { + border: 0; background: transparent; cursor: pointer; + font-family: var(--font-sans); font-size: var(--fs-xs); font-weight: var(--fw-medium); + color: var(--fg-link); padding: 0 6px; height: 28px; +} +.facet-clear-all:hover { text-decoration: underline; } + +/* Facet popover (anchored to a pill) */ +.facet-pop { + position: absolute; top: calc(100% + 6px); left: 0; + min-width: 240px; + background: var(--surface-0); border: 1px solid var(--border-default); + border-radius: var(--r-md); box-shadow: var(--shadow-lg); + padding: var(--sp-2); + z-index: 40; +} +.facet-pop .opt { + display: flex; align-items: center; gap: var(--sp-2); + width: 100%; text-align: left; + padding: 6px var(--sp-2); + border: 0; background: transparent; + border-radius: var(--r-sm); + font-family: inherit; font-size: var(--fs-sm); + color: var(--fg-2); + cursor: pointer; +} +.facet-pop .opt:hover { background: var(--bg-hover); color: var(--fg-1); } +.facet-pop .opt .check { + width: 14px; height: 14px; border-radius: 3px; + border: 1.5px solid var(--border-strong); + display: inline-flex; align-items: center; justify-content: center; + color: #fff; background: var(--surface-0); + flex-shrink: 0; +} +.facet-pop .opt.is-on .check { background: var(--ldh-blue-500); border-color: var(--ldh-blue-500); } +.facet-pop .opt .nm { flex: 1; } +.facet-pop .opt .cnt { font-family: var(--font-mono); font-size: 10px; color: var(--fg-hint); } +.facet-pop .facet-values { display: flex; flex-direction: column; gap: 1px; max-height: 264px; overflow: auto; } +.facet-pop .facet-empty { padding: 8px var(--sp-2); font-size: var(--fs-xs); color: var(--fg-muted); } +.facet-pop .head { + display: flex; align-items: center; justify-content: space-between; gap: var(--sp-2); + padding: 6px var(--sp-2) 6px; + border-bottom: 1px solid var(--border-default); + margin-bottom: 4px; +} +.facet-pop .head .pname { + font-family: var(--font-mono); font-size: 10px; letter-spacing: 0.02em; + text-transform: uppercase; color: var(--fg-muted); +} +.facet-pop .head .clear { + border: 0; background: transparent; cursor: pointer; + font-family: var(--font-sans); font-size: 11px; font-weight: var(--fw-medium); + color: var(--fg-link); padding: 0; +} +.facet-pop .head .clear:hover { text-decoration: underline; } + +/* View-mode switcher inside the view-toolbar (grid / list / map / chart) */ +.view-mode-seg { + display: inline-flex; align-items: center; + background: var(--surface-0); + border: 1px solid var(--border-default); + border-radius: var(--r-md); + padding: 2px; +} +.view-mode-seg button { + width: 28px; height: 24px; + display: inline-flex; align-items: center; justify-content: center; + background: transparent; border: 0; border-radius: 4px; + color: var(--fg-muted); +} +.view-mode-seg button:hover { background: var(--bg-hover); color: var(--fg-1); } +.view-mode-seg button.is-on { background: var(--bg-accent-quiet); color: var(--ldh-blue-700); } + +/* Sort dropdown (used in view-toolbar) */ +.sort-drop { + display: inline-flex; align-items: center; gap: 4px; + height: 28px; padding: 0 var(--sp-2) 0 var(--sp-3); + background: var(--surface-0); border: 1px solid var(--border-default); + border-radius: var(--r-md); font-size: var(--fs-sm); color: var(--fg-2); +} +.sort-drop .lbl { font-family: var(--font-mono); font-size: 10px; color: var(--fg-hint); text-transform: uppercase; letter-spacing: 0.02em; margin-right: 2px; } + +/* ---------- GridViewBlock — cards grid ---------- */ +.ldh-grid-block { + display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); + gap: var(--sp-4); + padding: var(--sp-5) var(--sp-6); +} +/* When a view block sits inside the standard shell, its .ldh-block-body + already provides the 24px content inset (titles align to it). The grid and the + view-toolbar must NOT re-pad horizontally or they double it (cards landed at + ~48px while the block title sat at 24px). Drop the redundant horizontal inset + and the grid's doubled bottom padding so everything aligns to the title. */ +.ldh-block-body > .ldh-grid-block { padding: var(--sp-2) 0 0; } +.ldh-block-body > .ldh-view-toolbar { padding-left: 0; padding-right: 0; } +.ldh-grid-block .card { + position: relative; + display: flex; flex-direction: column; align-items: stretch; + background: var(--bg-card); + border: 1px solid var(--border-default); + border-radius: var(--r-md); + overflow: hidden; + text-decoration: none; color: inherit; + transition: border-color var(--dur-fast) var(--ease-out-soft), box-shadow var(--dur-fast) var(--ease-out-soft), transform var(--dur-fast) var(--ease-out-soft); +} +.ldh-grid-block .card:hover { + border-color: var(--ldh-blue-300); + box-shadow: var(--shadow-sm); + transform: translateY(-2px); + text-decoration: none; +} +.ldh-grid-block .card .img { + aspect-ratio: 4 / 3; + display: flex; align-items: center; justify-content: center; + color: rgba(0,0,0,0.45); +} +.ldh-grid-block .card .img .msi { font-size: 56px; } +.ldh-grid-block .card .card-body { padding: var(--sp-3) var(--sp-4); display: flex; flex-direction: column; gap: 4px; } +.ldh-grid-block .card .ti { display: block; font-weight: var(--fw-semibold); color: var(--ldh-blue-700); font-size: var(--fs-base); line-height: var(--lh-snug); } +.ldh-grid-block .card .meta { display: block; font-size: var(--fs-xs); color: var(--fg-muted); line-height: var(--lh-base); } +.ldh-grid-block .card .pin { + position: absolute; top: var(--sp-2); right: var(--sp-2); + display: flex; gap: 2px; + opacity: 0; transition: opacity var(--dur-fast) var(--ease-out-soft); +} +.ldh-grid-block .card:hover .pin { opacity: 1; } +.ldh-grid-block .card .pin .tb { + width: 28px; height: 28px; + background: rgba(255,255,255,0.92); backdrop-filter: blur(6px); + border: 1px solid var(--border-default); border-radius: var(--r-sm); + color: var(--fg-2); + display: inline-flex; align-items: center; justify-content: center; +} + +/* Pastel image fills for category cards */ +.img-lavender { background: var(--surf-lavender); } +.img-mint { background: var(--surf-mint); } +.img-peach { background: var(--surf-peach); } +.img-sky { background: var(--surf-sky); } +.img-blush { background: var(--surf-blush); } +.img-sand { background: var(--surf-sand); } +.img-mesh { background: var(--grad-aurora); } + +/* ---------- ListViewBlock — rows of resources ---------- */ +.ldh-list-block { display: flex; flex-direction: column; } +.ldh-list-block .row { + display: grid; grid-template-columns: 28px 1fr auto auto; + align-items: center; gap: var(--sp-3); + padding: var(--sp-3) var(--sp-6); + border-bottom: 1px solid var(--border-default); + text-decoration: none; color: inherit; + transition: background var(--dur-fast) var(--ease-out-soft); +} +.ldh-list-block .row:last-child { border-bottom: 0; } +.ldh-list-block .row:hover { background: var(--surface-1); text-decoration: none; } +.ldh-list-block .row .ti { + font-weight: var(--fw-medium); color: var(--ldh-blue-700); font-size: var(--fs-base); +} +.ldh-list-block .row .desc { display: block; font-size: var(--fs-sm); color: var(--fg-muted); font-weight: 400; margin-top: 1px; } +.ldh-list-block .row .ic { + width: 28px; height: 28px; border-radius: var(--r-sm); + display: inline-flex; align-items: center; justify-content: center; + color: var(--fg-muted); background: var(--surface-2); +} +.ldh-list-block .row .ts { + font-family: var(--font-mono); font-size: var(--fs-xs); color: var(--fg-hint); white-space: nowrap; +} +.ldh-list-block .row .type { + font-family: var(--font-mono); font-size: 10px; color: var(--fg-muted); + padding: 2px 8px; background: var(--surface-2); border-radius: var(--r-pill); +} + +/* ---------- Full-bleed mode wrappers (Graph / Map) ---------- */ +.ldh-fullbleed { + position: relative; + height: calc(100vh - 56px - 40px - 64px - 64px); /* viewport - header - tabs - actionbar - some breathing */ + min-height: 520px; + border-radius: var(--r-lg); + overflow: hidden; + border: 1px solid var(--border-default); +} +.ldh-fullbleed .overlay { + position: absolute; + z-index: 5; + background: rgba(255,255,255,0.92); + backdrop-filter: blur(8px); + border: 1px solid var(--border-default); + border-radius: var(--r-md); + padding: var(--sp-2) var(--sp-3); + font-size: var(--fs-sm); color: var(--fg-2); + box-shadow: var(--shadow-sm); +} +.ldh-fullbleed .overlay.on-dark { + background: rgba(8, 14, 32, 0.7); + border-color: rgba(255,255,255,0.10); + color: rgba(255,255,255,0.92); +} +.ldh-fullbleed .overlay.tl { top: var(--sp-3); left: var(--sp-3); } +.ldh-fullbleed .overlay.tr { top: var(--sp-3); right: var(--sp-3); } +.ldh-fullbleed .overlay.bl { bottom: var(--sp-3); left: var(--sp-3); } +.ldh-fullbleed .overlay.bc { bottom: var(--sp-3); left: 50%; transform: translateX(-50%); } + +/* Graph viewer */ +.graph-canvas { + position: absolute; inset: 0; + background: + radial-gradient(circle at 1px 1px, rgba(255,255,255,0.10) 1px, transparent 1.4px) 0 0 / 22px 22px, + radial-gradient(60% 60% at 30% 20%, rgba(77,148,248,0.22) 0%, transparent 60%), + radial-gradient(50% 50% at 80% 70%, rgba(153,84,187,0.20) 0%, transparent 65%), + linear-gradient(180deg, #0a1230 0%, #141d3d 100%); +} +.graph-canvas svg { width: 100%; height: 100%; display: block; } +.graph-canvas .node-label { + position: absolute; transform: translate(-50%, -50%); + font-family: var(--font-mono); font-size: 10px; + color: rgba(255,255,255,0.85); + background: rgba(8, 14, 32, 0.6); + padding: 1px 6px; border-radius: 3px; + pointer-events: none; white-space: nowrap; +} +.graph-filters .row { + display: flex; align-items: center; gap: var(--sp-2); + padding: 4px 0; + font-size: var(--fs-sm); color: rgba(255,255,255,0.85); + cursor: pointer; +} +.graph-filters .row .check { + width: 14px; height: 14px; border-radius: 3px; + border: 1.5px solid rgba(255,255,255,0.45); + display: inline-flex; align-items: center; justify-content: center; + color: #0a1230; +} +.graph-filters .row.is-on .check { background: var(--ldh-blue-400); border-color: var(--ldh-blue-400); } +.graph-filters .row.is-disabled { opacity: 0.45; } + +.graph-zoom { + display: flex; flex-direction: column; gap: 2px; + padding: 2px; +} +.graph-zoom button { + width: 32px; height: 28px; + background: transparent; border: 0; color: var(--fg-2); + border-radius: 4px; + display: inline-flex; align-items: center; justify-content: center; +} +.graph-zoom button + button { border-top: 1px solid var(--border-default); border-radius: 0; } +.graph-zoom button:hover { background: var(--surface-2); } + +/* Selected-node side panel (right-side floating) */ +.graph-detail { + position: absolute; top: var(--sp-3); right: var(--sp-3); bottom: var(--sp-3); + width: 280px; + background: rgba(8, 14, 32, 0.84); + backdrop-filter: blur(12px); + color: rgba(255,255,255,0.92); + border: 1px solid rgba(255,255,255,0.10); + border-radius: var(--r-md); + padding: var(--sp-4); + display: flex; flex-direction: column; gap: var(--sp-3); + font-size: var(--fs-sm); + z-index: 6; +} +.graph-detail .iri { font-family: var(--font-mono); font-size: var(--fs-xs); color: rgba(255,255,255,0.65); word-break: break-all; } +.graph-detail .h { font-weight: var(--fw-semibold); color: #fff; } +.graph-detail dl { display: grid; grid-template-columns: 96px 1fr; gap: 4px var(--sp-3); margin: 0; } +.graph-detail dt { font-family: var(--font-mono); font-size: 10px; color: rgba(255,255,255,0.55); text-align: right; letter-spacing: 0.02em; } +.graph-detail dd { margin: 0; color: rgba(255,255,255,0.92); } + +/* Map canvas */ +.map-canvas { + position: absolute; inset: 0; + background: + radial-gradient(ellipse at 30% 40%, #f5edd9 0%, #f5edd9 12%, transparent 14%), + radial-gradient(ellipse at 55% 50%, #f5edd9 0%, #f5edd9 18%, transparent 20%), + radial-gradient(ellipse at 75% 35%, #f5edd9 0%, #f5edd9 10%, transparent 12%), + radial-gradient(ellipse at 65% 70%, #f5edd9 0%, #f5edd9 14%, transparent 16%), + linear-gradient(180deg, #d6e7f5 0%, #c4dbef 100%); + background-size: 200% 180%, 240% 200%, 180% 160%, 200% 180%, 100% 100%; + background-position: -40% 30%, 25% 20%, 60% -10%, 40% 60%, 0 0; + background-repeat: no-repeat; +} +.map-canvas::before { + content: ""; + position: absolute; inset: 0; + background-image: + linear-gradient(0deg, transparent 49%, rgba(78, 128, 178, 0.12) 50%, transparent 51%), + linear-gradient(90deg, transparent 49%, rgba(78, 128, 178, 0.12) 50%, transparent 51%); + background-size: 80px 80px; + opacity: 0.5; +} +.map-pin { + position: absolute; + width: 16px; height: 16px; border-radius: 50%; + background: var(--ldh-blue-500); + border: 3px solid #fff; + box-shadow: 0 4px 10px -2px rgba(15,23,42,0.45); + transform: translate(-50%, -100%); +} +.map-pin.warn { background: var(--warning-500); } +.map-pin.good { background: var(--success-500); } +.map-pin.accent { background: var(--ldh-violet-500); } +.map-attrib { + position: absolute; bottom: 6px; right: 8px; + font-size: 10px; color: rgba(0,0,0,0.55); font-family: var(--font-sans); + background: rgba(255,255,255,0.7); padding: 1px 6px; border-radius: 3px; +} +.map-scale { + position: absolute; bottom: 28px; left: 14px; + display: flex; align-items: center; gap: 6px; + background: rgba(255,255,255,0.85); padding: 3px 8px; border-radius: 4px; + font-family: var(--font-mono); font-size: 10px; color: var(--fg-2); +} +.map-scale .bar { display: inline-block; width: 50px; height: 4px; background: var(--ink-2); border-radius: 1px; } + +/* Document edit modal — larger than search; reuses .ldh-prop-* primitives */ +/* size="lg" + core header replace the old .ldh-doc-edit width/.mhead rules. */ + +/* IRI bar under the modal header */ +.ldh-doc-iri-edit { + padding: 10px var(--sp-6); + font-family: var(--font-mono); + font-size: var(--fs-xs); + color: var(--ldh-blue-700); + background: var(--surface-1); + border-bottom: 1px solid var(--border-default); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +/* Create-intent: parent container + slug input. The slug input grows to fill + remaining space; the parent IRI is read-only and ellipses on overflow. */ +.ldh-doc-iri-create { + display: flex; + align-items: center; + gap: 6px; + padding: 10px var(--sp-6); + font-family: var(--font-mono); + font-size: var(--fs-xs); + background: var(--surface-1); + border-bottom: 1px solid var(--border-default); + min-width: 0; +} +.ldh-doc-iri-create .msi { + color: var(--ldh-blue-700); + flex-shrink: 0; +} +.ldh-doc-iri-create .parent { + color: var(--ldh-blue-700); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + min-width: 0; + flex-shrink: 1; +} +.ldh-doc-iri-create .trail { + color: var(--ldh-blue-700); + flex-shrink: 0; +} +.ldh-doc-iri-create .slug { + font: inherit; + color: var(--fg-1); + background: var(--surface-0); + border: 1px solid var(--border-default); + border-radius: var(--r-sm); + padding: 3px 8px; + min-width: 0; + flex: 1; + outline: none; + transition: border-color var(--dur-fast) var(--ease-out-soft), box-shadow var(--dur-fast) var(--ease-out-soft); +} +.ldh-doc-iri-create .slug:focus { + border-color: var(--ldh-blue-400); + box-shadow: 0 0 0 3px rgba(77, 148, 248, 0.18); +} +.ldh-doc-edit-body { + flex: 1; overflow: auto; + display: flex; flex-direction: column; +} + +/* ---------- Constructor modal ---------- */ +.ldh-ctor-modal { width: min(760px, calc(100vw - var(--sp-12))); max-height: calc(100vh - 96px); display: flex; flex-direction: column; } +.ldh-ctor-body { flex: 1; overflow: auto; padding: var(--sp-5) var(--sp-6); display: flex; flex-direction: column; gap: var(--sp-4); } +.ldh-ctor-lede { margin: 0; font-size: var(--fs-sm); color: var(--fg-2); line-height: var(--lh-base); } +.ldh-ctor-card { + border: 1px solid var(--border-default); border-radius: var(--r-lg); + background: var(--surface-1); overflow: hidden; +} +.ldh-ctor-card-head { + display: flex; align-items: center; justify-content: space-between; + padding: var(--sp-3) var(--sp-4); + border-bottom: 1px solid var(--border-default); + background: var(--surface-0); +} +.ldh-ctor-card-head .ttl { + font-family: var(--font-mono); font-size: var(--fs-xs); letter-spacing: 0.02em; + color: var(--ldh-blue-700); font-weight: var(--fw-semibold); +} +.ldh-ctor-rmctor { + width: 28px; height: 28px; border: 0; background: transparent; border-radius: var(--r-sm); + display: inline-flex; align-items: center; justify-content: center; color: var(--fg-muted); cursor: pointer; +} +.ldh-ctor-rmctor:hover { background: var(--danger-50); color: var(--danger-500); } +.ldh-ctor-rows { display: flex; flex-direction: column; } +.ldh-ctor-empty { padding: var(--sp-4); font-size: var(--fs-sm); color: var(--fg-muted); text-align: center; } +.ldh-ctor-row { + display: grid; + grid-template-columns: minmax(0, 1.4fr) auto minmax(0, 1fr) 32px; + align-items: center; gap: var(--sp-2); + padding: var(--sp-2) var(--sp-4); + border-top: 1px solid var(--border-default); +} +.ldh-ctor-rows .ldh-ctor-row:first-child { border-top: 0; } +.ldh-ctor-row select { + height: 36px; min-width: 0; padding: 0 var(--sp-2) 0 10px; + border: 1px solid var(--border-default); border-radius: var(--r-sm); + background: var(--surface-0); color: var(--fg-1); + font: inherit; font-size: var(--fs-sm); cursor: pointer; +} +.ldh-ctor-row select.ctor-pred { font-weight: var(--fw-medium); } +.ldh-ctor-row select.ctor-range { font-family: var(--font-mono); } +.ldh-ctor-row select:focus-visible { outline: 0; border-color: var(--ldh-blue-400); box-shadow: 0 0 0 3px rgba(77,148,248,0.18); } + +/* Predicate typeahead (combobox) */ +.ctor-pred-ta { position: relative; min-width: 0; } +.ctor-pred-btn { + display: flex; align-items: baseline; gap: 8px; width: 100%; + height: 36px; padding: 0 8px 0 10px; + border: 1px solid var(--border-default); border-radius: var(--r-sm); + background: var(--surface-0); cursor: pointer; font: inherit; text-align: left; +} +.ctor-pred-btn:hover { border-color: var(--border-strong); } +.ctor-pred-btn.is-open { border-color: var(--ldh-blue-400); box-shadow: 0 0 0 3px rgba(77,148,248,0.18); } +.ctor-pred-btn .lbl { font-size: var(--fs-sm); font-weight: var(--fw-medium); color: var(--fg-1); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } +.ctor-pred-btn .curie { font-family: var(--font-mono); font-size: 10px; color: var(--fg-hint); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; flex: 1; min-width: 0; } +.ctor-pred-btn .caret { color: var(--fg-muted); align-self: center; flex-shrink: 0; } +.ctor-pred-pop { + position: absolute; left: 0; right: 0; top: calc(100% + 4px); z-index: 70; + background: var(--surface-0); border: 1px solid var(--border-default); + border-radius: var(--r-md); box-shadow: var(--shadow-md); + overflow: hidden; min-width: 320px; + animation: menu-pop var(--dur-fast) var(--ease-out-soft); +} +.ctor-pred-search { + display: flex; align-items: center; gap: 6px; + padding: 8px 10px; border-bottom: 1px solid var(--border-default); +} +.ctor-pred-search .msi { color: var(--fg-muted); } +.ctor-pred-search input { + flex: 1; border: 0; outline: none; background: transparent; + font: inherit; font-size: var(--fs-sm); color: var(--fg-1); +} +.ctor-pred-list { max-height: 264px; overflow: auto; padding: 4px; } +.ctor-pred-opt { + display: grid; grid-template-columns: 1fr auto auto; align-items: center; gap: 8px; + width: 100%; text-align: left; + padding: 7px 8px; border: 0; background: transparent; border-radius: var(--r-sm); cursor: pointer; +} +.ctor-pred-opt:hover, .ctor-pred-opt.is-active { background: var(--surface-1); } +.ctor-pred-opt.is-sel { background: var(--bg-accent-quiet); } +.ctor-pred-opt .lbl { font-size: var(--fs-sm); color: var(--fg-1); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } +.ctor-pred-opt .curie { font-family: var(--font-mono); font-size: 11px; color: var(--ldh-blue-700); } +.ctor-pred-opt .chk { color: var(--ldh-blue-700); } +.ctor-pred-empty { padding: 10px; display: flex; flex-direction: column; gap: 2px; font-size: var(--fs-sm); color: var(--fg-2); } +.ctor-pred-empty .hint { font-size: var(--fs-xs); color: var(--fg-muted); } +.ctor-term { + display: inline-flex; padding: 2px; gap: 2px; + background: var(--surface-2); border: 1px solid var(--border-default); border-radius: var(--r-md); +} +.ctor-term button { + border: 0; background: transparent; cursor: pointer; + padding: 4px 10px; border-radius: var(--r-sm); + font: inherit; font-size: var(--fs-xs); font-weight: var(--fw-medium); color: var(--fg-2); +} +.ctor-term button:hover { color: var(--fg-1); } +.ctor-term button.is-on { background: var(--surface-0); color: var(--ldh-blue-700); box-shadow: var(--shadow-xs); } +.ctor-rm { + width: 32px; height: 32px; border: 0; background: transparent; border-radius: var(--r-sm); + display: inline-flex; align-items: center; justify-content: center; color: var(--fg-muted); cursor: pointer; +} +.ctor-rm:hover { background: var(--danger-50); color: var(--danger-500); } +.ldh-ctor-addprop { + display: flex; align-items: center; gap: 6px; width: 100%; + padding: var(--sp-3) var(--sp-4); + border: 0; border-top: 1px dashed var(--border-default); + background: transparent; color: var(--ldh-blue-700); + font: inherit; font-size: var(--fs-sm); font-weight: var(--fw-medium); cursor: pointer; +} +.ldh-ctor-addprop:hover { background: var(--bg-accent-quiet); } +.ldh-ctor-addctor { + align-self: flex-start; + display: inline-flex; align-items: center; gap: 6px; + padding: 8px 12px; + border: 1px dashed var(--border-strong); border-radius: var(--r-md); + background: transparent; color: var(--fg-2); + font: inherit; font-size: var(--fs-sm); font-weight: var(--fw-medium); cursor: pointer; +} +.ldh-ctor-addctor:hover { color: var(--fg-1); border-color: var(--ldh-blue-400); background: var(--surface-1); } + +/* ============================================================ + Resource edit form — subject identity bar + form actions + Shared by the read-mode property editor and the edit/create + modal (via ), so every authoring surface + carries the identical subject-URI control and Actions menu. + ============================================================ */ +.ldh-edit-form { display: flex; flex-direction: column; } + +/* Subject toggle — header button (block head + modal head) that reveals the + advanced subject row. Hidden by default so the resource view stays clean. */ +.tb-subject { + width: 32px; height: 32px; flex-shrink: 0; + display: inline-flex; align-items: center; justify-content: center; + border: 0; background: transparent; border-radius: var(--r-sm); + color: var(--fg-muted); cursor: pointer; + transition: background var(--dur-fast) var(--ease-out-soft), color var(--dur-fast) var(--ease-out-soft); +} +.tb-subject:hover { background: var(--bg-hover); color: var(--fg-1); } +.tb-subject.is-on { background: var(--bg-accent-quiet); color: var(--ldh-blue-700); } + +/* Subject identity — a single row (term-type select + IRI field + copy + + Actions), revealed by the header toggle. Replaces the live product's + chevron-toggled URI row and the redundant read-only summary. */ +.ldh-subject { + display: flex; align-items: center; gap: var(--sp-2); + padding: var(--sp-3) var(--sp-4) var(--sp-3) var(--sp-5); + background: var(--surface-1); + border-bottom: 1px solid var(--border-default); +} +.ldh-subject .term-select { + flex-shrink: 0; + height: 38px; padding: 0 var(--sp-2) 0 12px; + border: 1px solid var(--border-default); border-radius: var(--r-sm); + background: var(--surface-0); color: var(--fg-1); + font: inherit; font-size: var(--fs-sm); font-weight: var(--fw-medium); + cursor: pointer; +} +.ldh-subject .term-select:focus-visible { outline: 0; border-color: var(--ldh-blue-400); box-shadow: 0 0 0 3px rgba(77,148,248,0.18); } +.ldh-subject .uri-field { + flex: 1; min-width: 0; + display: flex; align-items: center; gap: var(--sp-2); + background: var(--surface-0); + border: 1px solid var(--border-default); border-radius: var(--r-sm); + padding: 0 10px; height: 38px; + transition: border-color var(--dur-fast) var(--ease-out-soft), box-shadow var(--dur-fast) var(--ease-out-soft); +} +.ldh-subject .uri-field .msi { color: var(--ldh-blue-700); flex-shrink: 0; } +.ldh-subject .uri-field .msi.warn { color: var(--danger-500); margin-left: auto; } +.ldh-subject .uri-field input { + flex: 1; min-width: 0; border: 0; background: transparent; outline: none; + font-family: var(--font-mono); font-size: var(--fs-sm); color: var(--fg-1); +} +.ldh-subject .uri-field:focus-within { border-color: var(--ldh-blue-400); box-shadow: 0 0 0 3px rgba(77,148,248,0.18); } +.ldh-subject .uri-field.invalid { border-color: var(--danger-500); } +.ldh-subject .uri-field .parent { font-family: var(--font-mono); font-size: var(--fs-sm); color: var(--ldh-blue-700); white-space: nowrap; flex-shrink: 1; overflow: hidden; text-overflow: ellipsis; } +.ldh-subject .uri-field .slug { flex: 1; } +.ldh-subject .uri-field .trail { font-family: var(--font-mono); color: var(--ldh-blue-700); flex-shrink: 0; } +.ldh-subject .bnode-inline { + flex: 1; min-width: 0; + display: flex; align-items: center; gap: var(--sp-2); + height: 38px; font-size: var(--fs-sm); color: var(--fg-2); +} +.ldh-subject code { font-family: var(--font-mono); background: var(--surface-0); border: 1px solid var(--border-default); padding: 2px 7px; border-radius: var(--r-xs); color: var(--fg-1); } +.ldh-subject-tools { display: flex; align-items: center; gap: var(--sp-1); flex-shrink: 0; } +.ldh-subject-copy { + width: 34px; height: 34px; flex-shrink: 0; + display: inline-flex; align-items: center; justify-content: center; + border: 0; background: transparent; border-radius: var(--r-sm); + color: var(--fg-muted); cursor: pointer; + transition: background var(--dur-fast) var(--ease-out-soft), color var(--dur-fast) var(--ease-out-soft); +} +.ldh-subject-copy:hover { background: var(--bg-hover); color: var(--fg-1); } + +/* Form action(s): a single action is a direct button; 2+ become a dropdown. */ +.ldh-form-actions-wrap { position: relative; } +.ldh-form-action { + display: inline-flex; align-items: center; gap: 6px; + height: 34px; padding: 0 10px; + border: 1px solid var(--border-default); border-radius: var(--r-sm); + background: var(--surface-0); color: var(--fg-1); + font: inherit; font-size: var(--fs-sm); font-weight: var(--fw-medium); + cursor: pointer; white-space: nowrap; + transition: background var(--dur-fast) var(--ease-out-soft), border-color var(--dur-fast) var(--ease-out-soft); +} +.ldh-form-action:hover { background: var(--bg-hover); border-color: var(--border-strong); } +.ldh-form-action .caret { margin-left: -2px; transition: transform var(--dur-fast) var(--ease-out-soft); } +.ldh-form-action.is-open .caret { transform: rotate(180deg); } +.ldh-form-actions-menu { + position: absolute; right: 0; top: calc(100% + 6px); + min-width: 264px; + background: var(--surface-0); + border: 1px solid var(--border-default); border-radius: var(--r-md); + box-shadow: var(--shadow-md); + padding: var(--sp-1); z-index: 60; + animation: menu-pop var(--dur-fast) var(--ease-out-soft); +} +.ldh-form-actions-menu .it { + display: grid; grid-template-columns: 28px 1fr; gap: var(--sp-2); + width: 100%; text-align: left; + padding: var(--sp-2); border: 0; background: transparent; + border-radius: var(--r-sm); cursor: pointer; font: inherit; color: var(--fg-1); +} +.ldh-form-actions-menu .it:hover { background: var(--surface-1); } +.ldh-form-actions-menu .it .ico { + width: 28px; height: 28px; border-radius: var(--r-sm); + display: inline-flex; align-items: center; justify-content: center; + background: var(--bg-accent-quiet); color: var(--ldh-blue-700); +} +.ldh-form-actions-menu .it .body { min-width: 0; display: flex; flex-direction: column; } +.ldh-form-actions-menu .it .lbl { font-size: var(--fs-sm); font-weight: var(--fw-medium); } +.ldh-form-actions-menu .it .sub { font-size: var(--fs-xs); color: var(--fg-2); } +.ldh-form-actions-menu .it.is-danger .ico { background: var(--danger-50); color: var(--danger-500); } + +/* Modal chrome is core: .ldhc-backdrop / .ldhc-modal / .ldhc-modal-head|body|foot + (ui_kits/core/overlays.css). The old .ldh-modal-backdrop + .ldh-modal + .mhead + set was a SECOND modal system with no focus trap, no Escape handling and no + depth stacking; all five dialogs now use core , which has all three. + What remains below is per-dialog BODY styling only. */ +/* ---------- Access-request modal ---------- */ +/* The dialog's leading glyph is core ; .ldhc-modal-icon styles it. */ +.ar-body { padding: var(--sp-5); display: flex; flex-direction: column; gap: var(--sp-4); overflow-y: auto; } +.ar-pair { display: grid; grid-template-columns: 1fr 1fr; gap: var(--sp-3); } +.ar-card { + display: flex; flex-direction: column; gap: 4px; + padding: var(--sp-3); border: 1px solid var(--border-default); + border-radius: var(--r-md); background: var(--surface-1); min-width: 0; +} +.ar-k { font-size: var(--fs-xs); color: var(--fg-hint); font-weight: var(--fw-medium); } +.ar-v { display: flex; align-items: center; gap: 6px; font-size: var(--fs-sm); font-weight: var(--fw-medium); color: var(--fg-1); } +.ar-v .msi { color: var(--fg-muted); } +.ar-iri { + font-family: var(--font-mono); font-size: 11px; color: var(--ink-4); + white-space: nowrap; overflow: hidden; text-overflow: ellipsis; +} +.ar-current { + display: flex; align-items: center; gap: var(--sp-2); + font-size: var(--fs-sm); color: var(--fg-2); + background: var(--surf-sky); padding: var(--sp-2) var(--sp-3); border-radius: var(--r-sm); +} +.ar-current .msi { color: var(--bg-accent); flex-shrink: 0; } +.ar-modes { border: 0; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 6px; } +.ar-modes legend { font-size: var(--fs-xs); color: var(--fg-hint); font-weight: var(--fw-medium); padding: 0; margin-bottom: 2px; } +.ar-mode { + display: flex; align-items: center; gap: var(--sp-3); + padding: var(--sp-3); border: 1px solid var(--border-default); + border-radius: var(--r-md); cursor: pointer; + transition: border-color var(--dur-fast) var(--ease-out-soft), background var(--dur-fast) var(--ease-out-soft); +} +.ar-mode:hover { background: var(--bg-hover); } +.ar-mode input { position: absolute; opacity: 0; pointer-events: none; } +.ar-mode.is-on { border-color: var(--bg-accent); background: var(--bg-accent-quiet); } +.ar-mode-ic { + width: 32px; height: 32px; flex-shrink: 0; + display: inline-flex; align-items: center; justify-content: center; + border-radius: var(--r-sm); background: var(--surface-2); color: var(--fg-muted); +} +.ar-mode.is-on .ar-mode-ic { background: var(--bg-accent); color: var(--fg-on-accent); } +.ar-mode-txt { display: flex; flex-direction: column; gap: 1px; min-width: 0; flex: 1; } +.ar-mode-label { display: flex; align-items: center; gap: var(--sp-2); font-size: var(--fs-sm); font-weight: var(--fw-semibold); color: var(--fg-1); } +.ar-mode-pred { font-family: var(--font-mono); font-size: 10px; font-weight: var(--fw-regular); color: var(--ink-4); } +.ar-mode-desc { font-size: var(--fs-xs); color: var(--fg-muted); } +.ar-mode-check { color: var(--bg-accent); opacity: 0; flex-shrink: 0; } +.ar-mode.is-on .ar-mode-check { opacity: 1; } +.ar-msg { display: flex; flex-direction: column; gap: 6px; } +.ar-opt { font-weight: var(--fw-regular); color: var(--ink-4); } +.ar-msg textarea { + font: inherit; font-size: var(--fs-sm); color: var(--fg-1); + padding: var(--sp-2) var(--sp-3); border: 1px solid var(--border-default); + border-radius: var(--r-md); background: var(--surface-0); resize: vertical; +} +.ar-msg textarea:focus { outline: none; border-color: var(--ldh-blue-400); box-shadow: 0 0 0 3px var(--bg-accent-quiet); } +.ar-foot { + display: flex; align-items: center; justify-content: space-between; gap: var(--sp-3); + padding: var(--sp-3) var(--sp-5); border-top: 1px solid var(--border-default); background: var(--surface-1); +} +.ar-foot-note { display: inline-flex; align-items: center; gap: 6px; font-size: var(--fs-xs); color: var(--fg-hint); } +.ar-foot-note .msi { font-size: 14px; } +.ar-foot-btns { display: flex; align-items: center; gap: var(--sp-2); } +.ar-sent { padding: var(--sp-8) var(--sp-6); text-align: center; display: flex; flex-direction: column; align-items: center; gap: var(--sp-2); } +.ar-sent-ic { + width: 56px; height: 56px; margin-bottom: var(--sp-2); + display: inline-flex; align-items: center; justify-content: center; + border-radius: 999px; background: var(--success-50); color: var(--success-500); +} +.ar-sent-ic .msi { font-size: 28px; } +.ar-sent h3 { margin: 0; font-size: var(--fs-lg); font-weight: var(--fw-semibold); color: var(--fg-1); } +.ar-sent p { margin: 0 0 var(--sp-3); font-size: var(--fs-sm); color: var(--fg-muted); max-width: 360px; } +.ldhc-modal .minput { + display: flex; align-items: center; gap: var(--sp-2); + padding: 0 var(--sp-6); + border-bottom: 1px solid var(--border-default); + height: 64px; +} +.ldhc-modal .minput input { + flex: 1; height: 100%; border: 0; outline: 0; + background: transparent; font: inherit; font-size: var(--fs-lg); color: var(--fg-1); +} +.ldhc-modal .minput .msi { color: var(--fg-muted); } +.ldhc-modal .mtoolbar { + display: flex; align-items: center; gap: var(--sp-2); + padding: var(--sp-2) var(--sp-5); + background: var(--surface-1); + border-bottom: 1px solid var(--border-default); + font-size: var(--fs-sm); color: var(--fg-muted); +} +.ldhc-modal .mtoolbar .count b { + display: inline-block; min-width: 20px; padding: 2px 6px; + background: var(--ink-1); color: #fff; border-radius: var(--r-pill); + font-family: var(--font-mono); font-size: 10px; font-weight: 600; text-align: center; + margin-left: 6px; +} +.ldhc-modal .mtoolbar .right { margin-left: auto; display: flex; align-items: center; gap: var(--sp-2); } +.ldhc-modal .mlist { overflow-y: auto; flex: 1; padding: var(--sp-2); } +.ldhc-modal .mresult { + display: grid; grid-template-columns: 32px 1fr auto; + gap: var(--sp-3); align-items: center; + padding: var(--sp-3); border-radius: var(--r-sm); + cursor: pointer; +} +.ldhc-modal .mresult:hover { background: var(--surface-1); } +.ldhc-modal .mresult .ic { + width: 32px; height: 32px; border-radius: var(--r-sm); + background: var(--bg-accent-quiet); color: var(--ldh-blue-700); + display: inline-flex; align-items: center; justify-content: center; +} +.ldhc-modal .mresult .ti { font-weight: var(--fw-medium); color: var(--ldh-blue-700); } +.ldhc-modal .mresult .meta { display: block; font-size: var(--fs-xs); color: var(--fg-muted); margin-top: 2px; font-family: var(--font-mono); } +.ldhc-modal .mresult .badge { + font-family: var(--font-mono); font-size: 10px; color: var(--fg-muted); + padding: 2px 8px; background: var(--surface-2); border-radius: var(--r-pill); +} +.ldhc-modal .mhint { + display: flex; align-items: center; gap: var(--sp-3); + padding: var(--sp-3) var(--sp-5); + background: var(--surface-1); + border-top: 1px solid var(--border-default); + font-size: var(--fs-xs); color: var(--fg-muted); +} +.ldhc-modal .mhint kbd { + font-family: var(--font-mono); font-size: 10px; + padding: 2px 6px; border: 1px solid var(--border-default); border-radius: 4px; + background: var(--surface-0); +} +.chart-svg { width: 100%; height: 220px; } +.chart-svg .axis line, .chart-svg .axis path { stroke: var(--line-1); fill: none; } +.chart-svg .axis text { font-family: var(--font-mono); font-size: 9px; fill: var(--fg-muted); } +.chart-svg .grid line { stroke: var(--surface-3); stroke-dasharray: 2 3; } +.chart-svg .series-line { fill: none; stroke: var(--ldh-blue-500); stroke-width: 2; } +.chart-svg .series-area { fill: var(--ldh-blue-100); opacity: 0.5; } +.chart-svg .series-dot { fill: var(--ldh-blue-500); } +.chart-svg .bar { fill: var(--ldh-blue-500); } +.chart-svg .bar:hover { fill: var(--ldh-blue-600); } +.chart-svg .legend text { font-family: var(--font-sans); font-size: 10px; fill: var(--fg-muted); } + +/* ---------- Sidebar (optional, original LDH layout) ---------- */ +/* Sliding drawer anchored to the left edge of the viewport. Full viewport + height — overlays all chrome including header/tabs/actionbar when open. */ +.ldh-sidebar { + position: fixed; + left: 0; + top: 0; + bottom: 0; + width: 288px; + z-index: 60; + background: var(--bg-card); + border-right: 1px solid var(--border-default); + padding: var(--sp-4) var(--sp-3); + display: flex; flex-direction: column; gap: var(--sp-4); + font-size: var(--fs-sm); + overflow: auto; + scrollbar-width: thin; + transform: translateX(-100%); + transition: transform 260ms var(--ease-out-soft), box-shadow 200ms var(--ease-out-soft); + box-shadow: none; +} +.ldh-sidebar.is-open { + transform: translateX(0); + box-shadow: 12px 0 28px -20px rgba(15, 23, 42, 0.22); +} + +.sb-search { + display: flex; align-items: center; gap: 6px; + height: 34px; padding: 0 var(--sp-2); + background: var(--surface-2); + border: 1px solid transparent; + border-radius: var(--r-md); + color: var(--fg-muted); + transition: background var(--dur-fast) var(--ease-out-soft), border-color var(--dur-fast) var(--ease-out-soft); +} +.sb-search:focus-within { background: var(--surface-0); border-color: var(--ldh-blue-400); box-shadow: 0 0 0 3px var(--bg-accent-quiet); } +.sb-search input { + flex: 1; height: 100%; border: 0; outline: 0; background: transparent; + font: inherit; color: var(--fg-1); +} +.sb-search input::placeholder { color: var(--fg-hint); } +.sb-search-kbd { + font-family: var(--font-mono); + font-size: 10px; + letter-spacing: 0.04em; + color: var(--fg-muted); + background: var(--surface-2); + border: 1px solid var(--border-default); + border-radius: 4px; + padding: 1px 5px; + white-space: nowrap; +} +.sb-clear { + width: 22px; height: 22px; + background: transparent; border: 0; border-radius: 4px; + color: var(--fg-muted); + display: inline-flex; align-items: center; justify-content: center; + cursor: pointer; +} +.sb-clear:hover { background: var(--surface-3); color: var(--fg-1); } + +.sb-section { display: flex; flex-direction: column; gap: 4px; } +.sb-heading { + display: flex; align-items: center; justify-content: space-between; + padding: 4px 8px; + font-family: var(--font-mono); + font-size: 10px; + letter-spacing: var(--tracking-wide); + text-transform: uppercase; + color: var(--fg-muted); +} +.sb-heading .sb-count { + font-family: var(--font-mono); font-size: 10px; + background: var(--surface-2); color: var(--fg-muted); + padding: 1px 6px; border-radius: var(--r-pill); + letter-spacing: 0; +} + +.sb-tree, .sb-classes, .sb-other { display: flex; flex-direction: column; gap: 1px; } + +.sb-tree-row, .sb-class-row, .sb-other-row { + display: flex; align-items: center; gap: 6px; + width: 100%; + min-height: 28px; + padding: 4px 8px; + background: transparent; border: 0; border-radius: var(--r-sm); + color: var(--fg-2); + text-decoration: none; + cursor: pointer; + text-align: left; + font: inherit; + transition: background var(--dur-fast) var(--ease-out-soft); +} +.sb-tree-row:hover, .sb-class-row:hover, .sb-other-row:hover { background: var(--bg-hover); color: var(--fg-1); } + +.sb-tree-row .sb-caret { color: var(--fg-hint); } +.sb-tree-row .sb-icon { color: var(--fg-muted); flex-shrink: 0; } +.sb-tree-row .sb-label { flex: 1; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; font-size: var(--fs-sm); } +.sb-tree-row .sb-count { + font-family: var(--font-mono); font-size: 10px; + color: var(--fg-hint); + background: transparent; + padding: 0 2px; +} +.sb-tree-row.is-current { + background: var(--ldh-blue-50); + color: var(--ldh-blue-700); +} +.sb-tree-row.is-current .sb-icon { color: var(--ldh-blue-600); } +.sb-tree-row.is-current .sb-label { font-weight: var(--fw-medium); } +.sb-tree-row.is-current .sb-count { color: var(--ldh-blue-600); } + +.sb-class-row { display: grid; grid-template-columns: 8px 1fr auto auto; gap: 8px; align-items: baseline; padding: 5px 8px; } +.sb-class-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--rdf-class); } +.sb-class-row .sb-label { font-size: var(--fs-sm); color: var(--fg-1); font-weight: var(--fw-medium); } +.sb-class-row .sb-class-iri { font-family: var(--font-mono); font-size: 10px; color: var(--fg-hint); } +.sb-class-row .sb-count { font-family: var(--font-mono); font-size: 10px; color: var(--fg-muted); } + +.sb-other-row { color: var(--fg-2); } +.sb-other-row .sb-out { color: var(--fg-hint); margin-left: auto; opacity: 0; transition: opacity var(--dur-fast) var(--ease-out-soft); } +.sb-other-row:hover .sb-out { opacity: 1; } +.sb-other-row span:not(.sb-out) { font-size: var(--fs-sm); } + +.sb-foot { + margin-top: auto; + display: flex; align-items: center; justify-content: space-between; + padding: 8px 4px 2px; + border-top: 1px solid var(--border-default); + font-family: var(--font-mono); + font-size: 10px; + color: var(--fg-hint); +} + +/* ---------- Floating "Show sidebar" toggle ---------- */ +.sidebar-toggle { + position: fixed; left: var(--sp-4); bottom: var(--sp-4); + display: inline-flex; align-items: center; gap: 6px; + padding: 8px 12px; + background: var(--ink-1); + color: #fff; + border: 0; + border-radius: var(--r-pill); + font-family: var(--font-mono); + font-size: 11px; + letter-spacing: 0.02em; + cursor: pointer; + box-shadow: var(--shadow-lg); + z-index: 80; + transition: transform var(--dur-fast) var(--ease-out-soft), background var(--dur-fast) var(--ease-out-soft); +} +.sidebar-toggle:hover { transform: translateY(-1px); background: var(--ldh-blue-700); } +.sidebar-toggle[aria-pressed="true"] { background: var(--ldh-blue-600); } +.sidebar-toggle .msi { color: rgba(255,255,255,0.9); } + +/* ---------- Parallax navigation panel (right-side cousins / siblings within a view block) ---------- */ +.ldh-parallax { + display: grid; + grid-template-columns: 1fr 220px; + gap: 0; +} +.ldh-parallax .pl-main { min-width: 0; } +.ldh-parallax .pl-rail { + border-left: 1px solid var(--border-default); + background: var(--surface-1); + padding: var(--sp-3) var(--sp-3); + display: flex; flex-direction: column; gap: var(--sp-3); +} +.ldh-parallax .pl-rail .pl-heading { + font-family: var(--font-mono); + font-size: 10px; + letter-spacing: var(--tracking-wide); + text-transform: uppercase; + color: var(--fg-muted); + padding: 0 4px; +} +.ldh-parallax .pl-rail .pl-group { display: flex; flex-direction: column; gap: 2px; } +.ldh-parallax .pl-rail .pl-link { + display: flex; align-items: center; gap: 6px; + padding: 5px 6px; + border-radius: var(--r-sm); + font-size: var(--fs-sm); + color: var(--fg-2); + text-decoration: none; + font-family: var(--font-sans); +} +.ldh-parallax .pl-rail .pl-link:hover { background: var(--bg-hover); color: var(--fg-1); } +.ldh-parallax .pl-rail .pl-link.is-current { + background: var(--bg-accent-quiet); + color: var(--ldh-blue-700); + font-weight: var(--fw-medium); +} +.ldh-parallax .pl-rail .pl-link .msi { color: var(--fg-hint); } +.ldh-parallax .pl-rail .pl-link.is-current .msi { color: var(--ldh-blue-600); } +.ldh-parallax .pl-rail .pl-tag { + font-family: var(--font-mono); font-size: 9.5px; color: var(--fg-hint); + background: var(--surface-2); padding: 1px 6px; border-radius: var(--r-pill); + margin-left: auto; +} +.ldh-parallax .pl-rail .pl-link.is-current .pl-tag { background: var(--surface-0); color: var(--ldh-blue-600); } + +/* ========================================================================= + Pager — container pagination (limit/offset), sits quietly under a view's + list/table. Three zones: page-size selector · prev/status/next · page count. + ========================================================================= */ +.ldh-pager { + display: flex; align-items: center; justify-content: space-between; + gap: var(--sp-4); flex-wrap: wrap; + padding: var(--sp-3) var(--sp-6); + margin-top: var(--sp-2); + border-top: 1px solid var(--border-default); +} +/* When the pager sits flush under a grid/list body that already zeroes its + side padding (inside .ldh-block-body), align it to the same edges. */ +.ldh-block-body > .ldh-pager { padding-left: 0; padding-right: 0; } + +.ldh-pager-size { display: flex; align-items: center; gap: var(--sp-2); } +.ldh-pager-size label { + font-family: var(--font-mono); font-size: var(--fs-xs); + letter-spacing: var(--tracking-wide); text-transform: uppercase; + color: var(--fg-muted); +} +.ldh-pager-select { position: relative; display: inline-flex; align-items: center; } +.ldh-pager-select select { + appearance: none; -webkit-appearance: none; + font-family: var(--font-sans); font-size: var(--fs-sm); color: var(--fg-1); + background: var(--surface-0); + border: 1px solid var(--border-default); border-radius: var(--r-sm); + padding: 5px 26px 5px 10px; cursor: pointer; + transition: border-color var(--dur-fast) var(--ease-out-soft), box-shadow var(--dur-fast) var(--ease-out-soft); +} +.ldh-pager-select select:hover { border-color: var(--border-strong); } +.ldh-pager-select select:focus-visible { outline: 0; border-color: var(--ldh-blue-400); box-shadow: var(--focus-ring); } +.ldh-pager-select .caret { + position: absolute; right: 7px; pointer-events: none; color: var(--fg-hint); +} + +.ldh-pager-nav { display: flex; align-items: center; gap: var(--sp-2); } +/* Pager prev/next are core - - @@ -306,9 +250,6 @@ exclude-result-prefixes="#all" 'container': $container, 'container-id': $container-id, 'method': $method, - 'action': $action, - 'accept-charset': $accept-charset, - 'enctype': $enctype, 'chart-type-id': $chart-type-id, 'category-id': $category-id, 'series-id': $series-id, @@ -318,7 +259,6 @@ exclude-result-prefixes="#all" 'series': $series, 'canvas-id': $canvas-id, 'canvas-class': $canvas-class, - 'form-actions': $form-actions, 'cache': $cache }"/> @@ -370,107 +310,41 @@ exclude-result-prefixes="#all" - - - - + + + + + + ldh:render-chart - + -

- - - - - - - -
-
-
- - - -
-
- - -
-
- - -
-
-
- -
- - - - - - -
- - - - - + + + + + + + + + + + - + @@ -582,65 +456,11 @@ exclude-result-prefixes="#all" - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + @@ -657,70 +477,26 @@ exclude-result-prefixes="#all" - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + - + @@ -743,7 +519,7 @@ exclude-result-prefixes="#all" - + @@ -772,7 +548,8 @@ exclude-result-prefixes="#all" ixsl:http-request($context('request')) => ixsl:then(ldh:rethread-response($context, ?)) => ixsl:then(ldh:handle-response#1) - => ixsl:then(ldh:row-form-response#1) + => ixsl:then(ldh:row-form-response#1) => + ixsl:finally(ldh:reset-cursor#0) "/> @@ -825,16 +602,7 @@ exclude-result-prefixes="#all" - - -
- Could not load query from -
-                                    
-                                
-
-
-
+ @@ -879,11 +647,19 @@ exclude-result-prefixes="#all" - - - - - + + + + + + + + + + + + + @@ -906,16 +682,7 @@ exclude-result-prefixes="#all" - - -
- Error during query execution: -
-                                    
-                                
-
-
-
+ diff --git a/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/bootstrap/2.3.2/client/block/object.xsl b/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/bootstrap/2.3.2/client/block/object.xsl index 173b274e9f..3d1cd34fc1 100644 --- a/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/bootstrap/2.3.2/client/block/object.xsl +++ b/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/bootstrap/2.3.2/client/block/object.xsl @@ -105,13 +105,7 @@ exclude-result-prefixes="#all" - - -
- Self-referencing object detected: -
-
-
+
@@ -205,13 +199,8 @@ exclude-result-prefixes="#all" }))"/> - - -
- Document loaded successfully but resource was not found: -
-
-
+ + @@ -222,7 +211,7 @@ exclude-result-prefixes="#all" -
+
@@ -232,13 +221,7 @@ exclude-result-prefixes="#all" - - -
- Could not load resource: -
-
-
+ -
+
diff --git a/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/bootstrap/2.3.2/client/block/query.xsl b/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/bootstrap/2.3.2/client/block/query.xsl index a1944e0e3a..62903d44d3 100644 --- a/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/bootstrap/2.3.2/client/block/query.xsl +++ b/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/bootstrap/2.3.2/client/block/query.xsl @@ -7,11 +7,13 @@ + + ]> + + + + + + + + + - + + + + + + + + + @@ -63,7 +86,12 @@ exclude-result-prefixes="#all" - + + + + + + + + +
@@ -262,7 +293,7 @@ exclude-result-prefixes="#all" - + @@ -291,37 +322,50 @@ exclude-result-prefixes="#all" - + + + + + + +
+ + + + + + +
+
- - - + + -
+ + + + + + +
@@ -342,22 +386,41 @@ exclude-result-prefixes="#all"
- - - - - + + + + - - - + + + + - - + + + + + + - - + + + + + + + + + + + @@ -374,7 +437,7 @@ exclude-result-prefixes="#all" - + @@ -399,16 +462,9 @@ exclude-result-prefixes="#all" - - - - - - - - - - + + + @@ -420,24 +476,44 @@ exclude-result-prefixes="#all" + + + +
+ +
+
+
+ + - +
- + @@ -449,16 +525,21 @@ exclude-result-prefixes="#all" - + + + - + - + + @@ -484,7 +565,8 @@ exclude-result-prefixes="#all" ixsl:http-request($context('request')) => ixsl:then(ldh:rethread-response($context, ?)) => ixsl:then(ldh:handle-response#1) - => ixsl:then(ldh:row-form-response#1) + => ixsl:then(ldh:row-form-response#1) => + ixsl:finally(ldh:reset-cursor#0) "/> @@ -509,7 +591,7 @@ exclude-result-prefixes="#all" - + Aborting HTTP request that has already been sent @@ -528,7 +610,8 @@ exclude-result-prefixes="#all" ixsl:http-request($context('request'), $controller) => ixsl:then(ldh:rethread-response($context, ?)) => ixsl:then(ldh:handle-response#1) - => ixsl:then(ldh:rdf-document-response#1) + => ixsl:then(ldh:rdf-document-response#1) => + ixsl:finally(ldh:reset-cursor#0) " on-failure="ldh:promise-failure#1"/> @@ -567,18 +650,21 @@ exclude-result-prefixes="#all" - -
- -
+ + + +
+ +
+
@@ -600,16 +686,7 @@ exclude-result-prefixes="#all" - - -
- Error during query execution: -
-                                
-                            
-
-
-
+ + + @@ -94,6 +98,7 @@ exclude-result-prefixes="#all" 'mode': $mode, 'refresh-content': $refresh-content, 'query-uri': $query-uri, + 'form-actions': $form-actions, 'cache': ixsl:get(ixsl:get(ixsl:window(), 'LinkedDataHub.contents'), '`' || $block/@about || '`') }"/> @@ -183,16 +188,7 @@ exclude-result-prefixes="#all" - - -
- Could not load query results from -
-                                    
-                                
-
-
-
+ @@ -287,10 +283,10 @@ exclude-result-prefixes="#all"
- + - + @@ -302,16 +298,18 @@ exclude-result-prefixes="#all" - + @@ -321,6 +319,7 @@ exclude-result-prefixes="#all" + @@ -359,6 +358,7 @@ exclude-result-prefixes="#all" map { 'request': $request, 'container': ., + 'container-id': $container-id, 'count-var-name': $count-var-name, 'cache': $cache }"/> @@ -372,43 +372,134 @@ exclude-result-prefixes="#all" - + + + - + -
    - - -
+ +
+
+ + + + + + + + + + + +
+ +
+ + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + / + +
@@ -429,19 +520,19 @@ exclude-result-prefixes="#all"
-
+
- +
- + @@ -466,6 +557,9 @@ exclude-result-prefixes="#all" + + @@ -496,6 +590,7 @@ exclude-result-prefixes="#all" 'initial-var-name': $initial-var-name, 'focus-var-name': $focus-var-name, 'endpoint': $endpoint, + 'form-actions': $form-actions, 'cache': $cache }"/> @@ -504,8 +599,10 @@ exclude-result-prefixes="#all" + + - + @@ -514,6 +611,23 @@ exclude-result-prefixes="#all"
+ + + + + + + + + + + + + + + + + @@ -536,6 +650,45 @@ exclude-result-prefixes="#all" + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -545,10 +698,16 @@ exclude-result-prefixes="#all" + + + + + + @@ -558,6 +717,10 @@ exclude-result-prefixes="#all" + + + + @@ -592,7 +755,7 @@ exclude-result-prefixes="#all" - + @@ -698,22 +861,6 @@ exclude-result-prefixes="#all" - - - - - - -
- Error during query execution: -
-                        
-                    
-
-
-
-
- @@ -723,42 +870,60 @@ exclude-result-prefixes="#all" + + + + + + + + + + + + + + + + + + @@ -791,6 +956,7 @@ exclude-result-prefixes="#all" + @@ -810,98 +976,132 @@ exclude-result-prefixes="#all" - - - +
+
+ + + + + + + + + + +
+
+ + + + + + + -
-
- -
-
+ + + + + + + - - - - - -
- + + +
-
+
-
-

+ +

+ + + + + + + + +
+
+
+ + + @@ -923,6 +1123,9 @@ exclude-result-prefixes="#all" + + + @@ -937,6 +1140,7 @@ exclude-result-prefixes="#all" + @@ -954,6 +1158,9 @@ exclude-result-prefixes="#all" + + + @@ -979,6 +1186,7 @@ exclude-result-prefixes="#all" + @@ -1020,7 +1228,7 @@ exclude-result-prefixes="#all" - +
@@ -1035,125 +1243,243 @@ exclude-result-prefixes="#all" + + - - - - - - +
+ +
- + + +
- - - + + -
- - - - - - + + + - - - - - - + + + + + + + + + + + + + - + + + + - - - + +
+ + + + + - - - + + + + + + + + +
- - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + - + - - + + - - - - - - - - -
-
    - -
+ + + + + + + + + + + +
+ {ac:label($subject)} +
+
+ +
+ +
+
+
+
+ + + + + + + + +
- +
- + + + + +
+ + +
+ {ac:label(.)} +
+
+ +
+ +
+
+
+
+ + + + + + + + + +
+
+
+ + + - - - - - - +
+ +
- + + +
- - - + + - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -1164,169 +1490,182 @@ exclude-result-prefixes="#all" + - - - + + - - + - - - - - - - - - - - + + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - + + + + - - - - + + -
- - - - - + + + + + + + + + + + + + + + + - - - - -
+
- - - + + + - - BLOCK DELEGATION: view-mode handler triggered - - - BLOCK DELEGATION: block URI = - - BLOCK DELEGATION: cache found: + + + - - - -
+ - + - - BLOCK DELEGATION: pager previous triggered - - - BLOCK DELEGATION: block URI = - - BLOCK DELEGATION: cache found: + + + - - - + + + + + + + - - - - BLOCK DELEGATION: pager next triggered - - - BLOCK DELEGATION: block URI = - - BLOCK DELEGATION: cache found: - - - - - + + + + + + + + + + - + - - BLOCK DELEGATION: container-order triggered - - - BLOCK DELEGATION: block URI = - - BLOCK DELEGATION: cache found: + + + + + + - - - - + - - - - - BLOCK DELEGATION: btn-order-by triggered - - - BLOCK DELEGATION: block URI = - - BLOCK DELEGATION: cache found: + + + + + + + + + + + + - - - + - - + + - + - + - + - + @@ -1347,28 +1686,29 @@ exclude-result-prefixes="#all" + ixsl:finally(ldh:reset-cursor#0)" on-failure="ldh:promise-failure#1"/> - - + + - + + - + - + - + - + @@ -1389,24 +1729,24 @@ exclude-result-prefixes="#all" + ixsl:finally(ldh:reset-cursor#0)" on-failure="ldh:promise-failure#1"/> - + - + - + @@ -1432,24 +1772,24 @@ exclude-result-prefixes="#all" + ixsl:finally(ldh:reset-cursor#0)" on-failure="ldh:promise-failure#1"/> - + - + - + @@ -1475,34 +1815,98 @@ exclude-result-prefixes="#all" + ixsl:finally(ldh:reset-cursor#0)" on-failure="ldh:promise-failure#1"/> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + - - + + - + - - - + + + - - - + + + @@ -1520,33 +1924,58 @@ exclude-result-prefixes="#all" + ixsl:finally(ldh:reset-cursor#0)" on-failure="ldh:promise-failure#1"/> - - - + + - - + + + + + + + + + + + + + + + + + + + + + - + @@ -1604,7 +2033,8 @@ exclude-result-prefixes="#all" + ixsl:finally(ldh:reset-cursor#0)" on-failure="ldh:promise-failure#1"/> @@ -1617,7 +2047,7 @@ exclude-result-prefixes="#all" - + @@ -1626,37 +2056,58 @@ exclude-result-prefixes="#all" + + + + + + + + + + + + + + + + + + + + + + - - - - + + + - - + - - - - - - + + + + + @@ -1672,43 +2123,51 @@ exclude-result-prefixes="#all" - + + ixsl:finally(ldh:reset-cursor#0)" on-failure="ldh:promise-failure#1"/> - - - - - - + + + + + - + + - + - - - - - + + + + + + + + + + + + + @@ -1720,15 +2179,127 @@ exclude-result-prefixes="#all" - + + ixsl:finally(ldh:reset-cursor#0)" on-failure="ldh:promise-failure#1"/> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -1814,6 +2385,7 @@ exclude-result-prefixes="#all" + @@ -1822,16 +2394,9 @@ exclude-result-prefixes="#all" - - -
- Could not load service resource: -
-                                                
-                                            
-
-
-
+ +
@@ -1839,16 +2404,7 @@ exclude-result-prefixes="#all" - - -
- Could not load query resource: -
-                                    
-                                
-
-
-
+ @@ -1864,6 +2420,72 @@ exclude-result-prefixes="#all" + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -1880,6 +2502,7 @@ exclude-result-prefixes="#all" + ldh:render-view @@ -1905,33 +2528,22 @@ exclude-result-prefixes="#all" - + + + + + + - - - - + + + + @@ -1956,27 +2568,28 @@ exclude-result-prefixes="#all" + - - + + - + - - - - - - - - - + + + + + + + + + @@ -1990,32 +2603,41 @@ exclude-result-prefixes="#all" + - - - + + + ldh:parallax-response - + + + + + - - - - + + + + + + ixsl:then(ldh:parallax-property-response#1)" on-failure="ldh:promise-failure#1"/> - + @@ -2031,12 +2653,9 @@ exclude-result-prefixes="#all" -
- Error during query execution: -
-                                    
-                                
-
+ +
@@ -2048,63 +2667,76 @@ exclude-result-prefixes="#all"
+ - - - - + + ldh:parallax-property-response - - - -
  • - - - + - - - - - - - - - - - - - - - - - - - - -
  • -
    - - - - - - - + +
    - - - - + +
    + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + + + + + + + + + + + + + + + + + + + + @@ -2124,47 +2756,37 @@ exclude-result-prefixes="#all" - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + +
    + + + + + + +
  • + +
  • +
    +
    +
    + - - -
    - Error during query execution: -
    -                                    
    -                                
    -
    + + +
  • + +
  • - - @@ -2219,7 +2846,7 @@ exclude-result-prefixes="#all" - + @@ -2242,7 +2869,7 @@ exclude-result-prefixes="#all" - + @@ -2279,12 +2906,40 @@ exclude-result-prefixes="#all" + + + + + + + + + + + + + + + + + + + + + - Error loading result count + + + + + + + @@ -2316,7 +2971,7 @@ exclude-result-prefixes="#all" - + - + + @@ -2384,17 +3040,21 @@ exclude-result-prefixes="#all" - -
    @@ -180,15 +196,15 @@ exclude-result-prefixes="#all" - - + +
    - + - +
    @@ -198,7 +214,7 @@ exclude-result-prefixes="#all" - - + -
    -
    - -
    - - -
    + +
    -
    - - - - - - - - - - - -
    - -
    +
    +
    + + + + + + +
    + +
    + + +
    + +
    - - - + - + - + - + @@ -465,11 +473,17 @@ exclude-result-prefixes="#all" - - - - - + + + + + + + + + + + @@ -480,41 +494,31 @@ exclude-result-prefixes="#all" - - - - - - - - - - - - - - - - - - - - - - - + + + +
    + + + - - + + + +
    +
    + + + +
    - + - - - + + @@ -526,7 +530,6 @@ exclude-result-prefixes="#all" - @@ -536,25 +539,25 @@ exclude-result-prefixes="#all" - + - + - - + + - + - + @@ -574,8 +577,8 @@ exclude-result-prefixes="#all" - - + + @@ -638,7 +641,6 @@ exclude-result-prefixes="#all" - @@ -647,7 +649,7 @@ exclude-result-prefixes="#all" - + @@ -656,7 +658,6 @@ exclude-result-prefixes="#all" - @@ -674,25 +675,17 @@ exclude-result-prefixes="#all" - - + - - - - - + - - - diff --git a/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/bootstrap/2.3.2/client/form.xsl b/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/bootstrap/2.3.2/client/form.xsl index 30fc3973a0..28fa7b2cbf 100644 --- a/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/bootstrap/2.3.2/client/form.xsl +++ b/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/bootstrap/2.3.2/client/form.xsl @@ -42,12 +42,14 @@ xmlns:dct="&dct;" xmlns:typeahead="&typeahead;" xmlns:ldt="&ldt;" xmlns:acl="&acl;" +xmlns:foaf="&foaf;" xmlns:sd="&sd;" xmlns:sh="&sh;" xmlns:sioc="&sioc;" xmlns:spin="&spin;" xmlns:bs2="http://graphity.org/xsl/bootstrap/2.3.2" xmlns:rdfae="https://w3id.org/atomgraph/rdfa-editor#" +xmlns:cm="https://w3id.org/atomgraph/rdfa-editor/content-model#" extension-element-prefixes="ixsl" exclude-result-prefixes="#all" > @@ -112,47 +114,83 @@ WHERE + + + + + - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + - + + + + + + + + + + + + + + + + + + - + + + + + + + + + + + + + @@ -167,6 +205,8 @@ WHERE + + @@ -280,13 +320,13 @@ WHERE -
    - @@ -377,8 +417,8 @@ WHERE
    - - + +
    @@ -388,9 +428,9 @@ WHERE
    - - - + + +
    @@ -404,8 +444,8 @@ WHERE
    - - + +
    @@ -418,12 +458,12 @@ WHERE
    - - - - + + + +
    - +
    @@ -465,25 +505,33 @@ WHERE
    - - + +
    - - - - - - - - + + + + + + + + + + + + + + + + @@ -493,17 +541,30 @@ WHERE - + - - - - + + + + + + + + + + + + + + @@ -529,13 +590,13 @@ WHERE -
    -
    -
    -
    - Signup complete +
    +
    +
    +
    + check_circle
    -
    +

    Congratulations! Your WebID profile has been created. You can see its data below.

    Authentication details have been sent to your email address. @@ -563,7 +624,7 @@ WHERE - + @@ -596,8 +657,8 @@ WHERE [ ldh:load-object-metadata#1, 'ns-metadata-request', 'ns-metadata-response', ldh:set-object-metadata-ns#1 ] ])) => ixsl:then(ldh:merge-object-metadata#1) - => ixsl:then(ldh:render-row-form#1) - => ixsl:finally(ldh:reset-cursor#0) + => ixsl:then(ldh:render-row-form#1) => + ixsl:finally(ldh:reset-cursor#0) " on-failure="ldh:promise-failure#1"/> @@ -649,7 +710,10 @@ WHERE - + + + + @@ -770,14 +834,18 @@ WHERE }), map{ 'duplicates': 'use-last' })"/> - + - + + + @@ -816,13 +884,16 @@ WHERE - - + + + + + + - - + + - @@ -841,70 +912,73 @@ WHERE - - - - - - - - - - - - + + + + + + + + + + + - - - - - - - + + - - + + + + + + + + - + + - - + - - - - - - - - - - + + + - - - - - - - - + +

    + + + + +
    + + + + + + + + + +
    +
    @@ -917,7 +991,7 @@ WHERE - + @@ -928,6 +1002,7 @@ WHERE + @@ -951,7 +1026,8 @@ WHERE - + + @@ -972,7 +1048,6 @@ WHERE - @@ -998,21 +1073,33 @@ WHERE - - - - - + + + + + + + + + + + + + + - - - - - - + + + + @@ -1022,7 +1109,7 @@ WHERE - + @@ -1036,7 +1123,7 @@ WHERE - + @@ -1045,10 +1132,6 @@ WHERE - - - - @@ -1089,7 +1172,8 @@ WHERE ixsl:http-request($context('request')) => ixsl:then(ldh:rethread-response($context, ?)) => ixsl:then(ldh:handle-response#1) - => ixsl:then($callback) + => ixsl:then($callback) => + ixsl:finally(ldh:reset-cursor#0) " on-failure="ldh:promise-failure#1"/> @@ -1097,7 +1181,7 @@ WHERE - + @@ -1108,17 +1192,13 @@ WHERE - + - - - - @@ -1143,27 +1223,28 @@ WHERE ixsl:http-request($context('request')) => ixsl:then(ldh:rethread-response($context, ?)) => ixsl:then(ldh:handle-response#1) - => ixsl:then(ldh:row-form-response#1) + => ixsl:then(ldh:row-form-response#1) => + ixsl:finally(ldh:reset-cursor#0) " on-failure="ldh:promise-failure#1"/> - + - + + - - + + @@ -1175,10 +1256,9 @@ WHERE - - - - + + + @@ -1196,39 +1276,36 @@ WHERE - - - - - + + - - - - - + + + - + + + ixsl:then(ldh:render-add-value#1) => + ixsl:finally(ldh:reset-cursor#0)" on-failure="ldh:promise-failure#1"/> @@ -1237,23 +1314,25 @@ WHERE - + - - + + + - - + + - - - + + + + + + + + + - + + - - - - - - - - - + - - + + @@ -1359,22 +1440,14 @@ WHERE - - - - - - - - - - + + - + - + @@ -1415,8 +1488,9 @@ WHERE ldh:row-form-submit-violation - - + + + + + + @@ -1492,30 +1575,19 @@ WHERE - - - - - - - - - + + + + + - - + - - - - - - - - + + - + @@ -1524,17 +1596,20 @@ WHERE - - + + - + - + + + + + ixsl:finally(ldh:reset-cursor#0)" on-failure="ldh:promise-failure#1"/> @@ -1576,7 +1654,7 @@ WHERE - + @@ -1648,20 +1726,17 @@ WHERE - + + - + - - - - @@ -1697,7 +1772,7 @@ WHERE - + @@ -1784,28 +1859,24 @@ WHERE - - - - + + + + - + - + - + - - - - @@ -1847,23 +1918,20 @@ WHERE - - + + - + + - + - - - - @@ -1955,15 +2023,15 @@ WHERE - + - + - + @@ -1999,24 +2067,20 @@ WHERE - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + @@ -2028,7 +2092,7 @@ WHERE - + @@ -2064,49 +2128,39 @@ WHERE - - - - - - - - - - - - - - - - -
    -
    -
    - -
    -
    -
    -
    -
    -
    -
    - - - - - - - - - - + + + + + + + + + + + + + +
    + + + + +
    +
    +
    +
    - - - - + + + + + + + + + diff --git a/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/bootstrap/2.3.2/client/functions.xsl b/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/bootstrap/2.3.2/client/functions.xsl index 54ec483960..3d6f6af397 100644 --- a/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/bootstrap/2.3.2/client/functions.xsl +++ b/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/bootstrap/2.3.2/client/functions.xsl @@ -45,9 +45,22 @@ exclude-result-prefixes="#all" - + - + + + + + + + + @@ -110,22 +123,17 @@ exclude-result-prefixes="#all" - - - - + - - - + + + + + + <a href=""></a> @@ -181,13 +189,6 @@ exclude-result-prefixes="#all" - - - - - - - @@ -296,84 +297,6 @@ exclude-result-prefixes="#all" - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -422,130 +345,6 @@ exclude-result-prefixes="#all" - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Constructor skipped: a non-empty WHERE clause cannot be instantiated client-side: - - - - - - - Constructor template triple skipped: variable predicate - - - - - - - - - - - - - - - - - - - - - - - - - - - _:instance - &rdf;type - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -748,7 +547,16 @@ exclude-result-prefixes="#all" "/> - + + + + + + @@ -782,5 +590,131 @@ exclude-result-prefixes="#all" - + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + + + + +
    +                    
    +                
    +
    +
    +
    + + + + + + + + + + + + + + + +
    + + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/bootstrap/2.3.2/client/graph3d.xsl b/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/bootstrap/2.3.2/client/graph3d.xsl index 3ccab50832..0bd9c1046e 100644 --- a/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/bootstrap/2.3.2/client/graph3d.xsl +++ b/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/bootstrap/2.3.2/client/graph3d.xsl @@ -122,12 +122,13 @@ WHERE 'graph-state': $graph-state }" as="map(*)"/> - + + => ixsl:then(ldh:handle-graph3d-rdf-response($context, ?)) => + ixsl:finally(ldh:reset-cursor#0) + " on-failure="ldh:promise-failure#1"/> @@ -162,12 +163,13 @@ WHERE 'graph-state': $graph-state }" as="map(*)"/> - + + => ixsl:then(ldh:handle-graph3d-backlinks-response($context, ?)) => + ixsl:finally(ldh:reset-cursor#0) + " on-failure="ldh:promise-failure#1"/>
    @@ -282,7 +284,7 @@ WHERE - + @@ -476,8 +478,8 @@ WHERE
    - - + + @@ -496,7 +498,7 @@ WHERE - + diff --git a/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/bootstrap/2.3.2/client/map.xsl b/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/bootstrap/2.3.2/client/map.xsl index 8c2a464333..e3902e6cc5 100644 --- a/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/bootstrap/2.3.2/client/map.xsl +++ b/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/bootstrap/2.3.2/client/map.xsl @@ -6,6 +6,7 @@ + ]> + + ixsl:finally(ldh:reset-cursor#0)" on-failure="ldh:promise-failure#1"/> @@ -333,8 +338,6 @@ exclude-result-prefixes="#all" - - @@ -350,17 +353,11 @@ exclude-result-prefixes="#all" - - - - + - - - @@ -384,51 +381,62 @@ exclude-result-prefixes="#all" - - - - - - - - - - - - - + + + + + - - - - - - - + + + + + + + - - + + + + + + + - + - + - @@ -438,7 +446,7 @@ exclude-result-prefixes="#all"