diff --git a/.gitignore b/.gitignore index b6d335acaa..0a0a72c505 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,4 @@ /http-tests/out /fuseki .claude/scheduled_tasks.lock +/cli/target/ diff --git a/Dockerfile b/Dockerfile index a0f479ddf1..afcdb0e8d2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -38,8 +38,6 @@ ENV SOURCE_COMMIT=$SOURCE_COMMIT WORKDIR $CATALINA_HOME -ENV CACHE_MODEL_LOADS=true - ENV STYLESHEET=static/com/atomgraph/linkeddatahub/xsl/bootstrap/2.3.2/layout.xsl ENV CACHE_STYLESHEET=true diff --git a/cli/README.md b/cli/README.md new file mode 100644 index 0000000000..b376435346 --- /dev/null +++ b/cli/README.md @@ -0,0 +1,128 @@ +# 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, +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. + +## Build + +Requires Java 21 and Maven: + +```bash +cd cli +mvn package +``` + +This produces the self-contained `target/ldh.jar`. The `cli/bin/ldh` launcher runs it: + +```bash +export PATH="$PATH:$(pwd)/bin" +ldh --help +``` + +## Authentication + +Commands authenticate with a WebID client certificate from a **PKCS12 (.p12) keystore** — the format +produced by `bin/webid-keygen.sh`: + +```bash +ldh get --accept text/turtle \ + -f ssl/owner/keystore.p12 -p "$OWNER_CERT_PWD" \ + https://localhost:4443/ +``` + +Server certificates are not validated (equivalent of `curl -k`), matching the shell scripts' +behavior against self-signed development instances. + +### Environment variable defaults + +Repeated options can be set once via environment variables: + +| Variable | Option | +|---|---| +| `LDH_CERT_FILE` | `-f`, `--cert-file` | +| `LDH_CERT_PASSWORD` | `-p`, `--cert-password` | +| `LDH_BASE` | `-b`, `--base` | +| `LDH_PROXY` | `--proxy` | + +```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 +``` + +## 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 + 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. +- `--proxy` rewrites the request URI's origin to the proxy's origin, like the scripts do; printed + URLs keep the logical origin. +- `post`/`put` read RDF from stdin and resolve relative URIs against the target URI (the scripts' + `turtle --base` piping); `patch` reads a SPARQL 1.1 update from stdin, validates it and sends it + verbatim. + +Shell completion: `source <(ldh generate-completion)` (bash/zsh). + +## Script → command migration + +| Script | Command | +|---|---| +| `get.sh` | `ldh get` | +| `post.sh` | `ldh post` | +| `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` | +| `admin/packages/install-package.sh` | `ldh admin packages install-package` | +| `admin/packages/uninstall-package.sh` | `ldh admin packages uninstall-package` | +| `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` | + +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. + +### 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 + 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 + documenting a default). +- `install-package`/`uninstall-package` print nothing on success instead of the raw HTTP status + code; use the exit code. +- `import-csv`/`import-rdf` run their steps in-process instead of spawning subscripts, and pass + `--description` through to the import metadata. diff --git a/cli/bin/ldh b/cli/bin/ldh new file mode 100755 index 0000000000..93c9da9875 --- /dev/null +++ b/cli/bin/ldh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash + +# Launcher for the LinkedDataHub CLI. +# Uses the jar built by `mvn package` unless LDH_JAR points elsewhere. + +dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +exec java -jar "${LDH_JAR:-$dir/../target/ldh.jar}" "$@" diff --git a/cli/pom.xml b/cli/pom.xml new file mode 100644 index 0000000000..bb7dfa7475 --- /dev/null +++ b/cli/pom.xml @@ -0,0 +1,162 @@ + + + 4.0.0 + + com.atomgraph + linkeddatahub-cli + 1.0.0-SNAPSHOT + jar + + LinkedDataHub CLI + Command line interface for the LinkedDataHub HTTP API + https://github.com/AtomGraph/LinkedDataHub + + + UTF-8 + 21 + 3.1.11 + + + + + info.picocli + picocli + 4.7.7 + + + com.atomgraph + core + 5.0.2 + + + + org.glassfish.jersey.containers + jersey-container-servlet + + + + + org.apache.jena + jena-arq + 6.1.0 + + + + org.apache.httpcomponents + httpcore + + + org.apache.httpcomponents + httpclient-cache + + + org.apache.httpcomponents + httpclient + + + org.apache.httpcomponents + httpclient-osgi + + + org.apache.httpcomponents + httpcore-osgi + + + + + org.glassfish.jersey.connectors + jersey-apache-connector + ${jersey.version} + + + org.glassfish.jersey.media + jersey-media-multipart + ${jersey.version} + + + + jakarta.activation + jakarta.activation-api + 2.1.3 + runtime + + + org.slf4j + slf4j-nop + 2.0.17 + runtime + + + org.junit.jupiter + junit-jupiter + 5.12.2 + test + + + + + ldh + + + org.apache.maven.plugins + maven-compiler-plugin + 3.14.1 + + 21 + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.5.2 + + + org.apache.maven.plugins + maven-shade-plugin + 3.6.0 + + + package + + shade + + + false + + + com.atomgraph.linkeddatahub.cli.LDH + + true + + + + + + + META-INF/hk2-locator/default + + + + false + + + + + *:* + + META-INF/*.SF + META-INF/*.DSA + META-INF/*.RSA + module-info.class + META-INF/versions/*/module-info.class + + + + + + + + + + + diff --git a/cli/src/main/java/com/atomgraph/linkeddatahub/cli/BaseCommand.java b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/BaseCommand.java new file mode 100644 index 0000000000..986b32bf3d --- /dev/null +++ b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/BaseCommand.java @@ -0,0 +1,209 @@ +/* + * 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; + +import com.atomgraph.core.MediaTypes; +import com.atomgraph.linkeddatahub.cli.http.ClientFactory; +import com.atomgraph.linkeddatahub.cli.http.HttpException; +import com.atomgraph.linkeddatahub.cli.http.LDHClient; +import com.atomgraph.linkeddatahub.cli.mixin.CertAuthMixin; +import com.atomgraph.linkeddatahub.cli.mixin.ProxyMixin; +import jakarta.ws.rs.client.Entity; +import jakarta.ws.rs.core.MediaType; +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.util.concurrent.Callable; +import org.apache.jena.rdf.model.Model; +import org.apache.jena.rdf.model.ModelFactory; +import org.apache.jena.rdf.model.Resource; +import org.apache.jena.riot.Lang; +import org.apache.jena.riot.RDFLanguages; +import org.apache.jena.riot.RDFParser; +import picocli.CommandLine.Mixin; +import picocli.CommandLine.Model.CommandSpec; +import picocli.CommandLine.ParameterException; +import picocli.CommandLine.Spec; + +/** + * Base class for all commands: WebID certificate authentication, proxy handling + * and shared RDF/HTTP helpers. + * + * @author Martynas Jusevičius {@literal } + */ +public abstract class BaseCommand implements Callable +{ + + /** Accepted response media type used by the scripts (Accept: text/turtle) */ + protected static final MediaType[] ACCEPT_TURTLE = { com.atomgraph.core.MediaType.TEXT_TURTLE_TYPE }; + /** Accepted response media type for content block sequence scanning */ + protected static final MediaType[] ACCEPT_NTRIPLES = { com.atomgraph.core.MediaType.APPLICATION_NTRIPLES_TYPE }; + /** Turtle request body media type */ + protected static final MediaType TEXT_TURTLE_TYPE = com.atomgraph.core.MediaType.TEXT_TURTLE_TYPE; + + @Spec + private CommandSpec spec; + + @Mixin + private CertAuthMixin certAuth; + + @Mixin + private ProxyMixin proxyMixin; + + private LDHClient client; + + /** + * Returns the lazily-built authenticated client. + * + * @return client instance + */ + protected LDHClient getClient() + { + if (client == null) + { + getCertAuth().validate(getSpec()); + client = new LDHClient(ClientFactory.createClient(getCertAuth().getCertFile(), getCertAuth().getCertPassword()), + new MediaTypes(), getEffectiveProxy()); + } + + return client; + } + + /** + * Returns the proxy URI applied to requests. Commands that target the admin application + * override this to convert the proxy to the admin subdomain. + * + * @return proxy URI or null + */ + protected URI getEffectiveProxy() + { + return getProxyMixin().getProxy(); + } + + /** + * POSTs a model to a document, failing on error status. + * + * @param client client instance + * @param target document URI + * @param model appended model + */ + protected static void post(LDHClient client, URI target, Model model) + { + HttpException.check(target, client.post(target, Entity.entity(model, TEXT_TURTLE_TYPE), ACCEPT_TURTLE)).close(); + } + + /** + * PUTs a model as a document, failing on error status. + * + * @param client client instance + * @param target document URI + * @param model document model + */ + protected static void put(LDHClient client, URI target, Model model) + { + HttpException.check(target, client.put(target, Entity.entity(model, TEXT_TURTLE_TYPE), ACCEPT_TURTLE)).close(); + } + + /** + * Returns the subject resource for an appended description: the --uri value + * resolved against the target document URI, or a fresh blank node when not given. + * + * @param model model to create the resource in + * @param target target document URI + * @param uri --uri option value (absolute or relative, can be null) + * @return subject resource + */ + protected static Resource createSubject(Model model, URI target, String uri) + { + return uri != null ? model.createResource(target.resolve(uri).toString()) : model.createResource(); + } + + /** + * Parses an RDF stream into a model, resolving relative URIs against the base URI + * (the equivalent of the scripts' turtle --base piping). + * + * @param contentType RDF media type + * @param base base URI + * @param in RDF input stream + * @return parsed model + */ + protected Model readModel(String contentType, URI base, InputStream in) + { + Lang lang = RDFLanguages.contentTypeToLang(contentType); + if (lang == null) throw new ParameterException(getSpec().commandLine(), "Unsupported RDF media type: '" + contentType + "'"); + + Model model = ModelFactory.createDefaultModel(); + RDFParser.create().source(in).lang(lang).base(base.toString()).parse(model); + return model; + } + + /** + * Streams a response body to standard output unmodified. + * + * @param response response with the body to stream + * @throws IOException stream error + */ + protected static void printBody(jakarta.ws.rs.core.Response response) throws IOException + { + try (response; InputStream is = response.readEntity(InputStream.class)) + { + is.transferTo(System.out); + System.out.flush(); + } + } + + /** + * Prints a line to standard output. Command results (created document URLs) go through here. + * + * @param value printed value + */ + protected void print(Object value) + { + getSpec().commandLine().getOut().println(value); + } + + /** + * Returns the command spec. + * + * @return command spec + */ + protected CommandSpec getSpec() + { + return spec; + } + + /** + * Returns the certificate options. + * + * @return certificate mixin + */ + protected CertAuthMixin getCertAuth() + { + return certAuth; + } + + /** + * Returns the proxy options. + * + * @return proxy mixin + */ + protected ProxyMixin getProxyMixin() + { + return proxyMixin; + } + +} diff --git a/cli/src/main/java/com/atomgraph/linkeddatahub/cli/LDH.java b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/LDH.java new file mode 100644 index 0000000000..4f3842bbf7 --- /dev/null +++ b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/LDH.java @@ -0,0 +1,98 @@ +/* + * 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; + +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.Delete; +import com.atomgraph.linkeddatahub.cli.command.Get; +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.admin.Admin; +import com.atomgraph.linkeddatahub.cli.command.content.Content; +import com.atomgraph.linkeddatahub.cli.command.imports.Imports; +import org.apache.jena.sys.JenaSystem; +import picocli.AutoComplete; +import picocli.CommandLine; +import picocli.CommandLine.Command; +import picocli.CommandLine.Option; +import picocli.CommandLine.ScopeType; + +/** + * Root command of the LinkedDataHub CLI. + * + * @author Martynas Jusevičius {@literal } + */ +@Command(name = "ldh", + mixinStandardHelpOptions = true, + version = "ldh 1.0.0-SNAPSHOT", + 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, + AutoComplete.GenerateCompletion.class + }) +public class LDH +{ + + @Option(names = "--verbose", scope = ScopeType.INHERIT, description = "Print stack traces of errors") + boolean verbose; + + /** + * CLI entry point. + * + * @param args command line arguments + */ + public static void main(String[] args) + { + JenaSystem.init(); + + CommandLine cmd = new CommandLine(new LDH()); + cmd.setExecutionExceptionHandler(LDH::handleExecutionException); + System.exit(cmd.execute(args)); + } + + static int handleExecutionException(Exception ex, CommandLine cmdLine, CommandLine.ParseResult parseResult) + { + boolean verbose = cmdLine.getCommandSpec().root().userObject() instanceof LDH root && root.verbose; + + cmdLine.getErr().println(cmdLine.getColorScheme().errorText(messageOf(ex))); + if (verbose) ex.printStackTrace(cmdLine.getErr()); + + return CommandLine.ExitCode.SOFTWARE; + } + + static String messageOf(Throwable ex) + { + // unwrap Jersey ProcessingException chains down to the I/O cause, e.g. "Connection refused" + Throwable cause = ex; + while (cause.getCause() != null && (cause instanceof jakarta.ws.rs.ProcessingException || cause.getMessage() == null)) + cause = cause.getCause(); + + return cause.getMessage() != null ? cause.getMessage() : cause.toString(); + } + +} 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 new file mode 100644 index 0000000000..91194f7b21 --- /dev/null +++ b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/AddConstruct.java @@ -0,0 +1,120 @@ +/* + * 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.LDHClient; +import com.atomgraph.linkeddatahub.cli.mixin.BaseMixin; +import com.atomgraph.linkeddatahub.cli.vocab.LDH; +import com.atomgraph.linkeddatahub.cli.vocab.SP; +import java.net.URI; +import java.nio.file.Files; +import java.nio.file.Path; +import org.apache.jena.rdf.model.Model; +import org.apache.jena.rdf.model.ModelFactory; +import org.apache.jena.rdf.model.Resource; +import org.apache.jena.vocabulary.DCTerms; +import org.apache.jena.vocabulary.RDF; +import picocli.CommandLine.Command; +import picocli.CommandLine.Mixin; +import picocli.CommandLine.Option; +import picocli.CommandLine.Parameters; + +/** + * Adds a SPARQL CONSTRUCT query to a document. Mirrors bin/add-construct.sh. + * + * @author Martynas Jusevičius {@literal } + */ +@Command(name = "add-construct", description = "Adds a CONSTRUCT query to a document.") +public class AddConstruct extends BaseCommand +{ + + @Mixin + private BaseMixin baseMixin; + + @Option(names = "--title", required = true, paramLabel = "TITLE", description = "Title of the query") + private String title; + + @Option(names = "--query-file", required = true, paramLabel = "ABS_PATH", description = "Path to the file with the query string") + private Path queryFile; + + @Option(names = "--description", paramLabel = "DESCRIPTION", description = "Description of the query (optional)") + private String description; + + @Option(names = "--uri", paramLabel = "URI", description = "URI of the query (optional, blank node if not set)") + private String uri; + + @Option(names = "--service", paramLabel = "SERVICE_URI", description = "URI of the SPARQL service (optional)") + private URI service; + + @Parameters(paramLabel = "TARGET_URI", description = "URI of the document") + private URI target; + + @Override + public Integer call() throws Exception + { + baseMixin.require(getSpec()); // required by the script interface + + core(getClient(), target, uri, title, Files.readString(queryFile), service, description); + print(target); + + return 0; + } + + /** + * Appends the CONSTRUCT query description to the target document. + * + * @param client client instance + * @param target target document URI + * @param uri query URI (optional) + * @param title query title + * @param queryText query string + * @param service SPARQL service URI (optional) + * @param description query description (optional) + */ + public static void core(LDHClient client, URI target, String uri, String title, String queryText, URI service, String description) + { + post(client, target, buildModel(target, uri, SP.Construct, title, queryText, service, description)); + } + + /** + * Builds a SPIN query description. + * + * @param target target document URI + * @param uri query URI (optional) + * @param queryType SPIN query class (sp:Construct or sp:Select) + * @param title query title + * @param queryText query string + * @param service SPARQL service URI (optional) + * @param description query description (optional) + * @return query model + */ + public static Model buildModel(URI target, String uri, Resource queryType, String title, String queryText, URI service, String description) + { + Model model = ModelFactory.createDefaultModel(); + + Resource query = createSubject(model, target, uri). + addProperty(RDF.type, queryType). + addProperty(DCTerms.title, title). + addProperty(SP.text, queryText); + if (service != null) query.addProperty(LDH.service, model.createResource(service.toString())); + if (description != null) query.addProperty(DCTerms.description, description); + + return model; + } + +} 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 new file mode 100644 index 0000000000..8114dec2ab --- /dev/null +++ b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/AddFile.java @@ -0,0 +1,141 @@ +/* + * 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.http.LDHClient; +import com.atomgraph.linkeddatahub.cli.mixin.BaseMixin; +import com.atomgraph.linkeddatahub.cli.util.Digests; +import com.atomgraph.linkeddatahub.cli.vocab.NFO; +import jakarta.ws.rs.client.Entity; +import jakarta.ws.rs.core.MediaType; +import java.io.IOException; +import java.net.URI; +import java.nio.file.Files; +import java.nio.file.Path; +import org.apache.jena.vocabulary.DCTerms; +import org.apache.jena.vocabulary.RDF; +import org.glassfish.jersey.media.multipart.FormDataMultiPart; +import org.glassfish.jersey.media.multipart.file.FileDataBodyPart; +import picocli.CommandLine.Command; +import picocli.CommandLine.Mixin; +import picocli.CommandLine.Option; +import picocli.CommandLine.Parameters; + +/** + * Uploads a file using the RDF/POST multipart encoding. Mirrors bin/add-file.sh. + * + * @author Martynas Jusevičius {@literal } + */ +@Command(name = "add-file", description = "Uploads a file.") +public class AddFile extends BaseCommand +{ + + @Mixin + private BaseMixin baseMixin; + + @Option(names = "--title", required = true, paramLabel = "TITLE", description = "Title of the file") + private String title; + + @Option(names = "--description", paramLabel = "DESCRIPTION", description = "Description of the file (optional)") + private String description; + + @Option(names = "--file", required = true, paramLabel = "ABS_PATH", description = "Path to the file") + private Path file; + + @Option(names = "--content-type", paramLabel = "MEDIA_TYPE", description = "Media type of the file (optional, auto-detected if not set)") + private String contentType; + + @Parameters(paramLabel = "TARGET_URI", description = "URI of the document") + private URI target; + + @Override + public Integer call() throws Exception + { + URI base = baseMixin.require(getSpec()); + + URI fileURI = core(getClient(), base, target, file, contentType, title, description); + print(fileURI); + + return 0; + } + + /** + * Uploads the file to the target document and returns its content-addressed upload URI. + * The RDF/POST field order is positional: each pu must immediately precede + * its ol/ou value. + * + * @param client client instance + * @param base application base URI + * @param target target document URI + * @param file file path + * @param contentType file media type (optional, auto-detected if null) + * @param title file title + * @param description file description (optional) + * @return upload URI derived from the SHA1 hash of the file content + * @throws IOException file read error + */ + public static URI core(LDHClient client, URI base, URI target, Path file, String contentType, String title, String description) throws IOException + { + String fileContentType = contentType != null ? contentType : detectContentType(file); + + try (FormDataMultiPart multiPart = buildMultiPart(file, fileContentType, title, description)) + { + HttpException.check(target, client.post(target, Entity.entity(multiPart, multiPart.getMediaType()), ACCEPT_TURTLE)).close(); + } + + return URI.create(base.toString() + "uploads/" + Digests.sha1Hex(file)); + } + + /** + * Builds the RDF/POST multipart body. + * + * @param file file path + * @param contentType file media type + * @param title file title + * @param description file description (optional) + * @return multipart body + */ + public static FormDataMultiPart buildMultiPart(Path file, String contentType, String title, String description) + { + FormDataMultiPart multiPart = new FormDataMultiPart(); + + multiPart.field("rdf", ""); + multiPart.field("sb", "file"); + multiPart.field("pu", NFO.fileName.getURI()); + multiPart.bodyPart(new FileDataBodyPart("ol", file.toFile(), MediaType.valueOf(contentType))); + multiPart.field("pu", DCTerms.title.getURI()); + multiPart.field("ol", title); + multiPart.field("pu", RDF.type.getURI()); + multiPart.field("ou", NFO.FileDataObject.getURI()); + if (description != null) + { + multiPart.field("pu", DCTerms.description.getURI()); + multiPart.field("ol", description); + } + + return multiPart; + } + + static String detectContentType(Path file) throws IOException + { + String detected = Files.probeContentType(file); + return detected != null ? detected : "application/octet-stream"; + } + +} 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 new file mode 100644 index 0000000000..cb737c570a --- /dev/null +++ b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/AddGenericService.java @@ -0,0 +1,112 @@ +/* + * 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.mixin.BaseMixin; +import com.atomgraph.linkeddatahub.cli.vocab.A; +import com.atomgraph.linkeddatahub.cli.vocab.SD; +import java.net.URI; +import org.apache.jena.rdf.model.Model; +import org.apache.jena.rdf.model.ModelFactory; +import org.apache.jena.rdf.model.Resource; +import org.apache.jena.vocabulary.DCTerms; +import org.apache.jena.vocabulary.RDF; +import picocli.CommandLine.Command; +import picocli.CommandLine.Mixin; +import picocli.CommandLine.Option; +import picocli.CommandLine.Parameters; + +/** + * Appends a generic SPARQL service description to a document. Mirrors bin/add-generic-service.sh. + * + * @author Martynas Jusevičius {@literal } + */ +@Command(name = "add-generic-service", description = "Appends a generic SPARQL service to a document.") +public class AddGenericService extends BaseCommand +{ + + @Mixin + private BaseMixin baseMixin; + + @Option(names = "--title", required = true, paramLabel = "TITLE", description = "Title of the service") + private String title; + + @Option(names = "--description", paramLabel = "DESCRIPTION", description = "Description of the service (optional)") + private String description; + + @Option(names = "--uri", paramLabel = "URI", description = "URI of the service (optional, blank node if not set)") + private String uri; + + @Option(names = "--endpoint", required = true, paramLabel = "ENDPOINT_URI", description = "URI of the SPARQL endpoint") + private URI endpoint; + + @Option(names = "--graph-store", paramLabel = "GRAPH_STORE_URI", description = "URI of the Graph Store Protocol endpoint (optional)") + private URI graphStore; + + @Option(names = "--auth-user", paramLabel = "AUTH_USER", description = "Username for HTTP Basic auth (optional)") + private String authUser; + + @Option(names = "--auth-pwd", paramLabel = "AUTH_PASSWORD", description = "Password for HTTP Basic auth (optional)") + private String authPwd; + + @Parameters(paramLabel = "TARGET_URI", description = "URI of the document") + private URI target; + + @Override + public Integer call() throws Exception + { + baseMixin.require(getSpec()); // required by the script interface + + post(getClient(), target, buildModel(target, uri, title, endpoint, graphStore, authUser, authPwd, description)); + print(target); + + return 0; + } + + /** + * Builds the service description. + * + * @param target target document URI + * @param uri service URI (optional) + * @param title service title + * @param endpoint SPARQL endpoint URI + * @param graphStore Graph Store Protocol endpoint URI (optional) + * @param authUser HTTP Basic auth username (optional) + * @param authPwd HTTP Basic auth password (optional) + * @param description service description (optional) + * @return service model + */ + public static Model buildModel(URI target, String uri, String title, URI endpoint, URI graphStore, String authUser, String authPwd, String description) + { + Model model = ModelFactory.createDefaultModel(); + + Resource service = createSubject(model, target, uri). + addProperty(RDF.type, SD.Service). + addProperty(DCTerms.title, title). + addProperty(SD.endpoint, model.createResource(endpoint.toString())). + addProperty(SD.supportedLanguage, SD.SPARQL11Query). + addProperty(SD.supportedLanguage, SD.SPARQL11Update); + if (graphStore != null) service.addProperty(A.graphStore, model.createResource(graphStore.toString())); + if (authUser != null) service.addProperty(A.authUser, authUser); + if (authPwd != null) service.addProperty(A.authPwd, authPwd); + if (description != null) service.addProperty(DCTerms.description, description); + + return model; + } + +} 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 new file mode 100644 index 0000000000..9b15553827 --- /dev/null +++ b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/AddResultSetChart.java @@ -0,0 +1,110 @@ +/* + * 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.mixin.BaseMixin; +import com.atomgraph.linkeddatahub.cli.vocab.LDH; +import com.atomgraph.linkeddatahub.cli.vocab.SPIN; +import java.net.URI; +import org.apache.jena.rdf.model.Model; +import org.apache.jena.rdf.model.ModelFactory; +import org.apache.jena.rdf.model.Resource; +import org.apache.jena.vocabulary.DCTerms; +import org.apache.jena.vocabulary.RDF; +import picocli.CommandLine.Command; +import picocli.CommandLine.Mixin; +import picocli.CommandLine.Option; +import picocli.CommandLine.Parameters; + +/** + * Appends a chart of SPARQL SELECT results to a document. Mirrors bin/add-result-set-chart.sh. + * + * @author Martynas Jusevičius {@literal } + */ +@Command(name = "add-result-set-chart", description = "Appends a result set chart to a document.") +public class AddResultSetChart extends BaseCommand +{ + + @Mixin + private BaseMixin baseMixin; + + @Option(names = "--title", required = true, paramLabel = "TITLE", description = "Title of the chart") + private String title; + + @Option(names = "--description", paramLabel = "DESCRIPTION", description = "Description of the chart (optional)") + private String description; + + @Option(names = "--uri", paramLabel = "URI", description = "URI of the chart (optional, blank node if not set)") + private String uri; + + @Option(names = "--query", required = true, paramLabel = "QUERY_URI", description = "URI of the SELECT query") + private URI query; + + @Option(names = "--chart-type", required = true, paramLabel = "TYPE_URI", description = "URI of the chart type") + private URI chartType; + + @Option(names = "--category-var-name", required = true, paramLabel = "VAR_NAME", description = "Name of the category variable") + private String categoryVarName; + + @Option(names = "--series-var-name", required = true, paramLabel = "VAR_NAME", description = "Name of the series variable") + private String seriesVarName; + + @Parameters(paramLabel = "TARGET_URI", description = "URI of the document") + private URI target; + + @Override + public Integer call() throws Exception + { + baseMixin.require(getSpec()); // required by the script interface + + post(getClient(), target, buildModel(target, uri, title, query, chartType, categoryVarName, seriesVarName, description)); + print(target); + + return 0; + } + + /** + * Builds the chart description. + * + * @param target target document URI + * @param uri chart URI (optional) + * @param title chart title + * @param query SELECT query URI + * @param chartType chart type URI + * @param categoryVarName category variable name + * @param seriesVarName series variable name + * @param description chart description (optional) + * @return chart model + */ + public static Model buildModel(URI target, String uri, String title, URI query, URI chartType, String categoryVarName, String seriesVarName, String description) + { + Model model = ModelFactory.createDefaultModel(); + + Resource chart = createSubject(model, target, uri). + addProperty(RDF.type, LDH.ResultSetChart). + addProperty(DCTerms.title, title). + addProperty(SPIN.query, model.createResource(query.toString())). + addProperty(LDH.chartType, model.createResource(chartType.toString())). + addProperty(LDH.categoryVarName, categoryVarName). + addProperty(LDH.seriesVarName, seriesVarName); + if (description != null) chart.addProperty(DCTerms.description, description); + + return model; + } + +} 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 new file mode 100644 index 0000000000..63698b3420 --- /dev/null +++ b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/AddSelect.java @@ -0,0 +1,71 @@ +/* + * 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.mixin.BaseMixin; +import com.atomgraph.linkeddatahub.cli.vocab.SP; +import java.net.URI; +import java.nio.file.Files; +import java.nio.file.Path; +import picocli.CommandLine.Command; +import picocli.CommandLine.Mixin; +import picocli.CommandLine.Option; +import picocli.CommandLine.Parameters; + +/** + * Adds a SPARQL SELECT query to a document. Mirrors bin/add-select.sh. + * + * @author Martynas Jusevičius {@literal } + */ +@Command(name = "add-select", description = "Adds a SELECT query to a document.") +public class AddSelect extends BaseCommand +{ + + @Mixin + private BaseMixin baseMixin; + + @Option(names = "--title", required = true, paramLabel = "TITLE", description = "Title of the query") + private String title; + + @Option(names = "--query-file", required = true, paramLabel = "ABS_PATH", description = "Path to the file with the query string") + private Path queryFile; + + @Option(names = "--description", paramLabel = "DESCRIPTION", description = "Description of the query (optional)") + private String description; + + @Option(names = "--uri", paramLabel = "URI", description = "URI of the query (optional, blank node if not set)") + private String uri; + + @Option(names = "--service", paramLabel = "SERVICE_URI", description = "URI of the SPARQL service (optional)") + private URI service; + + @Parameters(paramLabel = "TARGET_URI", description = "URI of the document") + private URI target; + + @Override + public Integer call() throws Exception + { + baseMixin.require(getSpec()); // required by the script interface + + post(getClient(), target, AddConstruct.buildModel(target, uri, SP.Select, title, Files.readString(queryFile), service, description)); + print(target); + + return 0; + } + +} 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 new file mode 100644 index 0000000000..345775f45f --- /dev/null +++ b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/AddView.java @@ -0,0 +1,101 @@ +/* + * 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.mixin.BaseMixin; +import com.atomgraph.linkeddatahub.cli.vocab.AC; +import com.atomgraph.linkeddatahub.cli.vocab.LDH; +import com.atomgraph.linkeddatahub.cli.vocab.SPIN; +import java.net.URI; +import org.apache.jena.rdf.model.Model; +import org.apache.jena.rdf.model.ModelFactory; +import org.apache.jena.rdf.model.Resource; +import org.apache.jena.vocabulary.DCTerms; +import org.apache.jena.vocabulary.RDF; +import picocli.CommandLine.Command; +import picocli.CommandLine.Mixin; +import picocli.CommandLine.Option; +import picocli.CommandLine.Parameters; + +/** + * Appends a view of a SPARQL SELECT query to a document. Mirrors bin/add-view.sh. + * + * @author Martynas Jusevičius {@literal } + */ +@Command(name = "add-view", description = "Appends a view to a document.") +public class AddView extends BaseCommand +{ + + @Mixin + private BaseMixin baseMixin; + + @Option(names = "--query", required = true, paramLabel = "QUERY_URI", description = "URI of the SELECT query") + private URI query; + + @Option(names = "--title", paramLabel = "TITLE", description = "Title of the view (optional)") + private String title; + + @Option(names = "--description", paramLabel = "DESCRIPTION", description = "Description of the view (optional)") + private String description; + + @Option(names = "--uri", paramLabel = "URI", description = "URI of the view (optional, blank node if not set)") + private String uri; + + @Option(names = "--mode", paramLabel = "MODE_URI", description = "URI of the layout mode (optional)") + private URI mode; + + @Parameters(paramLabel = "TARGET_URI", description = "URI of the document") + private URI target; + + @Override + public Integer call() throws Exception + { + baseMixin.require(getSpec()); // required by the script interface + + post(getClient(), target, buildModel(target, uri, query, title, description, mode)); + print(target); + + return 0; + } + + /** + * Builds the view description. + * + * @param target target document URI + * @param uri view URI (optional) + * @param query SELECT query URI + * @param title view title (optional) + * @param description view description (optional) + * @param mode layout mode URI (optional) + * @return view model + */ + public static Model buildModel(URI target, String uri, URI query, String title, String description, URI mode) + { + Model model = ModelFactory.createDefaultModel(); + + Resource view = createSubject(model, target, uri). + addProperty(RDF.type, LDH.View). + addProperty(SPIN.query, model.createResource(query.toString())); + if (title != null) view.addProperty(DCTerms.title, title); + if (description != null) view.addProperty(DCTerms.description, description); + if (mode != null) view.addProperty(AC.mode, model.createResource(mode.toString())); + + return model; + } + +} 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 new file mode 100644 index 0000000000..5e3e327e1d --- /dev/null +++ b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/CreateContainer.java @@ -0,0 +1,114 @@ +/* + * 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.mixin.BaseMixin; +import com.atomgraph.linkeddatahub.cli.util.Slugs; +import com.atomgraph.linkeddatahub.cli.util.URIRewriter; +import com.atomgraph.linkeddatahub.cli.vocab.AC; +import com.atomgraph.linkeddatahub.cli.vocab.DH; +import com.atomgraph.linkeddatahub.cli.vocab.LDH; +import com.atomgraph.linkeddatahub.cli.vocab.SPIN; +import java.net.URI; +import org.apache.jena.rdf.model.Model; +import org.apache.jena.rdf.model.ModelFactory; +import org.apache.jena.rdf.model.Resource; +import org.apache.jena.vocabulary.DCTerms; +import org.apache.jena.vocabulary.RDF; +import picocli.CommandLine.Command; +import picocli.CommandLine.Mixin; +import picocli.CommandLine.Option; + +/** + * Creates a container document. Mirrors bin/create-container.sh. + * + * @author Martynas Jusevičius {@literal } + */ +@Command(name = "create-container", description = "Creates a container document.") +public class CreateContainer extends BaseCommand +{ + + @Mixin + private BaseMixin baseMixin; + + @Option(names = "--title", required = true, paramLabel = "TITLE", description = "Title of the container") + private String title; + + @Option(names = "--description", paramLabel = "DESCRIPTION", description = "Description of the container (optional)") + private String description; + + @Option(names = "--slug", paramLabel = "STRING", description = "String that will be used as URI path segment (optional)") + private String slug; + + @Option(names = "--parent", required = true, paramLabel = "PARENT_URI", description = "URI of the parent container") + private URI parent; + + @Option(names = "--block", paramLabel = "BLOCK_URI", description = "URI of the content block (optional)") + private URI block; + + @Option(names = "--mode", paramLabel = "MODE_URI", description = "URI of the layout mode of the children view (optional)") + private URI mode; + + @Override + public Integer call() throws Exception + { + baseMixin.require(getSpec()); // required by the script interface + + URI doc = URIRewriter.childURI(parent, slug != null ? slug : Slugs.defaultSlug()); + put(getClient(), doc, buildModel(doc, title, description, block, mode)); + print(doc); + + return 0; + } + + /** + * Builds the container document model with its first content block: the given block URI, + * a children view with an explicit mode, or the default children view. + * + * @param doc document URI + * @param title document title + * @param description document description (optional) + * @param block content block URI (optional) + * @param mode children view mode URI (optional, ignored when block is given) + * @return document model + */ + public static Model buildModel(URI doc, String title, String description, URI block, URI mode) + { + Model model = ModelFactory.createDefaultModel(); + + Resource container = model.createResource(doc.toString()). + addProperty(RDF.type, DH.Container). + addProperty(DCTerms.title, title); + + if (block != null) container.addProperty(RDF.li(1), model.createResource(block.toString())); + else if (mode != null) container.addProperty(RDF.li(1), model.createResource(). + addProperty(RDF.type, LDH.Object). + addProperty(RDF.value, model.createResource(). + addProperty(RDF.type, LDH.View). + addProperty(SPIN.query, LDH.SelectChildren). + addProperty(AC.mode, model.createResource(mode.toString())))); + else container.addProperty(RDF.li(1), model.createResource(). + addProperty(RDF.type, LDH.Object). + addProperty(RDF.value, LDH.ChildrenView)); + + if (description != null) container.addProperty(DCTerms.description, description); + + return model; + } + +} 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 new file mode 100644 index 0000000000..6f2e49b830 --- /dev/null +++ b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/CreateItem.java @@ -0,0 +1,90 @@ +/* + * 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.mixin.BaseMixin; +import com.atomgraph.linkeddatahub.cli.util.Slugs; +import com.atomgraph.linkeddatahub.cli.util.URIRewriter; +import com.atomgraph.linkeddatahub.cli.vocab.DH; +import java.net.URI; +import org.apache.jena.rdf.model.Model; +import org.apache.jena.rdf.model.ModelFactory; +import org.apache.jena.rdf.model.Resource; +import org.apache.jena.vocabulary.DCTerms; +import org.apache.jena.vocabulary.RDF; +import picocli.CommandLine.Command; +import picocli.CommandLine.Mixin; +import picocli.CommandLine.Option; + +/** + * Creates an item document. Mirrors bin/create-item.sh. + * + * @author Martynas Jusevičius {@literal } + */ +@Command(name = "create-item", description = "Creates an item document.") +public class CreateItem extends BaseCommand +{ + + @Mixin + private BaseMixin baseMixin; + + @Option(names = "--title", required = true, paramLabel = "TITLE", description = "Title of the item") + private String title; + + @Option(names = "--description", paramLabel = "DESCRIPTION", description = "Description of the item (optional)") + private String description; + + @Option(names = "--slug", paramLabel = "STRING", description = "String that will be used as URI path segment (optional)") + private String slug; + + @Option(names = "--container", required = true, paramLabel = "CONTAINER_URI", description = "URI of the parent container") + private URI container; + + @Override + public Integer call() throws Exception + { + baseMixin.require(getSpec()); // required by the script interface + + URI doc = URIRewriter.childURI(container, slug != null ? slug : Slugs.defaultSlug()); + put(getClient(), doc, buildModel(doc, title, description)); + print(doc); + + return 0; + } + + /** + * Builds the item document model. + * + * @param doc document URI + * @param title document title + * @param description document description (optional) + * @return document model + */ + public static Model buildModel(URI doc, String title, String description) + { + Model model = ModelFactory.createDefaultModel(); + + Resource item = model.createResource(doc.toString()). + addProperty(RDF.type, DH.Item). + addProperty(DCTerms.title, title); + if (description != null) item.addProperty(DCTerms.description, description); + + return model; + } + +} diff --git a/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/Delete.java b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/Delete.java new file mode 100644 index 0000000000..27988751c1 --- /dev/null +++ b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/Delete.java @@ -0,0 +1,45 @@ +/* + * 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 java.net.URI; +import picocli.CommandLine.Command; +import picocli.CommandLine.Parameters; + +/** + * Deletes an RDF document. Mirrors bin/delete.sh. + * + * @author Martynas Jusevičius {@literal } + */ +@Command(name = "delete", description = "Deletes an RDF document.") +public class Delete extends BaseCommand +{ + + @Parameters(paramLabel = "TARGET_URI", description = "URI of the document") + private URI target; + + @Override + public Integer call() throws Exception + { + HttpException.check(target, getClient().delete(target)).close(); + + return 0; + } + +} 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 new file mode 100644 index 0000000000..2244ad231e --- /dev/null +++ b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/Get.java @@ -0,0 +1,63 @@ +/* + * 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 jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import java.net.URI; +import picocli.CommandLine.Command; +import picocli.CommandLine.Option; +import picocli.CommandLine.Parameters; + +/** + * Retrieves an RDF description. Mirrors bin/get.sh. + * + * @author Martynas Jusevičius {@literal } + */ +@Command(name = "get", description = "Retrieves RDF description.") +public class Get extends BaseCommand +{ + + @Option(names = "--accept", required = true, paramLabel = "MEDIA_TYPE", description = "Requested media type (e.g. text/turtle)") + private String accept; + + @Option(names = "--head", description = "Requested headers only, no body (HEAD method)") + private boolean head; + + @Parameters(paramLabel = "TARGET_URI", description = "URI of the document") + private URI target; + + @Override + public Integer call() throws Exception + { + MediaType[] acceptedTypes = { MediaType.valueOf(accept) }; + + if (head) + try (Response response = HttpException.check(target, getClient().head(target, acceptedTypes))) + { + print("HTTP " + response.getStatus() + " " + response.getStatusInfo().getReasonPhrase()); + response.getStringHeaders().forEach((name, values) -> values.forEach(value -> print(name + ": " + value))); + } + else + printBody(HttpException.check(target, getClient().get(target, acceptedTypes))); + + return 0; + } + +} diff --git a/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/Patch.java b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/Patch.java new file mode 100644 index 0000000000..f41a9deed0 --- /dev/null +++ b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/Patch.java @@ -0,0 +1,52 @@ +/* + * 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 java.net.URI; +import java.nio.charset.StandardCharsets; +import org.apache.jena.query.Syntax; +import org.apache.jena.update.UpdateFactory; +import picocli.CommandLine.Command; +import picocli.CommandLine.Parameters; + +/** + * Patches an RDF document with a SPARQL update from standard input. Mirrors bin/patch.sh. + * + * @author Martynas Jusevičius {@literal } + */ +@Command(name = "patch", description = "Patches an RDF document using SPARQL update from stdin.") +public class Patch extends BaseCommand +{ + + @Parameters(paramLabel = "TARGET_URI", description = "URI of the document") + private URI target; + + @Override + public Integer call() throws Exception + { + String update = new String(System.in.readAllBytes(), StandardCharsets.UTF_8); + // validate as standard SPARQL 1.1 before sending; the original text is sent unmodified + UpdateFactory.create(update, target.toString(), Syntax.syntaxSPARQL_11); + + HttpException.check(target, getClient().patch(target, update)).close(); + + return 0; + } + +} diff --git a/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/Post.java b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/Post.java new file mode 100644 index 0000000000..8cc1aa393d --- /dev/null +++ b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/Post.java @@ -0,0 +1,54 @@ +/* + * 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 jakarta.ws.rs.client.Entity; +import jakarta.ws.rs.core.MediaType; +import java.net.URI; +import org.apache.jena.rdf.model.Model; +import picocli.CommandLine.Command; +import picocli.CommandLine.Option; +import picocli.CommandLine.Parameters; + +/** + * Creates an RDF document from standard input. Mirrors bin/post.sh. + * + * @author Martynas Jusevičius {@literal } + */ +@Command(name = "post", description = "Creates an RDF document from stdin.") +public class Post extends BaseCommand +{ + + @Option(names = {"-t", "--content-type"}, required = true, paramLabel = "MEDIA_TYPE", description = "Media type of the RDF body (e.g. text/turtle)") + private String contentType; + + @Parameters(paramLabel = "TARGET_URI", description = "URI of the document") + private URI target; + + @Override + public Integer call() throws Exception + { + Model model = readModel(contentType, target, System.in); + HttpException.check(target, getClient().post(target, Entity.entity(model, MediaType.valueOf(contentType)), ACCEPT_TURTLE)).close(); + print(target); + + return 0; + } + +} diff --git a/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/Put.java b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/Put.java new file mode 100644 index 0000000000..d30706740e --- /dev/null +++ b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/Put.java @@ -0,0 +1,54 @@ +/* + * 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 jakarta.ws.rs.client.Entity; +import jakarta.ws.rs.core.MediaType; +import java.net.URI; +import org.apache.jena.rdf.model.Model; +import picocli.CommandLine.Command; +import picocli.CommandLine.Option; +import picocli.CommandLine.Parameters; + +/** + * Creates or updates an RDF document from standard input. Mirrors bin/put.sh. + * + * @author Martynas Jusevičius {@literal } + */ +@Command(name = "put", description = "Creates or updates an RDF document from stdin.") +public class Put extends BaseCommand +{ + + @Option(names = {"-t", "--content-type"}, required = true, paramLabel = "MEDIA_TYPE", description = "Media type of the RDF body (e.g. text/turtle)") + private String contentType; + + @Parameters(paramLabel = "TARGET_URI", description = "URI of the document") + private URI target; + + @Override + public Integer call() throws Exception + { + Model model = readModel(contentType, target, System.in); + HttpException.check(target, getClient().put(target, Entity.entity(model, MediaType.valueOf(contentType)), ACCEPT_TURTLE)).close(); + print(target); + + return 0; + } + +} 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 new file mode 100644 index 0000000000..5a3ffc1498 --- /dev/null +++ b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/admin/AddOntologyImport.java @@ -0,0 +1,50 @@ +/* + * 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.BaseCommand; +import com.atomgraph.linkeddatahub.cli.http.HttpException; +import com.atomgraph.linkeddatahub.cli.sparql.Updates; +import java.net.URI; +import picocli.CommandLine.Command; +import picocli.CommandLine.Option; +import picocli.CommandLine.Parameters; + +/** + * Adds an owl:imports statement to an ontology. Mirrors bin/admin/add-ontology-import.sh. + * + * @author Martynas Jusevičius {@literal } + */ +@Command(name = "add-ontology-import", description = "Adds an owl:imports statement to an ontology.") +public class AddOntologyImport extends BaseCommand +{ + + @Option(names = "--import", required = true, paramLabel = "IMPORT_URI", description = "URI of the imported ontology") + private URI importURI; + + @Parameters(paramLabel = "ONTOLOGY_DOC_URI", description = "URI of the ontology document") + private URI target; + + @Override + public Integer call() throws Exception + { + HttpException.check(target, getClient().patch(target, Updates.insertOntologyImport(target, importURI))).close(); + + return 0; + } + +} 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 new file mode 100644 index 0000000000..f2be93bdf6 --- /dev/null +++ b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/admin/Admin.java @@ -0,0 +1,48 @@ +/* + * 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.command.admin.acl.Acl; +import com.atomgraph.linkeddatahub.cli.command.admin.ontologies.Ontologies; +import com.atomgraph.linkeddatahub.cli.command.admin.packages.Packages; +import java.util.concurrent.Callable; +import picocli.CommandLine.Command; +import picocli.CommandLine.Model.CommandSpec; +import picocli.CommandLine.ParameterException; +import picocli.CommandLine.Spec; + +/** + * Administrative command group. + * + * @author Martynas Jusevičius {@literal } + */ +@Command(name = "admin", + description = "Administrative commands.", + subcommands = { Ontologies.class, Acl.class, Packages.class, ClearOntology.class, AddOntologyImport.class }) +public class Admin implements Callable +{ + + @Spec + private CommandSpec spec; + + @Override + public Integer call() + { + throw new ParameterException(spec.commandLine(), "Missing required subcommand"); + } + +} 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 new file mode 100644 index 0000000000..642451bb3e --- /dev/null +++ b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/admin/ClearOntology.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.admin; + +import com.atomgraph.linkeddatahub.cli.BaseCommand; +import com.atomgraph.linkeddatahub.cli.http.HttpException; +import com.atomgraph.linkeddatahub.cli.mixin.BaseMixin; +import jakarta.ws.rs.core.Form; +import java.net.URI; +import picocli.CommandLine.Command; +import picocli.CommandLine.Mixin; +import picocli.CommandLine.Option; + +/** + * Clears an ontology from memory so it gets reloaded. Mirrors bin/admin/clear-ontology.sh. + * The base URI is the base of the admin application. + * + * @author Martynas Jusevičius {@literal } + */ +@Command(name = "clear-ontology", description = "Clears an ontology from memory and reloads it.") +public class ClearOntology extends BaseCommand +{ + + @Mixin + private BaseMixin baseMixin; + + @Option(names = "--ontology", required = true, paramLabel = "ONTOLOGY_URI", description = "URI of the ontology") + private URI ontology; + + @Override + public Integer call() throws Exception + { + URI base = baseMixin.require(getSpec()); + URI target = URI.create(base + "clear"); + + printBody(HttpException.check(target, getClient().postForm(target, new Form("uri", ontology.toString()), ACCEPT_TURTLE))); + + return 0; + } + +} 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/admin/acl/Acl.java new file mode 100644 index 0000000000..c44ef168dd --- /dev/null +++ b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/admin/acl/Acl.java @@ -0,0 +1,45 @@ +/* + * 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.acl; + +import java.util.concurrent.Callable; +import picocli.CommandLine.Command; +import picocli.CommandLine.Model.CommandSpec; +import picocli.CommandLine.ParameterException; +import picocli.CommandLine.Spec; + +/** + * Access control 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 implements Callable +{ + + @Spec + private CommandSpec spec; + + @Override + public Integer call() + { + throw new ParameterException(spec.commandLine(), "Missing required subcommand"); + } + +} 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 new file mode 100644 index 0000000000..73abb82fff --- /dev/null +++ b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/admin/acl/AddAgentToGroup.java @@ -0,0 +1,50 @@ +/* + * 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.acl; + +import com.atomgraph.linkeddatahub.cli.BaseCommand; +import com.atomgraph.linkeddatahub.cli.http.HttpException; +import com.atomgraph.linkeddatahub.cli.sparql.Updates; +import java.net.URI; +import picocli.CommandLine.Command; +import picocli.CommandLine.Option; +import picocli.CommandLine.Parameters; + +/** + * Adds an agent to a group. Mirrors bin/admin/acl/add-agent-to-group.sh. + * + * @author Martynas Jusevičius {@literal } + */ +@Command(name = "add-agent-to-group", description = "Adds an agent to a group.") +public class AddAgentToGroup extends BaseCommand +{ + + @Option(names = "--agent", required = true, paramLabel = "AGENT_URI", description = "URI of the agent") + private URI agent; + + @Parameters(paramLabel = "GROUP_DOC_URI", description = "URI of the group document") + private URI target; + + @Override + public Integer call() throws Exception + { + HttpException.check(target, getClient().patch(target, Updates.insertGroupMember(target, agent))).close(); + + return 0; + } + +} 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 new file mode 100644 index 0000000000..2c43b8ba46 --- /dev/null +++ b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/admin/acl/CreateAuthorization.java @@ -0,0 +1,163 @@ +/* + * 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.acl; + +import com.atomgraph.linkeddatahub.cli.BaseCommand; +import com.atomgraph.linkeddatahub.cli.mixin.BaseMixin; +import com.atomgraph.linkeddatahub.cli.util.Slugs; +import com.atomgraph.linkeddatahub.cli.util.URIRewriter; +import com.atomgraph.linkeddatahub.cli.vocab.ACL; +import com.atomgraph.linkeddatahub.cli.vocab.DH; +import java.net.URI; +import java.util.ArrayList; +import java.util.List; +import org.apache.jena.rdf.model.Model; +import org.apache.jena.rdf.model.ModelFactory; +import org.apache.jena.rdf.model.Property; +import org.apache.jena.rdf.model.Resource; +import org.apache.jena.sparql.vocabulary.FOAF; +import org.apache.jena.vocabulary.DCTerms; +import org.apache.jena.vocabulary.RDF; +import org.apache.jena.vocabulary.RDFS; +import picocli.CommandLine.Command; +import picocli.CommandLine.Mixin; +import picocli.CommandLine.Option; +import picocli.CommandLine.ParameterException; + +/** + * Creates an ACL authorization. Mirrors bin/admin/acl/create-authorization.sh. + * + * @author Martynas Jusevičius {@literal } + */ +@Command(name = "create-authorization", description = "Creates an ACL authorization.") +public class CreateAuthorization extends BaseCommand +{ + + @Mixin + private BaseMixin baseMixin; + + @Option(names = "--label", required = true, paramLabel = "LABEL", description = "Label of the authorization") + private String label; + + @Option(names = "--comment", paramLabel = "COMMENT", description = "Comment of the authorization (optional)") + private String comment; + + @Option(names = "--slug", paramLabel = "STRING", description = "String that will be used as URI path segment (optional)") + private String slug; + + @Option(names = "--uri", paramLabel = "URI", description = "URI of the authorization (optional, blank node if not set)") + private String uri; + + @Option(names = "--agent", paramLabel = "AGENT_URI", description = "URI of an authorized agent (repeatable)") + private List agents = new ArrayList<>(); + + @Option(names = "--agent-class", paramLabel = "AGENT_CLASS_URI", description = "URI of an authorized agent class (repeatable)") + private List agentClasses = new ArrayList<>(); + + @Option(names = "--agent-group", paramLabel = "AGENT_GROUP_URI", description = "URI of an authorized agent group (repeatable)") + private List agentGroups = new ArrayList<>(); + + @Option(names = "--to", paramLabel = "TO_URI", description = "URI of an accessed document (repeatable)") + private List to = new ArrayList<>(); + + @Option(names = "--to-all-in", paramLabel = "CLASS_URI", description = "URI of an accessed document class (repeatable)") + private List toAllIn = new ArrayList<>(); + + @Option(names = "--append", description = "Grant acl:Append mode") + private boolean append; + + @Option(names = "--control", description = "Grant acl:Control mode") + private boolean control; + + @Option(names = "--read", description = "Grant acl:Read mode") + private boolean read; + + @Option(names = "--write", description = "Grant acl:Write mode") + private boolean write; + + @Override + public Integer call() throws Exception + { + URI base = baseMixin.require(getSpec()); + if (agents.isEmpty() && agentClasses.isEmpty() && agentGroups.isEmpty()) + throw new ParameterException(getSpec().commandLine(), "At least one of '--agent', '--agent-class', '--agent-group' is required"); + if (to.isEmpty() && toAllIn.isEmpty()) + throw new ParameterException(getSpec().commandLine(), "At least one of '--to', '--to-all-in' is required"); + if (!append && !control && !read && !write) + throw new ParameterException(getSpec().commandLine(), "At least one of '--append', '--control', '--read', '--write' is required"); + + URI doc = URIRewriter.childURI(URI.create(base + "acl/authorizations/"), slug != null ? slug : Slugs.defaultSlug()); + + List modes = new ArrayList<>(); + if (append) modes.add(ACL.Append); + if (control) modes.add(ACL.Control); + if (read) modes.add(ACL.Read); + if (write) modes.add(ACL.Write); + + put(getClient(), doc, buildModel(doc, uri, label, comment, agents, agentClasses, agentGroups, to, toAllIn, modes)); + print(doc); + + return 0; + } + + /** + * Builds the authorization document model. + * + * @param doc document URI + * @param uri authorization URI (optional, blank node if null) + * @param label authorization label + * @param comment authorization comment (optional) + * @param agents authorized agent URIs + * @param agentClasses authorized agent class URIs + * @param agentGroups authorized agent group URIs + * @param to accessed document URIs + * @param toAllIn accessed document class URIs + * @param modes granted access modes + * @return document model + */ + public static Model buildModel(URI doc, String uri, String label, String comment, + List agents, List agentClasses, List agentGroups, + List to, List toAllIn, List modes) + { + Model model = ModelFactory.createDefaultModel(); + + Resource auth = createSubject(model, doc, uri). + addProperty(RDF.type, ACL.Authorization). + addProperty(RDFS.label, label); + if (comment != null) auth.addProperty(RDFS.comment, comment); + + model.createResource(doc.toString()). + addProperty(RDF.type, DH.Item). + addProperty(FOAF.primaryTopic, auth). + addProperty(DCTerms.title, label); + + addResourceValues(auth, ACL.agent, agents); + addResourceValues(auth, ACL.agentClass, agentClasses); + addResourceValues(auth, ACL.agentGroup, agentGroups); + addResourceValues(auth, ACL.accessTo, to); + addResourceValues(auth, ACL.accessToClass, toAllIn); + modes.forEach(mode -> auth.addProperty(ACL.mode, mode)); + + return model; + } + + static void addResourceValues(Resource subject, Property property, List values) + { + values.forEach(value -> subject.addProperty(property, subject.getModel().createResource(value.toString()))); + } + +} 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 new file mode 100644 index 0000000000..05ec6847e7 --- /dev/null +++ b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/admin/acl/CreateGroup.java @@ -0,0 +1,103 @@ +/* + * 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.acl; + +import com.atomgraph.linkeddatahub.cli.BaseCommand; +import com.atomgraph.linkeddatahub.cli.mixin.BaseMixin; +import com.atomgraph.linkeddatahub.cli.util.Slugs; +import com.atomgraph.linkeddatahub.cli.util.URIRewriter; +import com.atomgraph.linkeddatahub.cli.vocab.DH; +import java.net.URI; +import java.util.List; +import org.apache.jena.rdf.model.Model; +import org.apache.jena.rdf.model.ModelFactory; +import org.apache.jena.rdf.model.Resource; +import org.apache.jena.sparql.vocabulary.FOAF; +import org.apache.jena.vocabulary.DCTerms; +import org.apache.jena.vocabulary.RDF; +import picocli.CommandLine.Command; +import picocli.CommandLine.Mixin; +import picocli.CommandLine.Option; + +/** + * Creates an agent group. Mirrors bin/admin/acl/create-group.sh. + * + * @author Martynas Jusevičius {@literal } + */ +@Command(name = "create-group", description = "Creates an agent group.") +public class CreateGroup extends BaseCommand +{ + + @Mixin + private BaseMixin baseMixin; + + @Option(names = "--name", required = true, paramLabel = "NAME", description = "Name of the group") + private String name; + + @Option(names = "--description", paramLabel = "DESCRIPTION", description = "Description of the group (optional)") + private String description; + + @Option(names = "--slug", paramLabel = "STRING", description = "String that will be used as URI path segment (optional)") + private String slug; + + @Option(names = "--uri", paramLabel = "URI", description = "URI of the group (optional, blank node if not set)") + private String uri; + + @Option(names = "--member", required = true, paramLabel = "MEMBER_URI", description = "URI of a group member (repeatable)") + private List members; + + @Override + public Integer call() throws Exception + { + URI base = baseMixin.require(getSpec()); + URI doc = URIRewriter.childURI(URI.create(base + "acl/groups/"), slug != null ? slug : Slugs.defaultSlug()); + + put(getClient(), doc, buildModel(doc, uri, name, description, members)); + print(doc); + + return 0; + } + + /** + * Builds the group document model. + * + * @param doc document URI + * @param uri group URI (optional, blank node if null) + * @param name group name + * @param description group description (optional) + * @param members member agent URIs + * @return document model + */ + public static Model buildModel(URI doc, String uri, String name, String description, List members) + { + Model model = ModelFactory.createDefaultModel(); + + Resource group = createSubject(model, doc, uri). + addProperty(RDF.type, FOAF.Group). + addProperty(FOAF.name, name); + if (description != null) group.addProperty(DCTerms.description, description); + members.forEach(member -> group.addProperty(FOAF.member, model.createResource(member.toString()))); + + model.createResource(doc.toString()). + addProperty(RDF.type, DH.Item). + addProperty(FOAF.primaryTopic, group). + addProperty(DCTerms.title, name); + + return model; + } + +} diff --git a/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/admin/acl/MakePublic.java b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/admin/acl/MakePublic.java new file mode 100644 index 0000000000..a041e3521f --- /dev/null +++ b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/admin/acl/MakePublic.java @@ -0,0 +1,59 @@ +/* + * 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.acl; + +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 com.atomgraph.linkeddatahub.cli.util.URIRewriter; +import java.net.URI; +import picocli.CommandLine.Command; +import picocli.CommandLine.Mixin; + +/** + * Makes all end-user application documents publicly readable. Mirrors bin/admin/acl/make-public.sh. + * + * @author Martynas Jusevičius {@literal } + */ +@Command(name = "make-public", description = "Makes all end-user application documents publicly readable.") +public class MakePublic extends BaseCommand +{ + + @Mixin + private BaseMixin baseMixin; + + @Override + public Integer call() throws Exception + { + URI base = baseMixin.require(getSpec()); + URI adminBase = URIRewriter.adminBase(base); + URI target = URI.create(adminBase + "acl/authorizations/public/"); + + HttpException.check(target, getClient().patch(target, Updates.makePublic(base, adminBase))).close(); + + return 0; + } + + @Override + protected URI getEffectiveProxy() + { + // the request targets the admin app, so the proxy origin gets the admin subdomain too + return getProxyMixin().getProxy() != null ? URIRewriter.adminBase(getProxyMixin().getProxy()) : null; + } + +} 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 new file mode 100644 index 0000000000..64c097ce2a --- /dev/null +++ b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/admin/ontologies/AddClass.java @@ -0,0 +1,107 @@ +/* + * 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.ontologies; + +import com.atomgraph.linkeddatahub.cli.BaseCommand; +import com.atomgraph.linkeddatahub.cli.mixin.BaseMixin; +import com.atomgraph.linkeddatahub.cli.vocab.SPIN; +import java.net.URI; +import java.util.ArrayList; +import java.util.List; +import org.apache.jena.rdf.model.Model; +import org.apache.jena.rdf.model.ModelFactory; +import org.apache.jena.rdf.model.Resource; +import org.apache.jena.vocabulary.OWL; +import org.apache.jena.vocabulary.RDF; +import org.apache.jena.vocabulary.RDFS; +import picocli.CommandLine.Command; +import picocli.CommandLine.Mixin; +import picocli.CommandLine.Option; +import picocli.CommandLine.Parameters; + +/** + * Adds a class to an ontology. Mirrors bin/admin/ontologies/add-class.sh. + * + * @author Martynas Jusevičius {@literal } + */ +@Command(name = "add-class", description = "Adds a class to an ontology.") +public class AddClass extends BaseCommand +{ + + @Mixin + private BaseMixin baseMixin; + + @Option(names = "--label", required = true, paramLabel = "LABEL", description = "Label of the class") + private String label; + + @Option(names = "--comment", paramLabel = "COMMENT", description = "Comment of the class (optional)") + private String comment; + + @Option(names = "--uri", paramLabel = "URI", description = "URI of the class (optional, blank node if not set)") + private String uri; + + @Option(names = "--constructor", paramLabel = "CONSTRUCT_URI", description = "URI of the constructor query (optional)") + private URI constructor; + + @Option(names = "--constraint", paramLabel = "CONSTRAINT_URI", description = "URI of the constraint (optional)") + private URI constraint; + + @Option(names = "--sub-class-of", paramLabel = "SUPER_CLASS_URI", description = "URI of a superclass (optional, repeatable)") + private List superClasses = new ArrayList<>(); + + @Parameters(paramLabel = "TARGET_URI", description = "URI of the ontology document") + private URI target; + + @Override + public Integer call() throws Exception + { + baseMixin.require(getSpec()); // required by the script interface + + post(getClient(), target, buildModel(target, uri, label, comment, constructor, constraint, superClasses)); + print(target); + + return 0; + } + + /** + * Builds the class description. + * + * @param target target document URI + * @param uri class URI (optional) + * @param label class label + * @param comment class comment (optional) + * @param constructor constructor query URI (optional) + * @param constraint constraint URI (optional) + * @param superClasses superclass URIs + * @return class model + */ + public static Model buildModel(URI target, String uri, String label, String comment, URI constructor, URI constraint, List superClasses) + { + Model model = ModelFactory.createDefaultModel(); + + Resource cls = createSubject(model, target, uri). + addProperty(RDF.type, OWL.Class). + addProperty(RDFS.label, label); + if (comment != null) cls.addProperty(RDFS.comment, comment); + if (constructor != null) cls.addProperty(SPIN.constructor, model.createResource(constructor.toString())); + if (constraint != null) cls.addProperty(SPIN.constraint, model.createResource(constraint.toString())); + superClasses.forEach(superClass -> cls.addProperty(RDFS.subClassOf, model.createResource(superClass.toString()))); + + return model; + } + +} 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 new file mode 100644 index 0000000000..d5f222b642 --- /dev/null +++ b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/admin/ontologies/AddConstructor.java @@ -0,0 +1,101 @@ +/* + * 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.ontologies; + +import com.atomgraph.linkeddatahub.cli.BaseCommand; +import com.atomgraph.linkeddatahub.cli.mixin.BaseMixin; +import com.atomgraph.linkeddatahub.cli.vocab.LDH; +import com.atomgraph.linkeddatahub.cli.vocab.SP; +import java.net.URI; +import java.nio.file.Files; +import java.nio.file.Path; +import org.apache.jena.rdf.model.Model; +import org.apache.jena.rdf.model.ModelFactory; +import org.apache.jena.rdf.model.Resource; +import org.apache.jena.vocabulary.RDF; +import org.apache.jena.vocabulary.RDFS; +import picocli.CommandLine.Command; +import picocli.CommandLine.Mixin; +import picocli.CommandLine.Option; +import picocli.CommandLine.Parameters; + +/** + * Adds a SPARQL CONSTRUCT constructor query to an ontology. Mirrors bin/admin/ontologies/add-constructor.sh. + * + * @author Martynas Jusevičius {@literal } + */ +@Command(name = "add-constructor", description = "Adds a CONSTRUCT query to an ontology.") +public class AddConstructor extends BaseCommand +{ + + @Mixin + private BaseMixin baseMixin; + + @Option(names = "--label", required = true, paramLabel = "LABEL", description = "Label of the query") + private String label; + + @Option(names = "--comment", paramLabel = "COMMENT", description = "Comment of the query (optional)") + private String comment; + + @Option(names = "--uri", paramLabel = "URI", description = "URI of the query (optional, blank node if not set)") + private String uri; + + @Option(names = "--query-file", required = true, paramLabel = "ABS_PATH", description = "Path to the file with the query string") + private Path queryFile; + + @Parameters(paramLabel = "TARGET_URI", description = "URI of the ontology document") + private URI target; + + @Override + public Integer call() throws Exception + { + baseMixin.require(getSpec()); // required by the script interface + + post(getClient(), target, buildModel(target, uri, SP.Construct, label, Files.readString(queryFile), null, comment)); + print(target); + + return 0; + } + + /** + * Builds an ontology SPIN query description (labeled with rdfs:label, + * unlike the dct:title-based document queries). + * + * @param target target document URI + * @param uri query URI (optional) + * @param queryType SPIN query class (sp:Construct or sp:Select) + * @param label query label + * @param queryText query string + * @param service SPARQL service URI (optional) + * @param comment query comment (optional) + * @return query model + */ + public static Model buildModel(URI target, String uri, Resource queryType, String label, String queryText, URI service, String comment) + { + Model model = ModelFactory.createDefaultModel(); + + Resource query = createSubject(model, target, uri). + addProperty(RDF.type, queryType). + addProperty(RDFS.label, label). + addProperty(SP.text, queryText); + if (comment != null) query.addProperty(RDFS.comment, comment); + if (service != null) query.addProperty(LDH.service, model.createResource(service.toString())); + + return model; + } + +} 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 new file mode 100644 index 0000000000..74ff2db865 --- /dev/null +++ b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/admin/ontologies/AddPropertyConstraint.java @@ -0,0 +1,95 @@ +/* + * 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.ontologies; + +import com.atomgraph.linkeddatahub.cli.BaseCommand; +import com.atomgraph.linkeddatahub.cli.mixin.BaseMixin; +import com.atomgraph.linkeddatahub.cli.vocab.LDH; +import com.atomgraph.linkeddatahub.cli.vocab.SP; +import java.net.URI; +import org.apache.jena.rdf.model.Model; +import org.apache.jena.rdf.model.ModelFactory; +import org.apache.jena.rdf.model.Resource; +import org.apache.jena.vocabulary.RDF; +import org.apache.jena.vocabulary.RDFS; +import picocli.CommandLine.Command; +import picocli.CommandLine.Mixin; +import picocli.CommandLine.Option; +import picocli.CommandLine.Parameters; + +/** + * Adds a constraint that makes a property required. Mirrors bin/admin/ontologies/add-property-constraint.sh. + * + * @author Martynas Jusevičius {@literal } + */ +@Command(name = "add-property-constraint", description = "Adds a constraint that makes a property required.") +public class AddPropertyConstraint extends BaseCommand +{ + + @Mixin + private BaseMixin baseMixin; + + @Option(names = "--label", required = true, paramLabel = "LABEL", description = "Label of the constraint") + private String label; + + @Option(names = "--comment", paramLabel = "COMMENT", description = "Comment of the constraint (optional)") + private String comment; + + @Option(names = "--uri", paramLabel = "URI", description = "URI of the constraint (optional, blank node if not set)") + private String uri; + + @Option(names = "--property", required = true, paramLabel = "PROPERTY_URI", description = "URI of the required property") + private URI property; + + @Parameters(paramLabel = "TARGET_URI", description = "URI of the ontology document") + private URI target; + + @Override + public Integer call() throws Exception + { + baseMixin.require(getSpec()); // required by the script interface + + post(getClient(), target, buildModel(target, uri, label, property, comment)); + print(target); + + return 0; + } + + /** + * Builds the constraint description. + * + * @param target target document URI + * @param uri constraint URI (optional) + * @param label constraint label + * @param property required property URI + * @param comment constraint comment (optional) + * @return constraint model + */ + public static Model buildModel(URI target, String uri, String label, URI property, String comment) + { + Model model = ModelFactory.createDefaultModel(); + + Resource constraint = createSubject(model, target, uri). + addProperty(RDF.type, LDH.MissingPropertyValue). + addProperty(RDFS.label, label). + addProperty(SP.arg1, model.createResource(property.toString())); + if (comment != null) constraint.addProperty(RDFS.comment, comment); + + return model; + } + +} 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 new file mode 100644 index 0000000000..c2b6ee6cea --- /dev/null +++ b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/admin/ontologies/AddRestriction.java @@ -0,0 +1,104 @@ +/* + * 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.ontologies; + +import com.atomgraph.linkeddatahub.cli.BaseCommand; +import com.atomgraph.linkeddatahub.cli.mixin.BaseMixin; +import java.net.URI; +import org.apache.jena.rdf.model.Model; +import org.apache.jena.rdf.model.ModelFactory; +import org.apache.jena.rdf.model.Resource; +import org.apache.jena.vocabulary.OWL; +import org.apache.jena.vocabulary.RDF; +import org.apache.jena.vocabulary.RDFS; +import picocli.CommandLine.Command; +import picocli.CommandLine.Mixin; +import picocli.CommandLine.Option; +import picocli.CommandLine.Parameters; + +/** + * Adds an OWL restriction to an ontology. Mirrors bin/admin/ontologies/add-restriction.sh. + * + * @author Martynas Jusevičius {@literal } + */ +@Command(name = "add-restriction", description = "Adds an OWL restriction to an ontology.") +public class AddRestriction extends BaseCommand +{ + + @Mixin + private BaseMixin baseMixin; + + @Option(names = "--label", required = true, paramLabel = "LABEL", description = "Label of the restriction") + private String label; + + @Option(names = "--comment", paramLabel = "COMMENT", description = "Comment of the restriction (optional)") + private String comment; + + @Option(names = "--uri", paramLabel = "URI", description = "URI of the restriction (optional, blank node if not set)") + private String uri; + + @Option(names = "--on-property", paramLabel = "PROPERTY_URI", description = "URI of the restricted property (optional)") + private URI onProperty; + + @Option(names = "--all-values-from", paramLabel = "URI", description = "URI of the value class (optional)") + private URI allValuesFrom; + + @Option(names = "--has-value", paramLabel = "URI", description = "URI of the value resource (optional)") + private URI hasValue; + + @Parameters(paramLabel = "TARGET_URI", description = "URI of the ontology document") + private URI target; + + @Override + public Integer call() throws Exception + { + baseMixin.require(getSpec()); // required by the script interface + + post(getClient(), target, buildModel(target, uri, label, comment, onProperty, allValuesFrom, hasValue)); + print(target); + + return 0; + } + + /** + * Builds the restriction description. + * + * @param target target document URI + * @param uri restriction URI (optional) + * @param label restriction label + * @param comment restriction comment (optional) + * @param onProperty restricted property URI (optional) + * @param allValuesFrom value class URI (optional) + * @param hasValue value resource URI (optional) + * @return restriction model + */ + public static Model buildModel(URI target, String uri, String label, String comment, URI onProperty, URI allValuesFrom, URI hasValue) + { + Model model = ModelFactory.createDefaultModel(); + + Resource restriction = createSubject(model, target, uri). + addProperty(RDF.type, OWL.Restriction). + addProperty(RDFS.label, label); + if (comment != null) restriction.addProperty(RDFS.comment, comment); + if (onProperty != null) restriction.addProperty(OWL.onProperty, model.createResource(onProperty.toString())); + if (allValuesFrom != null) restriction.addProperty(OWL.allValuesFrom, model.createResource(allValuesFrom.toString())); + if (hasValue != null) restriction.addProperty(OWL.hasValue, model.createResource(hasValue.toString())); + + return model; + } + +} 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 new file mode 100644 index 0000000000..0938ffc9e4 --- /dev/null +++ b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/admin/ontologies/AddSelect.java @@ -0,0 +1,71 @@ +/* + * 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.ontologies; + +import com.atomgraph.linkeddatahub.cli.BaseCommand; +import com.atomgraph.linkeddatahub.cli.mixin.BaseMixin; +import com.atomgraph.linkeddatahub.cli.vocab.SP; +import java.net.URI; +import java.nio.file.Files; +import java.nio.file.Path; +import picocli.CommandLine.Command; +import picocli.CommandLine.Mixin; +import picocli.CommandLine.Option; +import picocli.CommandLine.Parameters; + +/** + * Adds a SPARQL SELECT query to an ontology. Mirrors bin/admin/ontologies/add-select.sh. + * + * @author Martynas Jusevičius {@literal } + */ +@Command(name = "add-select", description = "Adds a SELECT query to an ontology.") +public class AddSelect extends BaseCommand +{ + + @Mixin + private BaseMixin baseMixin; + + @Option(names = "--label", required = true, paramLabel = "LABEL", description = "Label of the query") + private String label; + + @Option(names = "--comment", paramLabel = "COMMENT", description = "Comment of the query (optional)") + private String comment; + + @Option(names = "--uri", paramLabel = "URI", description = "URI of the query (optional, blank node if not set)") + private String uri; + + @Option(names = "--query-file", required = true, paramLabel = "ABS_PATH", description = "Path to the file with the query string") + private Path queryFile; + + @Option(names = "--service", paramLabel = "SERVICE_URI", description = "URI of the SPARQL service (optional)") + private URI service; + + @Parameters(paramLabel = "TARGET_URI", description = "URI of the ontology document") + private URI target; + + @Override + public Integer call() throws Exception + { + baseMixin.require(getSpec()); // required by the script interface + + post(getClient(), target, AddConstructor.buildModel(target, uri, SP.Select, label, Files.readString(queryFile), service, comment)); + print(target); + + return 0; + } + +} 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 new file mode 100644 index 0000000000..96cface9ff --- /dev/null +++ b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/admin/ontologies/CreateOntology.java @@ -0,0 +1,99 @@ +/* + * 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.ontologies; + +import com.atomgraph.linkeddatahub.cli.BaseCommand; +import com.atomgraph.linkeddatahub.cli.mixin.BaseMixin; +import com.atomgraph.linkeddatahub.cli.util.Slugs; +import com.atomgraph.linkeddatahub.cli.util.URIRewriter; +import com.atomgraph.linkeddatahub.cli.vocab.DH; +import java.net.URI; +import org.apache.jena.rdf.model.Model; +import org.apache.jena.rdf.model.ModelFactory; +import org.apache.jena.rdf.model.Resource; +import org.apache.jena.sparql.vocabulary.FOAF; +import org.apache.jena.vocabulary.DCTerms; +import org.apache.jena.vocabulary.OWL; +import org.apache.jena.vocabulary.RDF; +import org.apache.jena.vocabulary.RDFS; +import picocli.CommandLine.Command; +import picocli.CommandLine.Mixin; +import picocli.CommandLine.Option; + +/** + * Creates a new ontology document. Mirrors bin/admin/ontologies/create-ontology.sh. + * + * @author Martynas Jusevičius {@literal } + */ +@Command(name = "create-ontology", description = "Creates a new ontology.") +public class CreateOntology extends BaseCommand +{ + + @Mixin + private BaseMixin baseMixin; + + @Option(names = "--label", required = true, paramLabel = "LABEL", description = "Label of the ontology") + private String label; + + @Option(names = "--comment", paramLabel = "COMMENT", description = "Comment of the ontology (optional)") + private String comment; + + @Option(names = "--slug", paramLabel = "STRING", description = "String that will be used as URI path segment (optional)") + private String slug; + + @Option(names = "--uri", paramLabel = "URI", description = "URI of the ontology (optional, blank node if not set)") + private String uri; + + @Override + public Integer call() throws Exception + { + URI base = baseMixin.require(getSpec()); + URI doc = URIRewriter.childURI(URI.create(base + "ontologies/"), slug != null ? slug : Slugs.defaultSlug()); + + put(getClient(), doc, buildModel(doc, uri, label, comment)); + print(doc); + + return 0; + } + + /** + * Builds the ontology document model. + * + * @param doc document URI + * @param uri ontology URI (optional, blank node if null) + * @param label ontology label + * @param comment ontology comment (optional) + * @return document model + */ + public static Model buildModel(URI doc, String uri, String label, String comment) + { + Model model = ModelFactory.createDefaultModel(); + + Resource ontology = createSubject(model, doc, uri). + addProperty(RDF.type, OWL.Ontology). + addProperty(RDFS.label, label); + if (comment != null) ontology.addProperty(RDFS.comment, comment); + + model.createResource(doc.toString()). + addProperty(RDF.type, DH.Item). + addProperty(FOAF.primaryTopic, ontology). + addProperty(DCTerms.title, label); + + return model; + } + +} 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 new file mode 100644 index 0000000000..a91607456b --- /dev/null +++ b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/admin/ontologies/ImportOntology.java @@ -0,0 +1,83 @@ +/* + * 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.ontologies; + +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.SD; +import com.atomgraph.linkeddatahub.cli.vocab.SPIN; +import jakarta.ws.rs.client.Entity; +import java.net.URI; +import org.apache.jena.rdf.model.Model; +import org.apache.jena.rdf.model.ModelFactory; +import org.apache.jena.vocabulary.DCTerms; +import picocli.CommandLine.Command; +import picocli.CommandLine.Mixin; +import picocli.CommandLine.Option; + +/** + * Imports an external ontology into a named graph via the transform endpoint. + * Mirrors bin/admin/ontologies/import-ontology.sh. + * + * @author Martynas Jusevičius {@literal } + */ +@Command(name = "import-ontology", description = "Imports an external ontology into a named graph.") +public class ImportOntology extends BaseCommand +{ + + @Mixin + private BaseMixin baseMixin; + + @Option(names = "--source", required = true, paramLabel = "SOURCE_URI", description = "URI of the imported ontology") + private URI source; + + @Option(names = "--graph", required = true, paramLabel = "GRAPH_URI", description = "URI of the named graph the ontology is imported into") + private URI graph; + + @Override + public Integer call() throws Exception + { + URI base = baseMixin.require(getSpec()); + URI target = URI.create(base + "transform"); + + printBody(HttpException.check(target, getClient().post(target, Entity.entity(buildModel(base, source, graph), TEXT_TURTLE_TYPE), ACCEPT_TURTLE))); + + return 0; + } + + /** + * Builds the transform argument description. + * + * @param base admin application base URI + * @param source imported ontology URI + * @param graph target named graph URI + * @return argument model + */ + public static Model buildModel(URI base, URI source, URI graph) + { + Model model = ModelFactory.createDefaultModel(); + + model.createResource(). + addProperty(DCTerms.source, model.createResource(source.toString())). + addProperty(SD.name, model.createResource(graph.toString())). + addProperty(SPIN.query, model.createResource(base + "queries/construct-constructors/#this")); + + return model; + } + +} 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/admin/ontologies/Ontologies.java new file mode 100644 index 0000000000..e7fdbdec41 --- /dev/null +++ b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/admin/ontologies/Ontologies.java @@ -0,0 +1,46 @@ +/* + * 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.ontologies; + +import java.util.concurrent.Callable; +import picocli.CommandLine.Command; +import picocli.CommandLine.Model.CommandSpec; +import picocli.CommandLine.ParameterException; +import picocli.CommandLine.Spec; + +/** + * Ontology management command group. + * + * @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 implements Callable +{ + + @Spec + private CommandSpec spec; + + @Override + public Integer call() + { + throw new ParameterException(spec.commandLine(), "Missing required subcommand"); + } + +} diff --git a/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/admin/packages/InstallPackage.java b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/admin/packages/InstallPackage.java new file mode 100644 index 0000000000..f2167060d9 --- /dev/null +++ b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/admin/packages/InstallPackage.java @@ -0,0 +1,60 @@ +/* + * 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.packages; + +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.util.URIRewriter; +import jakarta.ws.rs.core.Form; +import java.net.URI; +import picocli.CommandLine.Command; +import picocli.CommandLine.Mixin; +import picocli.CommandLine.Option; + +/** + * Installs a LinkedDataHub package. Mirrors bin/admin/packages/install-package.sh. + * + * @author Martynas Jusevičius {@literal } + */ +@Command(name = "install-package", description = "Installs a package.") +public class InstallPackage extends BaseCommand +{ + + @Mixin + private BaseMixin baseMixin; + + @Option(names = "--package", required = true, paramLabel = "PACKAGE_URI", description = "URI of the package") + private URI packageURI; + + @Override + public Integer call() throws Exception + { + URI target = URI.create(URIRewriter.adminBase(baseMixin.require(getSpec())) + "packages/install"); + HttpException.check(target, getClient().postForm(target, new Form("package-uri", packageURI.toString()), ACCEPT_TURTLE)).close(); + + return 0; + } + + @Override + protected URI getEffectiveProxy() + { + // the request targets the admin app, so the proxy origin gets the admin subdomain too + return getProxyMixin().getProxy() != null ? URIRewriter.adminBase(getProxyMixin().getProxy()) : null; + } + +} diff --git a/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/admin/packages/Packages.java b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/admin/packages/Packages.java new file mode 100644 index 0000000000..79191eb131 --- /dev/null +++ b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/admin/packages/Packages.java @@ -0,0 +1,45 @@ +/* + * 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.packages; + +import java.util.concurrent.Callable; +import picocli.CommandLine.Command; +import picocli.CommandLine.Model.CommandSpec; +import picocli.CommandLine.ParameterException; +import picocli.CommandLine.Spec; + +/** + * Package management command group. + * + * @author Martynas Jusevičius {@literal } + */ +@Command(name = "packages", + description = "Package management commands.", + subcommands = { InstallPackage.class, UninstallPackage.class }) +public class Packages implements Callable +{ + + @Spec + private CommandSpec spec; + + @Override + public Integer call() + { + throw new ParameterException(spec.commandLine(), "Missing required subcommand"); + } + +} diff --git a/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/admin/packages/UninstallPackage.java b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/admin/packages/UninstallPackage.java new file mode 100644 index 0000000000..78f463673d --- /dev/null +++ b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/admin/packages/UninstallPackage.java @@ -0,0 +1,60 @@ +/* + * 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.packages; + +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.util.URIRewriter; +import jakarta.ws.rs.core.Form; +import java.net.URI; +import picocli.CommandLine.Command; +import picocli.CommandLine.Mixin; +import picocli.CommandLine.Option; + +/** + * Uninstalls a LinkedDataHub package. Mirrors bin/admin/packages/uninstall-package.sh. + * + * @author Martynas Jusevičius {@literal } + */ +@Command(name = "uninstall-package", description = "Uninstalls a package.") +public class UninstallPackage extends BaseCommand +{ + + @Mixin + private BaseMixin baseMixin; + + @Option(names = "--package", required = true, paramLabel = "PACKAGE_URI", description = "URI of the package") + private URI packageURI; + + @Override + public Integer call() throws Exception + { + URI target = URI.create(URIRewriter.adminBase(baseMixin.require(getSpec())) + "packages/uninstall"); + HttpException.check(target, getClient().postForm(target, new Form("package-uri", packageURI.toString()), ACCEPT_TURTLE)).close(); + + return 0; + } + + @Override + protected URI getEffectiveProxy() + { + // the request targets the admin app, so the proxy origin gets the admin subdomain too + return getProxyMixin().getProxy() != null ? URIRewriter.adminBase(getProxyMixin().getProxy()) : null; + } + +} 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 new file mode 100644 index 0000000000..b9860d23dc --- /dev/null +++ b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/content/AddObjectBlock.java @@ -0,0 +1,121 @@ +/* + * 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.content; + +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.util.SequenceNumbers; +import com.atomgraph.linkeddatahub.cli.vocab.AC; +import com.atomgraph.linkeddatahub.cli.vocab.LDH; +import jakarta.ws.rs.core.Response; +import java.net.URI; +import org.apache.jena.rdf.model.Model; +import org.apache.jena.rdf.model.ModelFactory; +import org.apache.jena.rdf.model.Property; +import org.apache.jena.rdf.model.Resource; +import org.apache.jena.rdf.model.ResourceFactory; +import org.apache.jena.vocabulary.DCTerms; +import org.apache.jena.vocabulary.RDF; +import picocli.CommandLine.Command; +import picocli.CommandLine.Mixin; +import picocli.CommandLine.Option; +import picocli.CommandLine.Parameters; + +/** + * Appends an object content block to a document. Mirrors bin/content/add-object-block.sh. + * + * @author Martynas Jusevičius {@literal } + */ +@Command(name = "add-object-block", description = "Appends an object content block to a document.") +public class AddObjectBlock extends BaseCommand +{ + + @Mixin + private BaseMixin baseMixin; // accepted for script interface parity, unused + + @Option(names = "--value", required = true, paramLabel = "RESOURCE_URI", description = "URI of the object resource") + private URI value; + + @Option(names = "--title", paramLabel = "TITLE", description = "Title of the block (optional)") + private String title; + + @Option(names = "--description", paramLabel = "DESCRIPTION", description = "Description of the block (optional)") + private String description; + + @Option(names = "--uri", paramLabel = "URI", description = "URI of the block (optional, blank node if not set)") + private String uri; + + @Option(names = "--mode", paramLabel = "MODE_URI", description = "URI of the layout mode (optional)") + private URI mode; + + @Parameters(paramLabel = "TARGET_URI", description = "URI of the document") + private URI target; + + @Override + public Integer call() throws Exception + { + post(getClient(), target, buildModel(target, nextSequenceProperty(), uri, value, title, description, mode)); + print(target); + + return 0; + } + + /** + * Fetches the target document and returns the next free rdf:_N membership property. + * + * @return membership property + */ + protected Property nextSequenceProperty() + { + Model current; + try (Response response = HttpException.check(target, getClient().get(target, ACCEPT_NTRIPLES))) + { + current = response.readEntity(Model.class); + } + + return SequenceNumbers.nextSequenceProperty(current, ResourceFactory.createResource(target.toString())); + } + + /** + * Builds the object block description. + * + * @param target target document URI + * @param seq membership property (rdf:_N) + * @param uri block URI (optional) + * @param value object resource URI + * @param title block title (optional) + * @param description block description (optional) + * @param mode layout mode URI (optional) + * @return block model + */ + public static Model buildModel(URI target, Property seq, String uri, URI value, String title, String description, URI mode) + { + Model model = ModelFactory.createDefaultModel(); + + Resource block = createSubject(model, target, uri). + addProperty(RDF.type, LDH.Object). + addProperty(RDF.value, model.createResource(value.toString())); + model.createResource(target.toString()).addProperty(seq, block); + if (title != null) block.addProperty(DCTerms.title, title); + if (description != null) block.addProperty(DCTerms.description, description); + if (mode != null) block.addProperty(AC.mode, model.createResource(mode.toString())); + + return model; + } + +} 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 new file mode 100644 index 0000000000..7995fd0c2b --- /dev/null +++ b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/content/AddXHTMLBlock.java @@ -0,0 +1,106 @@ +/* + * 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.content; + +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.util.SequenceNumbers; +import com.atomgraph.linkeddatahub.cli.vocab.LDH; +import jakarta.ws.rs.core.Response; +import java.net.URI; +import org.apache.jena.rdf.model.Model; +import org.apache.jena.rdf.model.ModelFactory; +import org.apache.jena.rdf.model.Property; +import org.apache.jena.rdf.model.Resource; +import org.apache.jena.rdf.model.ResourceFactory; +import org.apache.jena.vocabulary.DCTerms; +import org.apache.jena.vocabulary.RDF; +import picocli.CommandLine.Command; +import picocli.CommandLine.Mixin; +import picocli.CommandLine.Option; +import picocli.CommandLine.Parameters; + +/** + * Appends an XHTML content block to a document. Mirrors bin/content/add-xhtml-block.sh. + * + * @author Martynas Jusevičius {@literal } + */ +@Command(name = "add-xhtml-block", description = "Appends an XHTML content block to a document.") +public class AddXHTMLBlock extends BaseCommand +{ + + @Mixin + private BaseMixin baseMixin; // accepted for script interface parity, unused + + @Option(names = "--value", required = true, paramLabel = "XHTML", description = "XHTML content (must be well-formed XML)") + private String value; + + @Option(names = "--title", paramLabel = "TITLE", description = "Title of the block (optional)") + private String title; + + @Option(names = "--description", paramLabel = "DESCRIPTION", description = "Description of the block (optional)") + private String description; + + @Option(names = "--uri", paramLabel = "URI", description = "URI of the block (optional, blank node if not set)") + private String uri; + + @Parameters(paramLabel = "TARGET_URI", description = "URI of the document") + private URI target; + + @Override + public Integer call() throws Exception + { + Model current; + try (Response response = HttpException.check(target, getClient().get(target, ACCEPT_NTRIPLES))) + { + current = response.readEntity(Model.class); + } + Property seq = SequenceNumbers.nextSequenceProperty(current, ResourceFactory.createResource(target.toString())); + + post(getClient(), target, buildModel(target, seq, uri, value, title, description)); + print(target); + + return 0; + } + + /** + * Builds the XHTML block description. + * + * @param target target document URI + * @param seq membership property (rdf:_N) + * @param uri block URI (optional) + * @param value XHTML content + * @param title block title (optional) + * @param description block description (optional) + * @return block model + */ + public static Model buildModel(URI target, Property seq, String uri, String value, String title, String description) + { + Model model = ModelFactory.createDefaultModel(); + + Resource block = createSubject(model, target, uri). + addProperty(RDF.type, LDH.XHTML). + addProperty(RDF.value, model.createTypedLiteral(value, RDF.dtXMLLiteral)); + model.createResource(target.toString()).addProperty(seq, block); + if (title != null) block.addProperty(DCTerms.title, title); + if (description != null) block.addProperty(DCTerms.description, description); + + return model; + } + +} 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/content/Content.java new file mode 100644 index 0000000000..900db7912c --- /dev/null +++ b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/content/Content.java @@ -0,0 +1,45 @@ +/* + * 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.content; + +import java.util.concurrent.Callable; +import picocli.CommandLine.Command; +import picocli.CommandLine.Model.CommandSpec; +import picocli.CommandLine.ParameterException; +import picocli.CommandLine.Spec; + +/** + * Content block command group. + * + * @author Martynas Jusevičius {@literal } + */ +@Command(name = "content", + description = "Content block commands.", + subcommands = { AddObjectBlock.class, AddXHTMLBlock.class, RemoveBlock.class }) +public class Content implements Callable +{ + + @Spec + private CommandSpec spec; + + @Override + public Integer call() + { + throw new ParameterException(spec.commandLine(), "Missing required subcommand"); + } + +} 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 new file mode 100644 index 0000000000..dd6bfee7cd --- /dev/null +++ b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/content/RemoveBlock.java @@ -0,0 +1,50 @@ +/* + * 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.content; + +import com.atomgraph.linkeddatahub.cli.BaseCommand; +import com.atomgraph.linkeddatahub.cli.http.HttpException; +import com.atomgraph.linkeddatahub.cli.sparql.Updates; +import java.net.URI; +import picocli.CommandLine.Command; +import picocli.CommandLine.Option; +import picocli.CommandLine.Parameters; + +/** + * Removes a content block (or all content blocks) from a document. Mirrors bin/content/remove-block.sh. + * + * @author Martynas Jusevičius {@literal } + */ +@Command(name = "remove-block", description = "Removes a content block from a document.") +public class RemoveBlock extends BaseCommand +{ + + @Option(names = "--block", paramLabel = "BLOCK_URI", description = "URI of the content block (optional, all blocks if not set)") + private URI block; + + @Parameters(paramLabel = "TARGET_URI", description = "URI of the document") + private URI target; + + @Override + public Integer call() throws Exception + { + HttpException.check(target, getClient().patch(target, Updates.removeBlock(target, block))).close(); + + return 0; + } + +} 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 new file mode 100644 index 0000000000..0decc0d175 --- /dev/null +++ b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/imports/AddCSVImport.java @@ -0,0 +1,123 @@ +/* + * 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.BaseCommand; +import com.atomgraph.linkeddatahub.cli.http.LDHClient; +import com.atomgraph.linkeddatahub.cli.mixin.BaseMixin; +import com.atomgraph.linkeddatahub.cli.vocab.LDH; +import com.atomgraph.linkeddatahub.cli.vocab.SPIN; +import java.net.URI; +import org.apache.jena.rdf.model.Model; +import org.apache.jena.rdf.model.ModelFactory; +import org.apache.jena.rdf.model.Resource; +import org.apache.jena.vocabulary.DCTerms; +import org.apache.jena.vocabulary.RDF; +import picocli.CommandLine.Command; +import picocli.CommandLine.Mixin; +import picocli.CommandLine.Option; +import picocli.CommandLine.Parameters; + +/** + * Adds CSV import metadata to a document. Mirrors bin/imports/add-csv-import.sh. + * + * @author Martynas Jusevičius {@literal } + */ +@Command(name = "add-csv-import", description = "Adds CSV import metadata to a document.") +public class AddCSVImport extends BaseCommand +{ + + @Mixin + private BaseMixin baseMixin; + + @Option(names = "--title", required = true, paramLabel = "TITLE", description = "Title of the import") + private String title; + + @Option(names = "--description", paramLabel = "DESCRIPTION", description = "Description of the import (optional)") + private String description; + + @Option(names = "--uri", paramLabel = "URI", description = "URI of the import (optional, blank node if not set)") + private String uri; + + @Option(names = "--query", required = true, paramLabel = "QUERY_URI", description = "URI of the transformation CONSTRUCT query") + private URI query; + + @Option(names = "--file", required = true, paramLabel = "FILE_URI", description = "URI of the uploaded CSV file") + private URI file; + + @Option(names = "--delimiter", defaultValue = ",", paramLabel = "CHAR", description = "CSV delimiter character (default: ${DEFAULT-VALUE})") + private String delimiter; + + @Parameters(paramLabel = "TARGET_URI", description = "URI of the document") + private URI target; + + @Override + public Integer call() throws Exception + { + baseMixin.require(getSpec()); // required by the script interface + + core(getClient(), target, uri, title, query, file, delimiter, description); + print(target); + + return 0; + } + + /** + * Appends the CSV import metadata to the target document. + * + * @param client client instance + * @param target target document URI + * @param uri import URI (optional) + * @param title import title + * @param query transformation query URI + * @param file uploaded file URI + * @param delimiter CSV delimiter + * @param description import description (optional) + */ + public static void core(LDHClient client, URI target, String uri, String title, URI query, URI file, String delimiter, String description) + { + post(client, target, buildModel(target, uri, title, query, file, delimiter, description)); + } + + /** + * Builds the CSV import description. + * + * @param target target document URI + * @param uri import URI (optional) + * @param title import title + * @param query transformation query URI + * @param file uploaded file URI + * @param delimiter CSV delimiter + * @param description import description (optional) + * @return import model + */ + public static Model buildModel(URI target, String uri, String title, URI query, URI file, String delimiter, String description) + { + Model model = ModelFactory.createDefaultModel(); + + Resource csvImport = createSubject(model, target, uri). + addProperty(RDF.type, LDH.CSVImport). + addProperty(DCTerms.title, title). + addProperty(SPIN.query, model.createResource(query.toString())). + addProperty(LDH.file, model.createResource(file.toString())). + addProperty(LDH.delimiter, delimiter); + if (description != null) csvImport.addProperty(DCTerms.description, description); + + return model; + } + +} 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 new file mode 100644 index 0000000000..4b91effcf6 --- /dev/null +++ b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/imports/AddRDFImport.java @@ -0,0 +1,124 @@ +/* + * 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.BaseCommand; +import com.atomgraph.linkeddatahub.cli.http.LDHClient; +import com.atomgraph.linkeddatahub.cli.mixin.BaseMixin; +import com.atomgraph.linkeddatahub.cli.vocab.LDH; +import com.atomgraph.linkeddatahub.cli.vocab.SD; +import com.atomgraph.linkeddatahub.cli.vocab.SPIN; +import java.net.URI; +import org.apache.jena.rdf.model.Model; +import org.apache.jena.rdf.model.ModelFactory; +import org.apache.jena.rdf.model.Resource; +import org.apache.jena.vocabulary.DCTerms; +import org.apache.jena.vocabulary.RDF; +import picocli.CommandLine.Command; +import picocli.CommandLine.Mixin; +import picocli.CommandLine.Option; +import picocli.CommandLine.Parameters; + +/** + * Adds RDF import metadata to a document. Mirrors bin/imports/add-rdf-import.sh. + * + * @author Martynas Jusevičius {@literal } + */ +@Command(name = "add-rdf-import", description = "Adds RDF import metadata to a document.") +public class AddRDFImport extends BaseCommand +{ + + @Mixin + private BaseMixin baseMixin; + + @Option(names = "--title", required = true, paramLabel = "TITLE", description = "Title of the import") + private String title; + + @Option(names = "--description", paramLabel = "DESCRIPTION", description = "Description of the import (optional)") + private String description; + + @Option(names = "--uri", paramLabel = "URI", description = "URI of the import (optional, blank node if not set)") + private String uri; + + @Option(names = "--query", paramLabel = "QUERY_URI", description = "URI of the transformation CONSTRUCT query (optional)") + private URI query; + + @Option(names = "--graph", paramLabel = "GRAPH_URI", description = "URI of the target named graph (optional)") + private URI graph; + + @Option(names = "--file", required = true, paramLabel = "FILE_URI", description = "URI of the uploaded RDF file") + private URI file; + + @Parameters(paramLabel = "TARGET_URI", description = "URI of the document") + private URI target; + + @Override + public Integer call() throws Exception + { + baseMixin.require(getSpec()); // required by the script interface + + core(getClient(), target, uri, title, file, query, graph, description); + print(target); + + return 0; + } + + /** + * Appends the RDF import metadata to the target document. + * + * @param client client instance + * @param target target document URI + * @param uri import URI (optional) + * @param title import title + * @param file uploaded file URI + * @param query transformation query URI (optional) + * @param graph target named graph URI (optional) + * @param description import description (optional) + */ + public static void core(LDHClient client, URI target, String uri, String title, URI file, URI query, URI graph, String description) + { + post(client, target, buildModel(target, uri, title, file, query, graph, description)); + } + + /** + * Builds the RDF import description. + * + * @param target target document URI + * @param uri import URI (optional) + * @param title import title + * @param file uploaded file URI + * @param query transformation query URI (optional) + * @param graph target named graph URI (optional) + * @param description import description (optional) + * @return import model + */ + public static Model buildModel(URI target, String uri, String title, URI file, URI query, URI graph, String description) + { + Model model = ModelFactory.createDefaultModel(); + + Resource rdfImport = createSubject(model, target, uri). + addProperty(RDF.type, LDH.RDFImport). + addProperty(DCTerms.title, title). + addProperty(LDH.file, model.createResource(file.toString())); + if (graph != null) rdfImport.addProperty(SD.name, model.createResource(graph.toString())); + if (query != null) rdfImport.addProperty(SPIN.query, model.createResource(query.toString())); + if (description != null) rdfImport.addProperty(DCTerms.description, description); + + return model; + } + +} 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 new file mode 100644 index 0000000000..ea45095184 --- /dev/null +++ b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/imports/ImportCSV.java @@ -0,0 +1,81 @@ +/* + * 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.BaseCommand; +import com.atomgraph.linkeddatahub.cli.command.AddConstruct; +import com.atomgraph.linkeddatahub.cli.command.AddFile; +import com.atomgraph.linkeddatahub.cli.mixin.BaseMixin; +import com.atomgraph.linkeddatahub.cli.util.Slugs; +import java.net.URI; +import java.nio.file.Files; +import java.nio.file.Path; +import picocli.CommandLine.Command; +import picocli.CommandLine.Mixin; +import picocli.CommandLine.Option; +import picocli.CommandLine.Parameters; + +/** + * Imports CSV data: adds the transformation query, uploads the CSV file and creates the + * import metadata on the target document. Mirrors bin/imports/import-csv.sh, + * calling the same steps in-process instead of via subscripts. + * + * @author Martynas Jusevičius {@literal } + */ +@Command(name = "import-csv", description = "Imports CSV data using a transformation query.") +public class ImportCSV extends BaseCommand +{ + + @Mixin + private BaseMixin baseMixin; + + @Option(names = "--title", required = true, paramLabel = "TITLE", description = "Title of the import") + private String title; + + @Option(names = "--description", paramLabel = "DESCRIPTION", description = "Description of the import (optional)") + private String description; + + @Option(names = "--query-file", required = true, paramLabel = "ABS_PATH", description = "Path to the file with the transformation CONSTRUCT query") + private Path queryFile; + + @Option(names = "--csv-file", required = true, paramLabel = "ABS_PATH", description = "Path to the CSV file") + private Path csvFile; + + @Option(names = "--delimiter", defaultValue = ",", paramLabel = "CHAR", description = "CSV delimiter character (default: ${DEFAULT-VALUE})") + private String delimiter; + + @Parameters(paramLabel = "TARGET_URI", description = "URI of the import document") + private URI target; + + @Override + public Integer call() throws Exception + { + URI base = baseMixin.require(getSpec()); + + String queryId = Slugs.defaultSlug(); + AddConstruct.core(getClient(), target, "#" + queryId, title, Files.readString(queryFile), null, null); + URI query = target.resolve("#" + queryId); + + URI fileURI = AddFile.core(getClient(), base, target, csvFile, "text/csv", title, null); + + AddCSVImport.core(getClient(), target, null, title, query, fileURI, delimiter, description); + print(target); + + return 0; + } + +} 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 new file mode 100644 index 0000000000..db3cbed84e --- /dev/null +++ b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/imports/ImportRDF.java @@ -0,0 +1,89 @@ +/* + * 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.BaseCommand; +import com.atomgraph.linkeddatahub.cli.command.AddConstruct; +import com.atomgraph.linkeddatahub.cli.command.AddFile; +import com.atomgraph.linkeddatahub.cli.mixin.BaseMixin; +import com.atomgraph.linkeddatahub.cli.util.Slugs; +import java.net.URI; +import java.nio.file.Files; +import java.nio.file.Path; +import picocli.CommandLine.Command; +import picocli.CommandLine.Mixin; +import picocli.CommandLine.Option; +import picocli.CommandLine.Parameters; + +/** + * Imports RDF data: optionally adds a transformation query, uploads the RDF file and creates + * the import metadata on the target document. Mirrors bin/imports/import-rdf.sh, + * calling the same steps in-process instead of via subscripts. + * + * @author Martynas Jusevičius {@literal } + */ +@Command(name = "import-rdf", description = "Imports RDF data, optionally using a transformation query.") +public class ImportRDF extends BaseCommand +{ + + @Mixin + private BaseMixin baseMixin; + + @Option(names = "--title", required = true, paramLabel = "TITLE", description = "Title of the import") + private String title; + + @Option(names = "--description", paramLabel = "DESCRIPTION", description = "Description of the import (optional)") + private String description; + + @Option(names = "--query-file", paramLabel = "ABS_PATH", description = "Path to the file with the transformation CONSTRUCT query (optional)") + private Path queryFile; + + @Option(names = "--rdf-file", required = true, paramLabel = "ABS_PATH", description = "Path to the RDF file") + private Path rdfFile; + + @Option(names = "--content-type", required = true, paramLabel = "MEDIA_TYPE", description = "Media type of the RDF file (e.g. text/turtle)") + private String contentType; + + @Option(names = "--graph", paramLabel = "GRAPH_URI", description = "URI of the target named graph (optional)") + private URI graph; + + @Parameters(paramLabel = "TARGET_URI", description = "URI of the import document") + private URI target; + + @Override + public Integer call() throws Exception + { + URI base = baseMixin.require(getSpec()); + + URI query = null; + if (queryFile != null) + { + String queryId = Slugs.defaultSlug(); + AddConstruct.core(getClient(), target, "#" + queryId, title, Files.readString(queryFile), null, null); + query = target.resolve("#" + queryId); + } + + URI fileURI = AddFile.core(getClient(), base, target, rdfFile, contentType, title, null); + + String importId = Slugs.defaultSlug(); + AddRDFImport.core(getClient(), target, "#" + importId, title, fileURI, query, query == null ? graph : null, description); + print(target); + + return 0; + } + +} 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/imports/Imports.java new file mode 100644 index 0000000000..65b084d19c --- /dev/null +++ b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/command/imports/Imports.java @@ -0,0 +1,45 @@ +/* + * 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 java.util.concurrent.Callable; +import picocli.CommandLine.Command; +import picocli.CommandLine.Model.CommandSpec; +import picocli.CommandLine.ParameterException; +import picocli.CommandLine.Spec; + +/** + * Data import command group. + * + * @author Martynas Jusevičius {@literal } + */ +@Command(name = "imports", + description = "Data import commands.", + subcommands = { AddCSVImport.class, AddRDFImport.class, ImportCSV.class, ImportRDF.class }) +public class Imports implements Callable +{ + + @Spec + private CommandSpec spec; + + @Override + public Integer call() + { + throw new ParameterException(spec.commandLine(), "Missing required subcommand"); + } + +} diff --git a/cli/src/main/java/com/atomgraph/linkeddatahub/cli/http/ClientFactory.java b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/http/ClientFactory.java new file mode 100644 index 0000000000..2a7cf1f24c --- /dev/null +++ b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/http/ClientFactory.java @@ -0,0 +1,127 @@ +/* + * 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.http; + +import com.atomgraph.core.io.ModelProvider; +import jakarta.ws.rs.client.Client; +import jakarta.ws.rs.client.ClientBuilder; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.GeneralSecurityException; +import java.security.KeyStore; +import java.security.SecureRandom; +import java.security.cert.X509Certificate; +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManager; +import javax.net.ssl.X509TrustManager; +import org.apache.http.config.Registry; +import org.apache.http.config.RegistryBuilder; +import org.apache.http.conn.socket.ConnectionSocketFactory; +import org.apache.http.conn.socket.PlainConnectionSocketFactory; +import org.apache.http.conn.ssl.NoopHostnameVerifier; +import org.apache.http.conn.ssl.SSLConnectionSocketFactory; +import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; +import org.glassfish.jersey.apache.connector.ApacheClientProperties; +import org.glassfish.jersey.apache.connector.ApacheConnectorProvider; +import org.glassfish.jersey.client.ClientConfig; +import org.glassfish.jersey.client.ClientProperties; +import org.glassfish.jersey.client.RequestEntityProcessing; +import org.glassfish.jersey.media.multipart.MultiPartFeature; + +/** + * Builds a Jersey HTTP client authenticated with a WebID client certificate from a PKCS12 keystore. + * Mirrors Application.getClient() in LinkedDataHub, with server certificate checks + * disabled (equivalent of curl -k against self-signed dev instances). + * + * @author Martynas Jusevičius {@literal } + */ +public final class ClientFactory +{ + + private ClientFactory() { } + + /** + * Builds the client instance. + * + * @param keyStoreFile PKCS12 (.p12) keystore file with the WebID certificate + * @param keyStorePassword keystore password + * @return client instance + */ + public static Client createClient(Path keyStoreFile, String keyStorePassword) + { + SSLContext ctx; + try + { + KeyStore keyStore = KeyStore.getInstance("PKCS12"); + try (InputStream is = Files.newInputStream(keyStoreFile)) + { + keyStore.load(is, keyStorePassword.toCharArray()); + } + + // for client authentication + KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); + kmf.init(keyStore, keyStorePassword.toCharArray()); + + ctx = SSLContext.getInstance("TLS"); + ctx.init(kmf.getKeyManagers(), new TrustManager[] { TRUST_ALL }, new SecureRandom()); + } + catch (IOException | GeneralSecurityException ex) + { + throw new IllegalArgumentException("Could not load PKCS12 keystore '" + keyStoreFile + "': " + ex.getMessage() + " (wrong password?)", ex); + } + + Registry socketFactoryRegistry = RegistryBuilder.create(). + register("https", new SSLConnectionSocketFactory(ctx, NoopHostnameVerifier.INSTANCE)). + register("http", new PlainConnectionSocketFactory()). + build(); + + ClientConfig config = new ClientConfig(); + config.connectorProvider(new ApacheConnectorProvider()); + config.register(MultiPartFeature.class); + config.register(new ModelProvider()); + config.property(ClientProperties.FOLLOW_REDIRECTS, false); // scripts use curl without -L + config.property(ClientProperties.REQUEST_ENTITY_PROCESSING, RequestEntityProcessing.BUFFERED); + config.property(ApacheClientProperties.CONNECTION_MANAGER, new PoolingHttpClientConnectionManager(socketFactoryRegistry)); + + return ClientBuilder.newBuilder(). + withConfig(config). + sslContext(ctx). + hostnameVerifier(NoopHostnameVerifier.INSTANCE). + build(); + } + + private static final X509TrustManager TRUST_ALL = new X509TrustManager() + { + + @Override + public void checkClientTrusted(X509Certificate[] chain, String authType) { } + + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType) { } + + @Override + public X509Certificate[] getAcceptedIssuers() + { + return new X509Certificate[0]; + } + + }; + +} diff --git a/cli/src/main/java/com/atomgraph/linkeddatahub/cli/http/HttpException.java b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/http/HttpException.java new file mode 100644 index 0000000000..a4bf717018 --- /dev/null +++ b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/http/HttpException.java @@ -0,0 +1,102 @@ +/* + * 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.http; + +import jakarta.ws.rs.core.Response; +import java.net.URI; + +/** + * Thrown when the server responds with an error status, mirroring curl -f. + * + * @author Martynas Jusevičius {@literal } + */ +public class HttpException extends RuntimeException +{ + + private static final int BODY_EXCERPT_LENGTH = 1024; + + private final int status; + private final URI uri; + + /** + * Constructs the exception. + * + * @param status HTTP status code + * @param reasonPhrase HTTP reason phrase + * @param uri request URI + * @param body response body excerpt (can be empty) + */ + public HttpException(int status, String reasonPhrase, URI uri, String body) + { + super("HTTP " + status + " " + reasonPhrase + " — " + uri + (body == null || body.isBlank() ? "" : "\n" + body)); + this.status = status; + this.uri = uri; + } + + /** + * Throws if the response has an error status (≥ 400), otherwise returns it. + * + * @param uri request URI (for the error message) + * @param response response to check + * @return the same response + */ + public static Response check(URI uri, Response response) + { + if (response.getStatus() >= 400) + { + String body = ""; + try + { + body = response.readEntity(String.class); + if (body.length() > BODY_EXCERPT_LENGTH) body = body.substring(0, BODY_EXCERPT_LENGTH) + "…"; + } + catch (Exception ex) + { + // body is unreadable - leave the excerpt empty + } + finally + { + response.close(); + } + + throw new HttpException(response.getStatus(), response.getStatusInfo().getReasonPhrase(), uri, body); + } + + return response; + } + + /** + * Returns the HTTP status code. + * + * @return status code + */ + public int getStatus() + { + return status; + } + + /** + * Returns the request URI. + * + * @return request URI + */ + public URI getUri() + { + return uri; + } + +} diff --git a/cli/src/main/java/com/atomgraph/linkeddatahub/cli/http/LDHClient.java b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/http/LDHClient.java new file mode 100644 index 0000000000..c62a0a4dd4 --- /dev/null +++ b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/http/LDHClient.java @@ -0,0 +1,100 @@ +/* + * 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.http; + +import com.atomgraph.core.MediaTypes; +import com.atomgraph.core.client.GraphStoreClient; +import com.atomgraph.linkeddatahub.cli.util.URIRewriter; +import jakarta.ws.rs.client.Client; +import jakarta.ws.rs.client.Entity; +import jakarta.ws.rs.client.WebTarget; +import jakarta.ws.rs.core.Form; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import java.net.URI; + +/** + * Graph Store Protocol client for LinkedDataHub documents (direct graph identification), + * extended with SPARQL update over PATCH and form-encoded POST. When a proxy URI is given, + * every request URI has its origin rewritten to the proxy's origin, matching the + * --proxy handling of the bin/ shell scripts. + * + * @author Martynas Jusevičius {@literal } + */ +public class LDHClient extends GraphStoreClient +{ + + /** SPARQL update media type */ + public static final MediaType APPLICATION_SPARQL_UPDATE_TYPE = MediaType.valueOf("application/sparql-update"); + + private final URI proxy; + + /** + * Constructs the client. + * + * @param client Jersey client with WebID client certificate + * @param mediaTypes registry of readable/writable media types + * @param proxy proxy URI whose origin replaces the request URI origin (optional, can be null) + */ + public LDHClient(Client client, MediaTypes mediaTypes, URI proxy) + { + super(client, mediaTypes); + this.proxy = proxy; + } + + @Override + protected WebTarget getWebTarget(URI uri) + { + return super.getWebTarget(getProxy() != null ? URIRewriter.rewrite(uri, getProxy()) : uri); + } + + /** + * Patches a document with a SPARQL update. + * + * @param uri document URI + * @param update SPARQL update string + * @return response + */ + public Response patch(URI uri, String update) + { + return getWebTarget(uri).request().method("PATCH", Entity.entity(update, APPLICATION_SPARQL_UPDATE_TYPE)); + } + + /** + * Posts a form-encoded request. + * + * @param uri target URI + * @param form form params + * @param acceptedTypes accepted response media types + * @return response + */ + public Response postForm(URI uri, Form form, MediaType... acceptedTypes) + { + return getWebTarget(uri).request(acceptedTypes).post(Entity.form(form)); + } + + /** + * Returns the proxy URI, if any. + * + * @return proxy URI or null + */ + public URI getProxy() + { + return proxy; + } + +} diff --git a/cli/src/main/java/com/atomgraph/linkeddatahub/cli/mixin/BaseMixin.java b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/mixin/BaseMixin.java new file mode 100644 index 0000000000..5b56680cf4 --- /dev/null +++ b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/mixin/BaseMixin.java @@ -0,0 +1,59 @@ +/* + * 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.mixin; + +import java.net.URI; +import picocli.CommandLine.Model.CommandSpec; +import picocli.CommandLine.Option; +import picocli.CommandLine.ParameterException; + +/** + * Application base URI option shared by commands whose script counterpart takes -b. + * + * @author Martynas Jusevičius {@literal } + */ +public class BaseMixin +{ + + @Option(names = {"-b", "--base"}, defaultValue = "${env:LDH_BASE}", paramLabel = "BASE_URI", + description = "Base URI of the application (env: LDH_BASE)") + private URI base; + + /** + * Validates presence of the base URI and returns it. + * + * @param spec command spec used to raise usage errors + * @return base URI + */ + public URI require(CommandSpec spec) + { + if (base == null) throw new ParameterException(spec.commandLine(), "Missing required option: '--base=BASE_URI' (or set LDH_BASE)"); + + return base; + } + + /** + * Returns the base URI, if any. + * + * @return base URI or null + */ + public URI getBase() + { + return base; + } + +} diff --git a/cli/src/main/java/com/atomgraph/linkeddatahub/cli/mixin/CertAuthMixin.java b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/mixin/CertAuthMixin.java new file mode 100644 index 0000000000..23ac29efaa --- /dev/null +++ b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/mixin/CertAuthMixin.java @@ -0,0 +1,71 @@ +/* + * 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.mixin; + +import java.nio.file.Path; +import picocli.CommandLine.Model.CommandSpec; +import picocli.CommandLine.Option; +import picocli.CommandLine.ParameterException; + +/** + * WebID client certificate options shared by all commands. + * + * @author Martynas Jusevičius {@literal } + */ +public class CertAuthMixin +{ + + @Option(names = {"-f", "--cert-file"}, defaultValue = "${env:LDH_CERT_FILE}", paramLabel = "CERT_FILE", + description = ".p12 (PKCS12) keystore with the WebID certificate of the agent (env: LDH_CERT_FILE)") + private Path certFile; + + @Option(names = {"-p", "--cert-password"}, defaultValue = "${env:LDH_CERT_PASSWORD}", paramLabel = "CERT_PASSWORD", + description = "Password of the WebID certificate (env: LDH_CERT_PASSWORD)") + private String certPassword; + + /** + * Validates that both certificate options are present. + * + * @param spec command spec used to raise usage errors + */ + public void validate(CommandSpec spec) + { + if (certFile == null) throw new ParameterException(spec.commandLine(), "Missing required option: '--cert-file=CERT_FILE' (or set LDH_CERT_FILE)"); + if (certPassword == null) throw new ParameterException(spec.commandLine(), "Missing required option: '--cert-password=CERT_PASSWORD' (or set LDH_CERT_PASSWORD)"); + } + + /** + * Returns the keystore path. + * + * @return keystore path + */ + public Path getCertFile() + { + return certFile; + } + + /** + * Returns the keystore password. + * + * @return keystore password + */ + public String getCertPassword() + { + return certPassword; + } + +} diff --git a/cli/src/main/java/com/atomgraph/linkeddatahub/cli/mixin/ProxyMixin.java b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/mixin/ProxyMixin.java new file mode 100644 index 0000000000..876a8d54a5 --- /dev/null +++ b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/mixin/ProxyMixin.java @@ -0,0 +1,44 @@ +/* + * 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.mixin; + +import java.net.URI; +import picocli.CommandLine.Option; + +/** + * Proxy option shared by all commands. + * + * @author Martynas Jusevičius {@literal } + */ +public class ProxyMixin +{ + + @Option(names = "--proxy", defaultValue = "${env:LDH_PROXY}", paramLabel = "PROXY_URL", + description = "The host this request will be proxied through (optional) (env: LDH_PROXY)") + private URI proxy; + + /** + * Returns the proxy URI, if any. + * + * @return proxy URI or null + */ + public URI getProxy() + { + return proxy; + } + +} 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 new file mode 100644 index 0000000000..cb621e377b --- /dev/null +++ b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/sparql/Updates.java @@ -0,0 +1,151 @@ +/* + * 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.sparql; + +import java.net.URI; +import org.apache.jena.query.ParameterizedSparqlString; + +/** + * SPARQL update templates for the PATCH-based commands. All templates are standard SPARQL 1.1; + * IRIs are injected via {@link ParameterizedSparqlString} to guarantee correct escaping. + * + * @author Martynas Jusevičius {@literal } + */ +public final class Updates +{ + + private Updates() { } + + /** + * Adds an owl:imports statement to the ontology that is the primary topic + * of the given document. + * + * @param ontologyDoc ontology document URI + * @param importURI imported ontology URI + * @return SPARQL update string + */ + public static String insertOntologyImport(URI ontologyDoc, URI importURI) + { + ParameterizedSparqlString pss = new ParameterizedSparqlString(""" + PREFIX owl: + PREFIX foaf: + INSERT { + ?ontology owl:imports ?import . + } + WHERE { + ?doc foaf:primaryTopic ?ontology . + } + """); + pss.setIri("doc", ontologyDoc.toString()); + pss.setIri("import", importURI.toString()); + return pss.toString(); + } + + /** + * Adds a foaf:member statement to the group that is the primary topic + * of the given document. + * + * @param groupDoc group document URI + * @param agent agent URI + * @return SPARQL update string + */ + public static String insertGroupMember(URI groupDoc, URI agent) + { + ParameterizedSparqlString pss = new ParameterizedSparqlString(""" + PREFIX foaf: + INSERT { + ?group foaf:member ?agent . + } + WHERE { + ?doc foaf:primaryTopic ?group . + } + """); + pss.setIri("doc", groupDoc.toString()); + pss.setIri("agent", agent.toString()); + return pss.toString(); + } + + /** + * Removes a content block (or all content blocks) from a document, together with the + * block's own description. + * + * @param doc document URI + * @param block block URI, or null to remove all blocks + * @return SPARQL update string + */ + public static String removeBlock(URI doc, URI block) + { + ParameterizedSparqlString pss = new ParameterizedSparqlString(""" + PREFIX rdf: + + DELETE + { + ?doc ?seq ?block . + ?block ?p ?o . + } + WHERE + { + ?doc ?seq ?block . + FILTER(strstarts(str(?seq), concat(str(rdf:), "_"))) + OPTIONAL + { + ?block ?p ?o + } + } + """); + pss.setIri("doc", doc.toString()); + if (block != null) pss.setIri("block", block.toString()); + return pss.toString(); + } + + /** + * Makes all end-user application documents publicly readable and allows queries over POST, + * by extending the built-in public authorization. + * + * @param base end-user application base URI + * @param adminBase admin application base URI + * @return SPARQL update string + */ + public static String makePublic(URI base, URI adminBase) + { + ParameterizedSparqlString pss = new ParameterizedSparqlString(""" + PREFIX acl: + PREFIX def: + PREFIX dh: + PREFIX nfo: + PREFIX foaf: + + INSERT + { + ?public acl:accessToClass def:Root, dh:Container, dh:Item, nfo:FileDataObject ; + acl:accessTo ?sparql . + + ?sparqlPost a acl:Authorization ; + acl:accessTo ?sparql ; + acl:mode acl:Append ; + acl:agentClass foaf:Agent, acl:AuthenticatedAgent . # hacky way to allow queries over POST + } + WHERE + {} + """); + pss.setIri("public", adminBase + "acl/authorizations/public/#this"); + pss.setIri("sparqlPost", adminBase + "acl/authorizations/public/#sparql-post"); + pss.setIri("sparql", base + "sparql"); + return pss.toString(); + } + +} diff --git a/cli/src/main/java/com/atomgraph/linkeddatahub/cli/util/Digests.java b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/util/Digests.java new file mode 100644 index 0000000000..dd1638251b --- /dev/null +++ b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/util/Digests.java @@ -0,0 +1,69 @@ +/* + * 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.io.IOException; +import java.io.InputStream; +import java.io.UncheckedIOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.HexFormat; + +/** + * File digest helpers. + * + * @author Martynas Jusevičius {@literal } + */ +public final class Digests +{ + + private Digests() { } + + /** + * Returns the lowercase hex SHA1 digest of a file, matching shasum -a 1. + * + * @param file file path + * @return hex digest string + */ + public static String sha1Hex(Path file) + { + try + { + MessageDigest md = MessageDigest.getInstance("SHA-1"); + + try (InputStream is = Files.newInputStream(file)) + { + byte[] buffer = new byte[8192]; + int read; + while ((read = is.read(buffer)) != -1) md.update(buffer, 0, read); + } + + return HexFormat.of().formatHex(md.digest()); + } + catch (NoSuchAlgorithmException ex) + { + throw new IllegalStateException(ex); + } + catch (IOException ex) + { + throw new UncheckedIOException("Could not read file '" + file + "'", ex); + } + } + +} diff --git a/cli/src/main/java/com/atomgraph/linkeddatahub/cli/util/SequenceNumbers.java b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/util/SequenceNumbers.java new file mode 100644 index 0000000000..22fcc37387 --- /dev/null +++ b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/util/SequenceNumbers.java @@ -0,0 +1,75 @@ +/* + * 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 org.apache.jena.rdf.model.Model; +import org.apache.jena.rdf.model.Property; +import org.apache.jena.rdf.model.RDFNode; +import org.apache.jena.rdf.model.Resource; +import org.apache.jena.rdf.model.ResourceFactory; +import org.apache.jena.rdf.model.StmtIterator; +import org.apache.jena.vocabulary.RDF; + +/** + * RDF container membership property (rdf:_N) helpers for content blocks. + * + * @author Martynas Jusevičius {@literal } + */ +public final class SequenceNumbers +{ + + private SequenceNumbers() { } + + /** + * Returns the next free membership property rdf:_(max + 1) for the given subject, + * where max is the highest existing rdf:_N predicate (0 if none). + * + * @param model model with the subject's description + * @param subject resource whose membership properties are scanned + * @return next membership property + */ + public static Property nextSequenceProperty(Model model, Resource subject) + { + String prefix = RDF.getURI() + "_"; + int max = 0; + + StmtIterator it = model.listStatements(subject, null, (RDFNode)null); + try + { + while (it.hasNext()) + { + String uri = it.next().getPredicate().getURI(); + if (uri.startsWith(prefix)) + try + { + max = Math.max(max, Integer.parseInt(uri.substring(prefix.length()))); + } + catch (NumberFormatException ex) + { + // not a membership property, e.g. rdf:_x + } + } + } + finally + { + it.close(); + } + + return ResourceFactory.createProperty(prefix + (max + 1)); + } + +} diff --git a/cli/src/main/java/com/atomgraph/linkeddatahub/cli/util/Slugs.java b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/util/Slugs.java new file mode 100644 index 0000000000..6d77b04ed8 --- /dev/null +++ b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/util/Slugs.java @@ -0,0 +1,42 @@ +/* + * 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.util.Locale; +import java.util.UUID; + +/** + * Default URI path segment generation. + * + * @author Martynas Jusevičius {@literal } + */ +public final class Slugs +{ + + private Slugs() { } + + /** + * Returns a random lowercase UUID slug, matching uuidgen | tr '[:upper:]' '[:lower:]'. + * + * @return slug string + */ + public static String defaultSlug() + { + return UUID.randomUUID().toString().toLowerCase(Locale.ROOT); + } + +} diff --git a/cli/src/main/java/com/atomgraph/linkeddatahub/cli/util/URIRewriter.java b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/util/URIRewriter.java new file mode 100644 index 0000000000..593181c035 --- /dev/null +++ b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/util/URIRewriter.java @@ -0,0 +1,104 @@ +/* + * 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.nio.charset.StandardCharsets; + +/** + * URI manipulation helpers matching the conventions of the bin/ shell scripts. + * + * @author Martynas Jusevičius {@literal } + */ +public final class URIRewriter +{ + + private URIRewriter() { } + + /** + * Replaces the origin (scheme and authority) of a URI with the origin of the proxy URI, + * keeping the path, query and fragment. + * + * @param uri logical URI + * @param proxy proxy URI whose origin is substituted + * @return rewritten URI + */ + public static URI rewrite(URI uri, URI proxy) + { + return URI.create(origin(proxy) + uri.toString().substring(origin(uri).length())); + } + + /** + * Returns the origin (scheme and authority) of a URI. + * + * @param uri URI + * @return origin string, e.g. https://localhost:4443 + */ + public static String origin(URI uri) + { + if (uri.getScheme() == null || uri.getRawAuthority() == null) throw new IllegalArgumentException("URI '" + uri + "' is not absolute"); + + return uri.getScheme() + "://" + uri.getRawAuthority(); + } + + /** + * Converts an end-user application base URI to the base URI of its admin application + * by prefixing the host with the admin. subdomain. + * + * @param base end-user base URI + * @return admin base URI + */ + public static URI adminBase(URI base) + { + return URI.create(base.toString().replaceFirst("://", "://admin.")); + } + + /** + * Percent-encodes a string as a URI path segment. All characters except RFC 3986 + * unreserved ones are encoded, including /. + * + * @param slug path segment + * @return encoded path segment + */ + public static String encodeSlug(String slug) + { + StringBuilder sb = new StringBuilder(); + + for (byte b : slug.getBytes(StandardCharsets.UTF_8)) + { + char c = (char)(b & 0xFF); + if ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || + c == '-' || c == '.' || c == '_' || c == '~') sb.append(c); + else sb.append('%').append(String.format("%02X", b & 0xFF)); + } + + return sb.toString(); + } + + /** + * Builds the URI of a child document from the parent container URI and a path segment slug. + * + * @param parent parent container URI (with trailing slash) + * @param slug path segment + * @return child document URI (with trailing slash) + */ + public static URI childURI(URI parent, String slug) + { + return URI.create(parent.toString() + encodeSlug(slug) + "/"); + } + +} diff --git a/cli/src/main/java/com/atomgraph/linkeddatahub/cli/vocab/A.java b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/vocab/A.java new file mode 100644 index 0000000000..73dd5abd3f --- /dev/null +++ b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/vocab/A.java @@ -0,0 +1,42 @@ +/* + * 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.vocab; + +import org.apache.jena.rdf.model.Property; +import org.apache.jena.rdf.model.ResourceFactory; + +/** + * AtomGraph Core vocabulary. + * + * @author Martynas Jusevičius {@literal } + */ +public final class A +{ + + /** Namespace URI */ + public static final String NS = "https://w3id.org/atomgraph/core#"; + + private A() { } + + /** a:graphStore property */ + public static final Property graphStore = ResourceFactory.createProperty(NS + "graphStore"); + /** a:authUser property */ + public static final Property authUser = ResourceFactory.createProperty(NS + "authUser"); + /** a:authPwd property */ + public static final Property authPwd = ResourceFactory.createProperty(NS + "authPwd"); + +} diff --git a/cli/src/main/java/com/atomgraph/linkeddatahub/cli/vocab/AC.java b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/vocab/AC.java new file mode 100644 index 0000000000..9de56512ea --- /dev/null +++ b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/vocab/AC.java @@ -0,0 +1,38 @@ +/* + * 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.vocab; + +import org.apache.jena.rdf.model.Property; +import org.apache.jena.rdf.model.ResourceFactory; + +/** + * AtomGraph Client vocabulary. + * + * @author Martynas Jusevičius {@literal } + */ +public final class AC +{ + + /** Namespace URI */ + public static final String NS = "https://w3id.org/atomgraph/client#"; + + private AC() { } + + /** ac:mode property */ + public static final Property mode = ResourceFactory.createProperty(NS + "mode"); + +} diff --git a/cli/src/main/java/com/atomgraph/linkeddatahub/cli/vocab/ACL.java b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/vocab/ACL.java new file mode 100644 index 0000000000..e7f3ff7096 --- /dev/null +++ b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/vocab/ACL.java @@ -0,0 +1,60 @@ +/* + * 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.vocab; + +import org.apache.jena.rdf.model.Property; +import org.apache.jena.rdf.model.Resource; +import org.apache.jena.rdf.model.ResourceFactory; + +/** + * W3C Web Access Control vocabulary. + * + * @author Martynas Jusevičius {@literal } + */ +public final class ACL +{ + + /** Namespace URI */ + public static final String NS = "http://www.w3.org/ns/auth/acl#"; + + private ACL() { } + + /** acl:Authorization class */ + public static final Resource Authorization = ResourceFactory.createResource(NS + "Authorization"); + /** acl:Append mode */ + public static final Resource Append = ResourceFactory.createResource(NS + "Append"); + /** acl:Control mode */ + public static final Resource Control = ResourceFactory.createResource(NS + "Control"); + /** acl:Read mode */ + public static final Resource Read = ResourceFactory.createResource(NS + "Read"); + /** acl:Write mode */ + public static final Resource Write = ResourceFactory.createResource(NS + "Write"); + + /** acl:agent property */ + public static final Property agent = ResourceFactory.createProperty(NS + "agent"); + /** acl:agentClass property */ + public static final Property agentClass = ResourceFactory.createProperty(NS + "agentClass"); + /** acl:agentGroup property */ + public static final Property agentGroup = ResourceFactory.createProperty(NS + "agentGroup"); + /** acl:accessTo property */ + public static final Property accessTo = ResourceFactory.createProperty(NS + "accessTo"); + /** acl:accessToClass property */ + public static final Property accessToClass = ResourceFactory.createProperty(NS + "accessToClass"); + /** acl:mode property */ + public static final Property mode = ResourceFactory.createProperty(NS + "mode"); + +} diff --git a/cli/src/main/java/com/atomgraph/linkeddatahub/cli/vocab/DH.java b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/vocab/DH.java new file mode 100644 index 0000000000..5829aae8e4 --- /dev/null +++ b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/vocab/DH.java @@ -0,0 +1,40 @@ +/* + * 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.vocab; + +import org.apache.jena.rdf.model.Resource; +import org.apache.jena.rdf.model.ResourceFactory; + +/** + * Document hierarchy vocabulary. + * + * @author Martynas Jusevičius {@literal } + */ +public final class DH +{ + + /** Namespace URI */ + public static final String NS = "https://www.w3.org/ns/ldt/document-hierarchy#"; + + private DH() { } + + /** dh:Item class */ + public static final Resource Item = ResourceFactory.createResource(NS + "Item"); + /** dh:Container class */ + public static final Resource Container = ResourceFactory.createResource(NS + "Container"); + +} 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 new file mode 100644 index 0000000000..d256327514 --- /dev/null +++ b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/vocab/LDH.java @@ -0,0 +1,68 @@ +/* + * 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.vocab; + +import org.apache.jena.rdf.model.Property; +import org.apache.jena.rdf.model.Resource; +import org.apache.jena.rdf.model.ResourceFactory; + +/** + * LinkedDataHub vocabulary. + * + * @author Martynas Jusevičius {@literal } + */ +public final class LDH +{ + + /** Namespace URI */ + public static final String NS = "https://w3id.org/atomgraph/linkeddatahub#"; + + private LDH() { } + + /** ldh:Object class */ + public static final Resource Object = ResourceFactory.createResource(NS + "Object"); + /** ldh:XHTML class */ + public static final Resource XHTML = ResourceFactory.createResource(NS + "XHTML"); + /** ldh:View class */ + public static final Resource View = ResourceFactory.createResource(NS + "View"); + /** ldh:ResultSetChart class */ + public static final Resource ResultSetChart = ResourceFactory.createResource(NS + "ResultSetChart"); + /** ldh:CSVImport class */ + public static final Resource CSVImport = ResourceFactory.createResource(NS + "CSVImport"); + /** ldh:RDFImport class */ + public static final Resource RDFImport = ResourceFactory.createResource(NS + "RDFImport"); + /** ldh:MissingPropertyValue constraint class */ + public static final Resource MissingPropertyValue = ResourceFactory.createResource(NS + "MissingPropertyValue"); + /** ldh:ChildrenView resource */ + public static final Resource ChildrenView = ResourceFactory.createResource(NS + "ChildrenView"); + /** ldh:SelectChildren query resource */ + public static final Resource SelectChildren = ResourceFactory.createResource(NS + "SelectChildren"); + + /** ldh:service property */ + public static final Property service = ResourceFactory.createProperty(NS + "service"); + /** ldh:file property */ + public static final Property file = ResourceFactory.createProperty(NS + "file"); + /** ldh:delimiter property */ + public static final Property delimiter = ResourceFactory.createProperty(NS + "delimiter"); + /** ldh:chartType property */ + public static final Property chartType = ResourceFactory.createProperty(NS + "chartType"); + /** ldh:categoryVarName property */ + public static final Property categoryVarName = ResourceFactory.createProperty(NS + "categoryVarName"); + /** ldh:seriesVarName property */ + public static final Property seriesVarName = ResourceFactory.createProperty(NS + "seriesVarName"); + +} diff --git a/cli/src/main/java/com/atomgraph/linkeddatahub/cli/vocab/NFO.java b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/vocab/NFO.java new file mode 100644 index 0000000000..de6a53c654 --- /dev/null +++ b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/vocab/NFO.java @@ -0,0 +1,42 @@ +/* + * 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.vocab; + +import org.apache.jena.rdf.model.Property; +import org.apache.jena.rdf.model.Resource; +import org.apache.jena.rdf.model.ResourceFactory; + +/** + * NEPOMUK File Ontology vocabulary. + * + * @author Martynas Jusevičius {@literal } + */ +public final class NFO +{ + + /** Namespace URI */ + public static final String NS = "http://www.semanticdesktop.org/ontologies/2007/03/22/nfo#"; + + private NFO() { } + + /** nfo:FileDataObject class */ + public static final Resource FileDataObject = ResourceFactory.createResource(NS + "FileDataObject"); + + /** nfo:fileName property */ + public static final Property fileName = ResourceFactory.createProperty(NS + "fileName"); + +} diff --git a/cli/src/main/java/com/atomgraph/linkeddatahub/cli/vocab/SD.java b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/vocab/SD.java new file mode 100644 index 0000000000..4bec9d26fe --- /dev/null +++ b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/vocab/SD.java @@ -0,0 +1,50 @@ +/* + * 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.vocab; + +import org.apache.jena.rdf.model.Property; +import org.apache.jena.rdf.model.Resource; +import org.apache.jena.rdf.model.ResourceFactory; + +/** + * SPARQL 1.1 Service Description vocabulary. + * + * @author Martynas Jusevičius {@literal } + */ +public final class SD +{ + + /** Namespace URI */ + public static final String NS = "http://www.w3.org/ns/sparql-service-description#"; + + private SD() { } + + /** sd:Service class */ + public static final Resource Service = ResourceFactory.createResource(NS + "Service"); + /** sd:SPARQL11Query language */ + public static final Resource SPARQL11Query = ResourceFactory.createResource(NS + "SPARQL11Query"); + /** sd:SPARQL11Update language */ + public static final Resource SPARQL11Update = ResourceFactory.createResource(NS + "SPARQL11Update"); + + /** sd:endpoint property */ + public static final Property endpoint = ResourceFactory.createProperty(NS + "endpoint"); + /** sd:supportedLanguage property */ + public static final Property supportedLanguage = ResourceFactory.createProperty(NS + "supportedLanguage"); + /** sd:name property */ + public static final Property name = ResourceFactory.createProperty(NS + "name"); + +} diff --git a/cli/src/main/java/com/atomgraph/linkeddatahub/cli/vocab/SP.java b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/vocab/SP.java new file mode 100644 index 0000000000..6634441f9b --- /dev/null +++ b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/vocab/SP.java @@ -0,0 +1,46 @@ +/* + * 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.vocab; + +import org.apache.jena.rdf.model.Property; +import org.apache.jena.rdf.model.Resource; +import org.apache.jena.rdf.model.ResourceFactory; + +/** + * SPIN SPARQL syntax vocabulary. + * + * @author Martynas Jusevičius {@literal } + */ +public final class SP +{ + + /** Namespace URI */ + public static final String NS = "http://spinrdf.org/sp#"; + + private SP() { } + + /** sp:Construct class */ + public static final Resource Construct = ResourceFactory.createResource(NS + "Construct"); + /** sp:Select class */ + public static final Resource Select = ResourceFactory.createResource(NS + "Select"); + + /** sp:text property */ + public static final Property text = ResourceFactory.createProperty(NS + "text"); + /** sp:arg1 property */ + public static final Property arg1 = ResourceFactory.createProperty(NS + "arg1"); + +} diff --git a/cli/src/main/java/com/atomgraph/linkeddatahub/cli/vocab/SPIN.java b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/vocab/SPIN.java new file mode 100644 index 0000000000..a4cbec33d3 --- /dev/null +++ b/cli/src/main/java/com/atomgraph/linkeddatahub/cli/vocab/SPIN.java @@ -0,0 +1,42 @@ +/* + * 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.vocab; + +import org.apache.jena.rdf.model.Property; +import org.apache.jena.rdf.model.ResourceFactory; + +/** + * SPIN modeling vocabulary. + * + * @author Martynas Jusevičius {@literal } + */ +public final class SPIN +{ + + /** Namespace URI */ + public static final String NS = "http://spinrdf.org/spin#"; + + private SPIN() { } + + /** spin:query property */ + public static final Property query = ResourceFactory.createProperty(NS + "query"); + /** spin:constructor property */ + public static final Property constructor = ResourceFactory.createProperty(NS + "constructor"); + /** spin:constraint property */ + public static final Property constraint = ResourceFactory.createProperty(NS + "constraint"); + +} diff --git a/cli/src/test/java/com/atomgraph/linkeddatahub/cli/CommandParsingTest.java b/cli/src/test/java/com/atomgraph/linkeddatahub/cli/CommandParsingTest.java new file mode 100644 index 0000000000..f42ccf76d4 --- /dev/null +++ b/cli/src/test/java/com/atomgraph/linkeddatahub/cli/CommandParsingTest.java @@ -0,0 +1,116 @@ +/* + * 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; + +import java.io.PrintWriter; +import java.io.Writer; +import java.net.URI; +import java.util.List; +import org.junit.jupiter.api.Test; +import picocli.CommandLine; +import picocli.CommandLine.ParseResult; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Tests for the command tree and argument parsing. + */ +public class CommandParsingTest +{ + + static CommandLine commandLine() + { + CommandLine cmd = new CommandLine(new LDH()); + cmd.setOut(new PrintWriter(Writer.nullWriter())); + cmd.setErr(new PrintWriter(Writer.nullWriter())); + return cmd; + } + + @Test + public void commandTreeMirrorsScriptLayout() + { + 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"). + forEach(name -> assertTrue(root.getSubcommands().containsKey(name), name)); + + CommandLine admin = root.getSubcommands().get("admin"); + List.of("ontologies", "acl", "packages", "clear-ontology", "add-ontology-import"). + 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("install-package", "uninstall-package"). + forEach(name -> assertTrue(admin.getSubcommands().get("packages").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("add-csv-import", "add-rdf-import", "import-csv", "import-rdf"). + forEach(name -> assertTrue(root.getSubcommands().get("imports").getSubcommands().containsKey(name), name)); + } + + @Test + public void missingRequiredOptionIsUsageError() + { + assertEquals(CommandLine.ExitCode.USAGE, commandLine().execute("create-item", "--container", "https://localhost:4443/some/")); + } + + @Test + public void unknownOptionIsUsageError() + { + assertEquals(CommandLine.ExitCode.USAGE, commandLine().execute("get", "--bogus")); + } + + @Test + public void bareGroupCommandIsUsageError() + { + assertEquals(CommandLine.ExitCode.USAGE, commandLine().execute("admin")); + assertEquals(CommandLine.ExitCode.USAGE, commandLine().execute("admin", "acl")); + } + + @Test + public void repeatableOptionsAccumulate() + { + ParseResult parseResult = commandLine().parseArgs("admin", "acl", "create-group", + "-f", "cert.p12", "-p", "secret", "-b", "https://admin.localhost:4443/", + "--name", "Editors", + "--member", "https://localhost:4443/acl/agents/a/#this", + "--member", "https://localhost:4443/acl/agents/b/#this"); + + ParseResult createGroup = parseResult.subcommand().subcommand().subcommand(); + List members = createGroup.matchedOption("--member").getValue(); + assertEquals(2, members.size()); + } + + @Test + public void missingCertOptionsFailValidationAtExecutionTime() + { + // cert options have env-var defaults, so they are validated at execution time, not parse time + assertNotNull(commandLine().parseArgs("delete", "https://localhost:4443/some/")); + assertEquals(CommandLine.ExitCode.USAGE, commandLine().execute("delete", "https://localhost:4443/some/")); + } + +} diff --git a/cli/src/test/java/com/atomgraph/linkeddatahub/cli/command/AddFileMultiPartTest.java b/cli/src/test/java/com/atomgraph/linkeddatahub/cli/command/AddFileMultiPartTest.java new file mode 100644 index 0000000000..a40c4ae8f7 --- /dev/null +++ b/cli/src/test/java/com/atomgraph/linkeddatahub/cli/command/AddFileMultiPartTest.java @@ -0,0 +1,71 @@ +/* + * 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 java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import org.glassfish.jersey.media.multipart.FormDataBodyPart; +import org.glassfish.jersey.media.multipart.FormDataMultiPart; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * Tests the RDF/POST multipart encoding of {@link AddFile}: the field order is positional, + * each pu must immediately precede its ol/ou value. + */ +public class AddFileMultiPartTest +{ + + @TempDir + Path tempDir; + + @Test + public void fieldOrderIsPositional() throws Exception + { + Path file = tempDir.resolve("data.csv"); + Files.writeString(file, "a,b\n1,2\n"); + + try (FormDataMultiPart multiPart = AddFile.buildMultiPart(file, "text/csv", "Data", null)) + { + List names = multiPart.getBodyParts().stream(). + map(part -> ((FormDataBodyPart)part).getName()). + toList(); + + assertEquals(List.of("rdf", "sb", "pu", "ol", "pu", "ol", "pu", "ou"), names); + assertEquals("text/csv", multiPart.getBodyParts().get(3).getMediaType().toString()); + } + } + + @Test + public void descriptionAppendsTrailingPair() throws Exception + { + Path file = tempDir.resolve("data.csv"); + Files.writeString(file, "a,b\n1,2\n"); + + try (FormDataMultiPart multiPart = AddFile.buildMultiPart(file, "text/csv", "Data", "Description")) + { + List names = multiPart.getBodyParts().stream(). + map(part -> ((FormDataBodyPart)part).getName()). + toList(); + + assertEquals(List.of("rdf", "sb", "pu", "ol", "pu", "ol", "pu", "ou", "pu", "ol"), names); + } + } + +} diff --git a/cli/src/test/java/com/atomgraph/linkeddatahub/cli/command/ModelBuildersTest.java b/cli/src/test/java/com/atomgraph/linkeddatahub/cli/command/ModelBuildersTest.java new file mode 100644 index 0000000000..30a4b84cc3 --- /dev/null +++ b/cli/src/test/java/com/atomgraph/linkeddatahub/cli/command/ModelBuildersTest.java @@ -0,0 +1,240 @@ +/* + * 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.command.admin.acl.CreateAuthorization; +import com.atomgraph.linkeddatahub.cli.command.admin.acl.CreateGroup; +import com.atomgraph.linkeddatahub.cli.command.admin.ontologies.CreateOntology; +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 com.atomgraph.linkeddatahub.cli.vocab.ACL; +import com.atomgraph.linkeddatahub.cli.vocab.SP; +import java.io.StringReader; +import java.net.URI; +import java.util.List; +import org.apache.jena.rdf.model.Model; +import org.apache.jena.rdf.model.ModelFactory; +import org.apache.jena.riot.Lang; +import org.apache.jena.riot.RDFParser; +import org.apache.jena.vocabulary.RDF; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Compares command model builders against the Turtle bodies of the original shell scripts. + */ +public class ModelBuildersTest +{ + + private static final URI TARGET = URI.create("https://localhost:4443/some/"); + private static final String PREFIXES = """ + @prefix dh: . + @prefix ldh: . + @prefix rdf: . + @prefix dct: . + @prefix spin: . + @prefix sp: . + @prefix ac: . + @prefix acl: . + @prefix sd: . + @prefix owl: . + @prefix rdfs: . + @prefix foaf: . + """; + + static Model parse(String turtle) + { + Model model = ModelFactory.createDefaultModel(); + RDFParser.create().source(new StringReader(PREFIXES + turtle)).lang(Lang.TURTLE).base(TARGET.toString()).parse(model); + return model; + } + + static void assertIsomorphic(Model expected, Model actual) + { + assertTrue(actual.isIsomorphicWith(expected), + "Expected:\n" + expected.toString() + "\nActual:\n" + actual.toString()); + } + + @Test + public void createItem() + { + URI doc = URI.create("https://localhost:4443/some/my-item/"); + + assertIsomorphic(parse(""" + a dh:Item ; + dct:title "My item" ; + dct:description "Desc" . + """), + CreateItem.buildModel(doc, "My item", "Desc")); + } + + @Test + public void createContainerDefaultChildrenView() + { + assertIsomorphic(parse(""" + <> a dh:Container ; + dct:title "Some" ; + rdf:_1 [ a ldh:Object ; rdf:value ldh:ChildrenView ] . + """), + CreateContainer.buildModel(TARGET, "Some", null, null, null)); + } + + @Test + public void createContainerWithMode() + { + assertIsomorphic(parse(""" + <> a dh:Container ; + dct:title "Some" ; + rdf:_1 [ a ldh:Object ; rdf:value [ a ldh:View ; spin:query ldh:SelectChildren ; ac:mode ] ] . + """), + CreateContainer.buildModel(TARGET, "Some", null, null, URI.create("https://w3id.org/atomgraph/client#GridMode"))); + } + + @Test + public void createContainerWithBlock() + { + assertIsomorphic(parse(""" + <> a dh:Container ; + dct:title "Some" ; + rdf:_1 . + """), + CreateContainer.buildModel(TARGET, "Some", null, URI.create("https://localhost:4443/some/#block"), null)); + } + + @Test + public void addViewWithURIAndMode() + { + assertIsomorphic(parse(""" + <#view> a ldh:View ; + spin:query ; + dct:title "View" ; + ac:mode . + """), + AddView.buildModel(TARGET, "#view", URI.create("https://localhost:4443/queries/q/#this"), "View", null, + URI.create("https://w3id.org/atomgraph/client#GridMode"))); + } + + @Test + public void addConstructQuery() + { + assertIsomorphic(parse(""" + _:q a sp:Construct ; + dct:title "Query" ; + sp:text \"""CONSTRUCT { ?s ?p ?o } WHERE { ?s ?p ?o }\""" ; + ldh:service . + """), + AddConstruct.buildModel(TARGET, null, SP.Construct, "Query", "CONSTRUCT { ?s ?p ?o } WHERE { ?s ?p ?o }", + URI.create("https://localhost:4443/services/s/#this"), null)); + } + + @Test + public void createOntology() + { + URI doc = URI.create("https://admin.localhost:4443/ontologies/my-ont/"); + + assertIsomorphic(parse(""" + @base . + _:ontology a owl:Ontology ; + rdfs:label "My ontology" ; + rdfs:comment "Comment" . + <> a dh:Item ; + foaf:primaryTopic _:ontology ; + dct:title "My ontology" . + """), + CreateOntology.buildModel(doc, null, "My ontology", "Comment")); + } + + @Test + public void createGroup() + { + URI doc = URI.create("https://admin.localhost:4443/acl/groups/editors/"); + + assertIsomorphic(parse(""" + @base . + _:group a foaf:Group ; + foaf:name "Editors" ; + foaf:member , . + <> a dh:Item ; + foaf:primaryTopic _:group ; + dct:title "Editors" . + """), + CreateGroup.buildModel(doc, null, "Editors", null, + List.of(URI.create("https://localhost:4443/acl/agents/a/#this"), URI.create("https://localhost:4443/acl/agents/b/#this")))); + } + + @Test + public void createAuthorization() + { + URI doc = URI.create("https://admin.localhost:4443/acl/authorizations/auth/"); + + assertIsomorphic(parse(""" + @base . + _:auth a acl:Authorization ; + rdfs:label "Auth" ; + acl:agent ; + acl:accessTo ; + acl:mode acl:Read, acl:Write . + <> a dh:Item ; + foaf:primaryTopic _:auth ; + dct:title "Auth" . + """), + CreateAuthorization.buildModel(doc, null, "Auth", null, + List.of(URI.create("https://localhost:4443/acl/agents/a/#this")), List.of(), List.of(), + List.of(URI.create("https://localhost:4443/some/")), List.of(), + List.of(ACL.Read, ACL.Write))); + } + + @Test + public void addCSVImport() + { + assertIsomorphic(parse(""" + _:import a ldh:CSVImport ; + dct:title "Cities" ; + spin:query ; + ldh:file ; + ldh:delimiter "," . + """), + AddCSVImport.buildModel(TARGET, null, "Cities", URI.create("https://localhost:4443/some/#query"), + URI.create("https://localhost:4443/uploads/abc"), ",", null)); + } + + @Test + public void addRDFImportWithGraph() + { + assertIsomorphic(parse(""" + <#import> a ldh:RDFImport ; + dct:title "Data" ; + ldh:file ; + sd:name . + """), + AddRDFImport.buildModel(TARGET, "#import", "Data", URI.create("https://localhost:4443/uploads/abc"), + null, URI.create("https://localhost:4443/graphs/g/"), null)); + } + + @Test + public void addXHTMLBlock() + { + assertIsomorphic(parse(""" + <> rdf:_4 _:block . + _:block a ldh:XHTML ; + rdf:value "

Hello

"^^rdf:XMLLiteral . + """), + AddXHTMLBlock.buildModel(TARGET, RDF.li(4), null, "

Hello

", null, null)); + } + +} diff --git a/cli/src/test/java/com/atomgraph/linkeddatahub/cli/http/ClientFactoryTest.java b/cli/src/test/java/com/atomgraph/linkeddatahub/cli/http/ClientFactoryTest.java new file mode 100644 index 0000000000..9fd10ba3d6 --- /dev/null +++ b/cli/src/test/java/com/atomgraph/linkeddatahub/cli/http/ClientFactoryTest.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.http; + +import java.nio.file.Path; +import java.nio.file.Paths; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +/** + * Tests for {@link ClientFactory}. + */ +public class ClientFactoryTest +{ + + static Path keyStorePath() throws Exception + { + return Paths.get(ClientFactoryTest.class.getResource("/test-keystore.p12").toURI()); + } + + @Test + public void createsClientFromPKCS12Keystore() throws Exception + { + assertNotNull(ClientFactory.createClient(keyStorePath(), "changeit")); + } + + @Test + public void failsCleanlyOnWrongPassword() throws Exception + { + Path keyStore = keyStorePath(); + + assertThrows(IllegalArgumentException.class, () -> ClientFactory.createClient(keyStore, "wrong")); + } + + @Test + public void failsCleanlyOnMissingFile() + { + assertThrows(IllegalArgumentException.class, () -> ClientFactory.createClient(Path.of("/nonexistent.p12"), "changeit")); + } + +} 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 new file mode 100644 index 0000000000..f7608273a5 --- /dev/null +++ b/cli/src/test/java/com/atomgraph/linkeddatahub/cli/sparql/UpdatesTest.java @@ -0,0 +1,85 @@ +/* + * 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.sparql; + +import java.net.URI; +import org.apache.jena.query.Syntax; +import org.apache.jena.update.UpdateFactory; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Tests for {@link Updates}: every template must be valid standard SPARQL 1.1. + */ +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/"); + + @Test + public void insertOntologyImportIsValidSPARQL11() + { + String update = Updates.insertOntologyImport(DOC, URI.create("https://example.org/ontology#")); + + assertDoesNotThrow(() -> UpdateFactory.create(update, Syntax.syntaxSPARQL_11)); + assertTrue(update.contains("")); + } + + @Test + public void insertGroupMemberIsValidSPARQL11() + { + String update = Updates.insertGroupMember(DOC, URI.create("https://localhost:4443/agents/x/#this")); + + assertDoesNotThrow(() -> UpdateFactory.create(update, Syntax.syntaxSPARQL_11)); + assertTrue(update.contains("")); + } + + @Test + public void removeBlockWithoutBlockKeepsVariable() + { + String update = Updates.removeBlock(DOC, null); + + assertDoesNotThrow(() -> UpdateFactory.create(update, Syntax.syntaxSPARQL_11)); + assertTrue(update.contains("?block")); + } + + @Test + public void removeBlockWithBlockInjectsIRI() + { + String update = Updates.removeBlock(DOC, URI.create("https://localhost:4443/some/#block")); + + assertDoesNotThrow(() -> UpdateFactory.create(update, Syntax.syntaxSPARQL_11)); + assertTrue(update.contains("")); + assertFalse(update.contains("?block")); + } + + @Test + public void makePublicIsValidSPARQL11() + { + String update = Updates.makePublic(BASE, ADMIN_BASE); + + assertDoesNotThrow(() -> UpdateFactory.create(update, Syntax.syntaxSPARQL_11)); + assertTrue(update.contains("")); + assertTrue(update.contains("")); + assertTrue(update.contains("")); + } + +} diff --git a/cli/src/test/java/com/atomgraph/linkeddatahub/cli/util/SequenceNumbersTest.java b/cli/src/test/java/com/atomgraph/linkeddatahub/cli/util/SequenceNumbersTest.java new file mode 100644 index 0000000000..275030c711 --- /dev/null +++ b/cli/src/test/java/com/atomgraph/linkeddatahub/cli/util/SequenceNumbersTest.java @@ -0,0 +1,74 @@ +/* + * 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 org.apache.jena.rdf.model.Model; +import org.apache.jena.rdf.model.ModelFactory; +import org.apache.jena.rdf.model.Resource; +import org.apache.jena.vocabulary.RDF; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * Tests for {@link SequenceNumbers}. + */ +public class SequenceNumbersTest +{ + + private static final String DOC = "https://localhost:4443/some/"; + + @Test + public void returnsFirstMembershipPropertyOnEmptyModel() + { + Model model = ModelFactory.createDefaultModel(); + + assertEquals(RDF.li(1), SequenceNumbers.nextSequenceProperty(model, model.createResource(DOC))); + } + + @Test + public void returnsMaxPlusOneWithGaps() + { + Model model = ModelFactory.createDefaultModel(); + Resource doc = model.createResource(DOC); + doc.addProperty(RDF.li(1), model.createResource()); + doc.addProperty(RDF.li(2), model.createResource()); + doc.addProperty(RDF.li(7), model.createResource()); + + assertEquals(RDF.li(8), SequenceNumbers.nextSequenceProperty(model, doc)); + } + + @Test + public void ignoresOtherSubjects() + { + Model model = ModelFactory.createDefaultModel(); + model.createResource("https://localhost:4443/other/").addProperty(RDF.li(5), model.createResource()); + + assertEquals(RDF.li(1), SequenceNumbers.nextSequenceProperty(model, model.createResource(DOC))); + } + + @Test + public void ignoresNonNumericSuffixes() + { + Model model = ModelFactory.createDefaultModel(); + Resource doc = model.createResource(DOC); + doc.addProperty(model.createProperty(RDF.getURI() + "_x"), model.createResource()); + doc.addProperty(RDF.li(3), model.createResource()); + + assertEquals(RDF.li(4), SequenceNumbers.nextSequenceProperty(model, doc)); + } + +} diff --git a/cli/src/test/java/com/atomgraph/linkeddatahub/cli/util/URIRewriterTest.java b/cli/src/test/java/com/atomgraph/linkeddatahub/cli/util/URIRewriterTest.java new file mode 100644 index 0000000000..222a90863d --- /dev/null +++ b/cli/src/test/java/com/atomgraph/linkeddatahub/cli/util/URIRewriterTest.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 for {@link URIRewriter}. + */ +public class URIRewriterTest +{ + + @Test + public void rewriteReplacesOriginKeepingPathQueryFragment() + { + assertEquals(URI.create("https://localhost:8443/a%20b/c/?d=e#f"), + URIRewriter.rewrite(URI.create("https://linkeddatahub.com/a%20b/c/?d=e#f"), URI.create("https://localhost:8443"))); + } + + @Test + public void rewriteIgnoresProxyPath() + { + assertEquals(URI.create("https://localhost:8443/some/"), + URIRewriter.rewrite(URI.create("https://linkeddatahub.com:4443/some/"), URI.create("https://localhost:8443/ignored/"))); + } + + @Test + public void rewriteRejectsRelativeURI() + { + assertThrows(IllegalArgumentException.class, + () -> URIRewriter.rewrite(URI.create("/relative/path"), URI.create("https://localhost:8443"))); + } + + @Test + public void adminBasePrefixesHostWithAdminSubdomain() + { + assertEquals(URI.create("https://admin.localhost:4443/"), URIRewriter.adminBase(URI.create("https://localhost:4443/"))); + } + + @Test + public void encodeSlugKeepsUnreservedCharacters() + { + assertEquals("abc-._~123", URIRewriter.encodeSlug("abc-._~123")); + } + + @Test + public void encodeSlugEncodesReservedAndNonASCII() + { + assertEquals("a%20b%2F%C4%87", URIRewriter.encodeSlug("a b/ć")); + } + + @Test + public void childURIAppendsEncodedSlugAndSlash() + { + assertEquals(URI.create("https://localhost:4443/some/my%20item/"), + URIRewriter.childURI(URI.create("https://localhost:4443/some/"), "my item")); + } + +} diff --git a/cli/src/test/resources/test-keystore.p12 b/cli/src/test/resources/test-keystore.p12 new file mode 100644 index 0000000000..f50715e253 Binary files /dev/null and b/cli/src/test/resources/test-keystore.p12 differ diff --git a/docker-compose.yml b/docker-compose.yml index 02b5f52033..dd1883586b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -107,6 +107,10 @@ services: fuseki-admin: image: atomgraph/fuseki:6.1.0 user: root # otherwise fuseki user does not have permissions to the mounted folder which is owner by root + mem_limit: 1536m # leave headroom above heap for TDB mmap/native memory + restart: on-failure + environment: + - JAVA_OPTIONS=-Xmx768m -Xms768m expose: - 3030 volumes: @@ -116,6 +120,10 @@ services: fuseki-end-user: image: atomgraph/fuseki:6.1.0 user: root # otherwise the fuseki user does not have permissions to the mounted folder which is owner by root + mem_limit: 3072m # leave headroom above heap for TDB mmap/native memory + restart: on-failure + environment: + - JAVA_OPTIONS=-Xmx1536m -Xms1536m expose: - 3030 volumes: @@ -384,28 +392,24 @@ configs: return (pass); } - if (req.http.Client-Cert) { - # Authenticated HTML is user-specific → never cache - if (req.http.Accept ~ "text/html" || - req.http.Accept ~ "application/xhtml+xml") { - return (pass); - } - - # Conditional requests must reach backend for validation - if (req.http.If-Match || req.http.If-None-Match || - req.http.If-Modified-Since || req.http.If-Unmodified-Since) { - return (pass); - } - - # /access endpoint returns agent-specific group memberships - if (req.url ~ "^/access") { - return (pass); - } + # Delegated requests carry user identity in the On-Behalf-Of header. + # The backend response echoes that identity in the Link header + # (acl#agent). The cache key does not include the asserted identity, + # so caching would let a later anonymous request to the same + # URL+Accept read back the previous agent's WebID and ACL grant. + if (req.http.On-Behalf-Of) { + return (pass); + } - # SPARQL referencing /acl/agents/ depends on agent identity → don't cache - if (req.url ~ "%2Facl%2Fagents%2F") { - return (pass); - } + # Authenticated responses get acl#agent stamped into the Link header by + # the backend, regardless of representation (HTML, RDF/XML, Turtle, + # JSON-LD, SPARQL results, …). The URL-keyed cache slot ignores the + # asserting identity, so any such response would leak to anonymous + # readers of the same URL. /static/* is the only safely-shared path — + # it's served by Tomcat's default servlet (web.xml:365-371), bypasses + # Jersey, and carries no identity-bearing headers. + if (req.http.Client-Cert && req.url !~ "^/static/") { + return (pass); } if (req.http.Cookie) { diff --git a/http-tests/document-hierarchy/GET-namespace-forClass-rdfs.sh b/http-tests/document-hierarchy/GET-namespace-forClass-rdfs.sh new file mode 100644 index 0000000000..e363dfbcf4 --- /dev/null +++ b/http-tests/document-hierarchy/GET-namespace-forClass-rdfs.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash +set -euo pipefail + +initialize_dataset "$END_USER_BASE_URL" "$TMP_END_USER_DATASET" "$END_USER_ENDPOINT_URL" +initialize_dataset "$ADMIN_BASE_URL" "$TMP_ADMIN_DATASET" "$ADMIN_ENDPOINT_URL" +purge_cache "$END_USER_VARNISH_SERVICE" +purge_cache "$ADMIN_VARNISH_SERVICE" +purge_cache "$FRONTEND_VARNISH_SERVICE" + +# sp:Describe is declared only as rdfs:Class (not owl:Class) in sp.ttl. +# OntologyFilter must promote rdfs:Class to owl:Class during materialization so +# that OWL2 profiles recognise third-party vocab terms and return their SPIN constructors. + +response=$(curl -k -f -s \ + -G \ + -E "$OWNER_CERT_FILE":"$OWNER_CERT_PWD" \ + -H "Accept: application/rdf+xml" \ + --data-urlencode "forClass=http://spinrdf.org/sp#Describe" \ + "${END_USER_BASE_URL}ns") + +# response must be non-empty: sp:Describe must be recognised as an OntClass +echo "$response" | grep -q "http://spinrdf.org/sp#Describe" diff --git a/http-tests/document-hierarchy/PATCH-blank-node-skolemized.sh b/http-tests/document-hierarchy/PATCH-blank-node-skolemized.sh new file mode 100755 index 0000000000..fec9f83436 --- /dev/null +++ b/http-tests/document-hierarchy/PATCH-blank-node-skolemized.sh @@ -0,0 +1,57 @@ +#!/usr/bin/env bash +set -euo pipefail + +initialize_dataset "$END_USER_BASE_URL" "$TMP_END_USER_DATASET" "$END_USER_ENDPOINT_URL" +initialize_dataset "$ADMIN_BASE_URL" "$TMP_ADMIN_DATASET" "$ADMIN_ENDPOINT_URL" +purge_cache "$END_USER_VARNISH_SERVICE" +purge_cache "$ADMIN_VARNISH_SERVICE" +purge_cache "$FRONTEND_VARNISH_SERVICE" + +# add agent to the writers group + +add-agent-to-group.sh \ + -f "$OWNER_CERT_FILE" \ + -p "$OWNER_CERT_PWD" \ + --agent "$AGENT_URI" \ + "${ADMIN_BASE_URL}acl/groups/writers/" + +# PATCH the root with an INSERT that introduces a blank node. +# Use rdf:_99 to avoid colliding with existing rdf:_1..rdf:_8 in the test dataset. +# Expected: 204 No Content; the blank node is skolemized to a hash URI before persisting. + +update=$(cat < +PREFIX dct: + +INSERT +{ + <${END_USER_BASE_URL}> rdf:_99 _:bnode0 . + _:bnode0 dct:title "Blank node title" +} +WHERE +{} +EOF +) + +curl -k -w "%{http_code}\n" -o /dev/null -f -s \ + -E "$AGENT_CERT_FILE":"$AGENT_CERT_PWD" \ + -X PATCH \ + -H "Content-Type: application/sparql-update" \ + "$END_USER_BASE_URL" \ + --data-binary "$update" \ +| grep -q "$STATUS_NO_CONTENT" + +# fetch the persisted graph and verify the blank node was skolemized to a hash URI + +response=$(curl -k -f -s -G \ + -E "$AGENT_CERT_FILE":"$AGENT_CERT_PWD" \ + -H "Accept: application/n-triples" \ + "$END_USER_BASE_URL") + +rdf_99_line=$(echo "$response" | grep -E "^<${END_USER_BASE_URL}> " || true) + +[ -n "$rdf_99_line" ] || exit 1 + +# object of rdf:_99 must be a URI (<...>), not a blank node label (_:...) +! echo "$rdf_99_line" | grep -qE '_:[A-Za-z0-9]+ \.$' +echo "$rdf_99_line" | grep -qE '<\S+> \.$' diff --git a/http-tests/proxy/GET-proxied-no-cache-poisoning.sh b/http-tests/proxy/GET-proxied-no-cache-poisoning.sh new file mode 100755 index 0000000000..89337d2383 --- /dev/null +++ b/http-tests/proxy/GET-proxied-no-cache-poisoning.sh @@ -0,0 +1,78 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Regression: ProxyRequestFilter's server-side fetch attaches On-Behalf-Of +# (via WebIDDelegationFilter), and the backend response carries the asserted +# agent's WebID in the Link header (acl#agent). varnish-frontend must not +# cache that response under a URL-keyed entry — otherwise a subsequent +# anonymous request to the same URL+Accept replays the cached 200 and reads +# back the previous agent's identity (and inherits whatever ACL grant they +# had). + +purge_cache "$END_USER_VARNISH_SERVICE" +purge_cache "$ADMIN_VARNISH_SERVICE" +purge_cache "$FRONTEND_VARNISH_SERVICE" + +# Step A: authenticated owner fires a proxy request from the end-user +# dataspace to the admin dataspace. This triggers WebIDDelegationFilter → +# On-Behalf-Of on the server-side hop into varnish-frontend. + +curl -k -f -s -o /dev/null \ + -E "$OWNER_CERT_FILE":"$OWNER_CERT_PWD" \ + -G \ + -H 'Accept: application/rdf+xml' \ + --data-urlencode "uri=${ADMIN_BASE_URL}" \ + "${END_USER_BASE_URL}" + +# Step B: anonymous direct request to the admin URL with the same Accept. +# If the cache was poisoned in Step A, this returns 200 with the owner's +# WebID in the Link header. Expected after the fix: varnish-frontend should +# pass on On-Behalf-Of and store nothing, so this goes to the backend +# anonymously and gets 403. + +response=$(curl -k -s -i -H 'Accept: application/rdf+xml' "${ADMIN_BASE_URL}") + +status=$(printf '%s\n' "$response" | awk 'NR==1{print $2}' | tr -d '\r') +link_leak=$(printf '%s\n' "$response" | tr -d '\r' | grep -i '^link:' | grep -c 'acl#agent' || true) + +if [ "$status" != "$STATUS_FORBIDDEN" ]; then + echo "Step B: expected $STATUS_FORBIDDEN, got $status" + exit 1 +fi + +if [ "$link_leak" != "0" ]; then + echo "Step B: anonymous response leaks acl#agent (cache poisoning)" + exit 1 +fi + +# Step C: authenticated owner fetches the admin URL directly with cert at TLS, +# Accept: application/rdf+xml. The Client-Cert header reaches varnish-frontend +# (nginx-forwarded). The backend stamps acl#agent into the Link header for the +# authenticated 200. varnish-frontend must NOT cache this response — its hash +# key ignores identity, so a subsequent anonymous request would replay the 200. + +purge_cache "$FRONTEND_VARNISH_SERVICE" + +curl -k -f -s -o /dev/null \ + -E "$OWNER_CERT_FILE":"$OWNER_CERT_PWD" \ + -H 'Accept: application/rdf+xml' \ + "${ADMIN_BASE_URL}" + +# Step D: anonymous direct fetch of the same URL. With the fix in place +# (Client-Cert + non-/static/ path → pass in vcl_recv), Step C didn't store +# anything, so this reaches the backend anonymously and gets 403. + +response=$(curl -k -s -i -H 'Accept: application/rdf+xml' "${ADMIN_BASE_URL}") + +status=$(printf '%s\n' "$response" | awk 'NR==1{print $2}' | tr -d '\r') +link_leak=$(printf '%s\n' "$response" | tr -d '\r' | grep -i '^link:' | grep -c 'acl#agent' || true) + +if [ "$status" != "$STATUS_FORBIDDEN" ]; then + echo "Step D: expected $STATUS_FORBIDDEN, got $status" + exit 1 +fi + +if [ "$link_leak" != "0" ]; then + echo "Step D: anonymous response leaks acl#agent (cache poisoning)" + exit 1 +fi diff --git a/platform/context.xsl b/platform/context.xsl index 529c95d5d7..ff80b8a232 100644 --- a/platform/context.xsl +++ b/platform/context.xsl @@ -17,7 +17,6 @@ xmlns:orcid="&orcid;" - @@ -66,9 +65,6 @@ xmlns:orcid="&orcid;" - - - diff --git a/platform/entrypoint.sh b/platform/entrypoint.sh index 9296588602..d5c81dd76c 100755 --- a/platform/entrypoint.sh +++ b/platform/entrypoint.sh @@ -1043,10 +1043,6 @@ if [ -n "$PROXY_PORT" ]; then PROXY_PORT_PARAM="--stringparam ldhc:proxyPort '$PROXY_PORT' " fi -if [ -n "$CACHE_MODEL_LOADS" ]; then - CACHE_MODEL_LOADS_PARAM="--stringparam a:cacheModelLoads '$CACHE_MODEL_LOADS' " -fi - # stylesheet URL must be relative to the base context URL if [ -n "$STYLESHEET" ]; then STYLESHEET_PARAM="--stringparam ac:stylesheet '$STYLESHEET' " @@ -1166,7 +1162,6 @@ fi transform="xsltproc \ --output conf/Catalina/localhost/ROOT.xml \ - $CACHE_MODEL_LOADS_PARAM \ $STYLESHEET_PARAM \ $CACHE_STYLESHEET_PARAM \ $RESOLVING_UNCACHED_PARAM \ diff --git a/pom.xml b/pom.xml index 836b779e83..37bc0c6c90 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ com.atomgraph linkeddatahub - 5.5.4 + 5.6.0-SNAPSHOT ${packaging.type} AtomGraph LinkedDataHub @@ -93,6 +93,19 @@ org.glassfish.jersey.connectors jersey-apache-connector 3.1.11 + + + + commons-logging + commons-logging + + + + + + commons-codec + commons-codec + 1.22.0 org.glassfish.jersey.media @@ -112,7 +125,7 @@ com.google.guava guava - 31.1-jre + 33.6.0-jre com.atomgraph.etl.csv @@ -146,14 +159,7 @@ ${project.groupId} twirl - 1.1.0 - - - - org.slf4j - slf4j-reload4j - - + 2.0.0 org.slf4j @@ -163,13 +169,13 @@ ${project.groupId} client - 4.3.0 + 5.0.2 classes ${project.groupId} client - 4.3.0 + 5.0.2 war @@ -185,24 +191,24 @@ org.jsoup jsoup - 1.22.1 + 1.22.2 org.junit.jupiter junit-jupiter - 5.11.4 + 6.1.0 test org.mockito mockito-core - 5.12.0 + 5.18.0 test org.mockito mockito-junit-jupiter - 5.12.0 + 5.18.0 test diff --git a/src/main/java/com/atomgraph/linkeddatahub/Application.java b/src/main/java/com/atomgraph/linkeddatahub/Application.java index c9780cee50..2d429ce4f5 100644 --- a/src/main/java/com/atomgraph/linkeddatahub/Application.java +++ b/src/main/java/com/atomgraph/linkeddatahub/Application.java @@ -16,6 +16,11 @@ */ package com.atomgraph.linkeddatahub; +import com.atomgraph.client.util.jena.PrefixGraphRepository; +import com.atomgraph.client.util.StylesheetResolver; +import com.atomgraph.linkeddatahub.writer.impl.SameSiteSourceResolver; +import com.atomgraph.linkeddatahub.server.util.OntologyRepository; +import org.apache.jena.riot.RDFParser; import com.atomgraph.linkeddatahub.server.mapper.HttpHostConnectExceptionMapper; import com.atomgraph.linkeddatahub.server.mapper.InternalURLExceptionMapper; import com.atomgraph.linkeddatahub.server.mapper.MessagingExceptionMapper; @@ -26,10 +31,6 @@ import com.atomgraph.linkeddatahub.server.mapper.ForbiddenExceptionMapper; import com.atomgraph.linkeddatahub.server.mapper.auth.webid.WebIDCertificateExceptionMapper; import com.atomgraph.client.MediaTypes; -import com.atomgraph.client.locator.PrefixMapper; -import org.apache.jena.ontology.OntDocumentManager; -import org.apache.jena.util.FileManager; -import org.apache.jena.util.LocationMapper; import jakarta.annotation.PostConstruct; import jakarta.servlet.ServletConfig; import jakarta.ws.rs.core.Context; @@ -37,8 +38,6 @@ import org.apache.jena.riot.RDFFormat; import org.apache.jena.riot.RDFWriterRegistry; import com.atomgraph.client.mapper.ClientErrorExceptionMapper; -import com.atomgraph.client.util.DataManager; -import com.atomgraph.client.util.DataManagerImpl; import com.atomgraph.client.vocabulary.AC; import com.atomgraph.client.writer.function.UUID; import com.atomgraph.core.exception.ConfigurationException; @@ -49,7 +48,7 @@ import com.atomgraph.core.io.UpdateRequestProvider; import com.atomgraph.core.mapper.BadGatewayExceptionMapper; import com.atomgraph.core.provider.QueryParamProvider; -import com.atomgraph.linkeddatahub.writer.factory.DataManagerFactory; +import com.atomgraph.linkeddatahub.writer.factory.SourceResolverFactory; import com.atomgraph.server.vocabulary.LDT; import com.atomgraph.server.mapper.NotFoundExceptionMapper; import com.atomgraph.core.riot.RDFLanguages; @@ -67,7 +66,6 @@ import com.atomgraph.linkeddatahub.model.Service; import com.atomgraph.linkeddatahub.writer.factory.xslt.XsltExecutableSupplier; import com.atomgraph.linkeddatahub.writer.factory.XsltExecutableSupplierFactory; -import com.atomgraph.client.util.XsltResolver; import com.atomgraph.linkeddatahub.client.GraphStoreClient; import com.atomgraph.linkeddatahub.client.filter.ClientUriRewriteFilter; import com.atomgraph.linkeddatahub.client.filter.JSONGRDDLFilter; @@ -135,7 +133,6 @@ import com.google.common.eventbus.EventBus; import com.google.common.eventbus.Subscribe; import org.apache.jena.enhanced.BuiltinPersonalities; -import org.apache.jena.ontology.OntModelSpec; import org.apache.jena.riot.RDFParserRegistry; import org.slf4j.Logger; import java.net.URI; @@ -164,7 +161,7 @@ import javax.net.ssl.TrustManagerFactory; import jakarta.servlet.ServletContext; import javax.xml.transform.Source; -import org.apache.jena.ontology.Ontology; +import org.apache.jena.ontapi.model.OntModel; import org.apache.jena.query.Dataset; import org.apache.jena.query.Query; import org.apache.jena.query.QueryExecution; @@ -174,7 +171,6 @@ import org.slf4j.LoggerFactory; import com.atomgraph.server.mapper.SHACLConstraintViolationExceptionMapper; import com.atomgraph.server.mapper.SPINConstraintViolationExceptionMapper; -import com.atomgraph.spinrdf.vocabulary.SP; import com.apicatalog.jsonld.JsonLdError; import com.apicatalog.jsonld.JsonLdOptions; import java.io.FileOutputStream; @@ -231,7 +227,6 @@ import org.apache.jena.riot.resultset.ResultSetLang; import org.apache.jena.sparql.graph.GraphReadOnly; import org.apache.jena.vocabulary.DCTerms; -import org.apache.jena.vocabulary.LocationMappingVocab; import org.apache.jena.vocabulary.RDF; import org.glassfish.hk2.api.TypeLiteral; import org.glassfish.hk2.utilities.binding.AbstractBinder; @@ -268,17 +263,16 @@ public class Application extends ResourceConfig private final ExecutorService importThreadPool; private final ServletConfig servletConfig; private final EventBus eventBus = new EventBus(); - private final DataManager dataManager; - private final Map endUserOntModelSpecs; + private final PrefixGraphRepository repository; + private final SameSiteSourceResolver resolver; + private final Map endUserRepositories; private final MediaTypes mediaTypes; private final Client client, externalClient, importClient, noCertClient; private final Query documentTypeQuery, documentOwnerQuery, aclQuery, ownerAclQuery, webIDQuery, agentQuery, userAccountQuery, ontologyQuery; // no relative URIs private final Integer maxGetRequestSize; - private final boolean preemptiveAuth; private final Processor xsltProc = new Processor(false); private final XsltCompiler xsltComp; private final XsltExecutable xsltExec; - private final OntModelSpec ontModelSpec; private final boolean cacheStylesheet; private final boolean resolvingUncached; private final URI baseURI, uploadRoot; @@ -322,9 +316,7 @@ public Application(@Context ServletConfig servletConfig) throws URISyntaxExcepti this(servletConfig, new MediaTypes(), servletConfig.getServletContext().getInitParameter(A.maxGetRequestSize.getURI()) != null ? Integer.valueOf(servletConfig.getServletContext().getInitParameter(A.maxGetRequestSize.getURI())) : null, - servletConfig.getServletContext().getInitParameter(A.cacheModelLoads.getURI()) != null ? Boolean.parseBoolean(servletConfig.getServletContext().getInitParameter(A.cacheModelLoads.getURI())) : true, - servletConfig.getServletContext().getInitParameter(A.preemptiveAuth.getURI()) != null ? Boolean.parseBoolean(servletConfig.getServletContext().getInitParameter(A.preemptiveAuth.getURI())) : false, - new PrefixMapper(servletConfig.getServletContext().getInitParameter(AC.prefixMapping.getURI()) != null ? servletConfig.getServletContext().getInitParameter(AC.prefixMapping.getURI()) : null), + servletConfig.getServletContext().getInitParameter(AC.prefixMapping.getURI()), servletConfig.getServletContext().getInitParameter(LDHC.contextDataset.getURI()) != null ? servletConfig.getServletContext().getInitParameter(LDHC.contextDataset.getURI()) : null, com.atomgraph.client.Application.getSource(servletConfig.getServletContext(), servletConfig.getServletContext().getInitParameter(AC.stylesheet.getURI()) != null ? servletConfig.getServletContext().getInitParameter(AC.stylesheet.getURI()) : null), servletConfig.getServletContext().getInitParameter(AC.cacheStylesheet.getURI()) != null ? Boolean.parseBoolean(servletConfig.getServletContext().getInitParameter(AC.cacheStylesheet.getURI())) : false, @@ -387,9 +379,7 @@ public Application(@Context ServletConfig servletConfig) throws URISyntaxExcepti * @param servletConfig servlet config * @param mediaTypes supported media types * @param maxGetRequestSize maximum GET request size - * @param cacheModelLoads true if model loads should be cached - * @param preemptiveAuth true if HTTP Basic auth credentials should be sent preemptively - * @param locationMapper Jena's LocationMapper instance + * @param prefixMappingConfig location-mapping configuration source * @param contextDatasetURIString location of the context dataset * @param stylesheet stylesheet URI * @param cacheStylesheet true if stylesheet should be cached @@ -438,8 +428,8 @@ public Application(@Context ServletConfig servletConfig) throws URISyntaxExcepti * @param backendProxyEndUserString backend proxy URI for the end-user SPARQL service (endpoint URI rewriting + cache invalidation), or null */ public Application(final ServletConfig servletConfig, final MediaTypes mediaTypes, - final Integer maxGetRequestSize, final boolean cacheModelLoads, final boolean preemptiveAuth, - final LocationMapper locationMapper, final String contextDatasetURIString, + final Integer maxGetRequestSize, + final String prefixMappingConfig, final String contextDatasetURIString, final Source stylesheet, final boolean cacheStylesheet, final boolean resolvingUncached, final String clientKeyStoreURIString, final String clientKeyStorePassword, final String secretaryCertAlias, @@ -579,7 +569,6 @@ public Application(final ServletConfig servletConfig, final MediaTypes mediaType this.servletConfig = servletConfig; this.mediaTypes = mediaTypes; this.maxGetRequestSize = maxGetRequestSize; - this.preemptiveAuth = preemptiveAuth; this.cacheStylesheet = cacheStylesheet; this.resolvingUncached = resolvingUncached; this.enableLinkedDataProxy = enableLinkedDataProxy; @@ -687,12 +676,8 @@ public Application(final ServletConfig servletConfig, final MediaTypes mediaType } // register plain RDF/XML writer as default - RDFWriterRegistry.register(Lang.RDFXML, RDFFormat.RDFXML_PLAIN); + RDFWriterRegistry.register(Lang.RDFXML, RDFFormat.RDFXML_PLAIN); - // initialize mapping for locally stored vocabularies - LocationMapper.setGlobalLocationMapper(locationMapper); - if (log.isTraceEnabled()) log.trace("LocationMapper.get(): {}", locationMapper); - try { this.contextDataset = getDataset(servletConfig.getServletContext(), contextDatasetURI); @@ -752,7 +737,6 @@ public Application(final ServletConfig servletConfig, final MediaTypes mediaType throw new IllegalStateException(new CertificateException("Secretary certificate with alias " + secretaryCertAlias + " not a valid WebID sertificate (SNA URI is missing)")); } - SP.init(BuiltinPersonalities.model); BuiltinPersonalities.model.add(Authorization.class, AuthorizationImpl.factory); BuiltinPersonalities.model.add(Agent.class, AgentImpl.factory); BuiltinPersonalities.model.add(UserAccount.class, UserAccountImpl.factory); @@ -797,14 +781,16 @@ else if (app.hasProperty(RDF.type, LAPP.EndUserApplication)) serviceIt.close(); } - // TO-DO: config property for cacheModelLoads - endUserOntModelSpecs = new HashMap<>(); - dataManager = new DataManagerImpl(locationMapper, new HashMap<>(), GraphStoreClient.create(client, mediaTypes), cacheModelLoads, preemptiveAuth, resolvingUncached); - ontModelSpec = OntModelSpec.OWL_MEM_RDFS_INF; - ontModelSpec.setImportModelGetter(dataManager); - OntDocumentManager.getInstance().setFileManager((FileManager)dataManager); - OntDocumentManager.getInstance().setCacheModels(true); // need to re-set after changing FileManager - ontModelSpec.setDocumentManager(OntDocumentManager.getInstance()); + endUserRepositories = new ConcurrentHashMap<>(); + // global graph repository: bundled vocabularies/ontologies mapped from the prefix-mapping config + repository = new PrefixGraphRepository(GraphStoreClient.create(client, mediaTypes)); + if (prefixMappingConfig != null) + { + Model prefixMappingModel = ModelFactory.createDefaultModel(); + RDFParser.create().source(prefixMappingConfig).streamManager(repository.getStreamManager()).build().parse(prefixMappingModel); + repository.processConfig(prefixMappingModel); + } + resolver = new SameSiteSourceResolver(repository, GraphStoreClient.create(client, mediaTypes), resolvingUncached, baseURI); if (mailUser != null && mailPassword != null) // enable SMTP authentication { @@ -841,19 +827,15 @@ protected PasswordAuthentication getPasswordAuthentication() xsltProc.registerExtensionFunction(new com.atomgraph.linkeddatahub.writer.function.URLDecode()); xsltProc.registerExtensionFunction(new com.atomgraph.linkeddatahub.writer.function.SendHTTPRequest(xsltProc, client)); - Model mappingModel = locationMapper.toModel(); - ResIterator prefixedMappings = mappingModel.listResourcesWithProperty(LocationMappingVocab.prefix); try { - while (prefixedMappings.hasNext()) + for (String prefix : getRepository().getPrefixMappings().keySet()) { - Resource prefixMapping = prefixedMappings.next(); - String prefix = prefixMapping.getRequiredProperty(LocationMappingVocab.prefix).getString(); // register mapped RDF documents in the XSLT processor so that document() returns them cached, throughout multiple transformations - TreeInfo doc = xsltProc.getUnderlyingConfiguration().buildDocumentTree(dataManager.resolve("", prefix)); + TreeInfo doc = xsltProc.getUnderlyingConfiguration().buildDocumentTree(getResolver().resolve("", prefix)); xsltProc.getUnderlyingConfiguration().getGlobalDocumentPool().add(doc, prefix); } - + // register HTTPS URL of translations.rdf so it doesn't have to be requested repeatedly try (InputStream translations = servletConfig.getServletContext().getResourceAsStream(XSLTWriterBase.TRANSLATIONS_PATH)) { @@ -866,14 +848,10 @@ protected PasswordAuthentication getPasswordAuthentication() if (log.isErrorEnabled()) log.error("Error reading mapped RDF document: {}", ex); throw new IllegalStateException(ex); } - finally - { - prefixedMappings.close(); - } xsltComp = xsltProc.newXsltCompiler(); xsltComp.setParameter(new QName("ldh", LDH.base.getNameSpace(), LDH.base.getLocalName()), new XdmAtomicValue(baseURI)); - xsltComp.setURIResolver(new XsltResolver(LocationMapper.get(), new HashMap<>(), GraphStoreClient.create(client, mediaTypes), false, false, true)); // default Xerces parser does not support HTTPS + xsltComp.setURIResolver(new StylesheetResolver(client)); // resolves xsl:import to raw stylesheet sources xsltExec = xsltComp.compile(stylesheet); } catch (FileNotFoundException ex) @@ -940,8 +918,8 @@ public void init() register(new QueryProvider()); register(new QueryParamProvider()); register(new UpdateRequestProvider()); - register(new ModelXSLTWriter(getXsltExecutable(), getOntModelSpec(), getDataManager(), getMessageDigest())); // writes (X)HTML responses - register(new ResultSetXSLTWriter(getXsltExecutable(), getOntModelSpec(), getDataManager(), getMessageDigest())); // writes (X)HTML responses + register(new ModelXSLTWriter(getXsltExecutable(), getResolver(), getMessageDigest())); // writes (X)HTML responses + register(new ResultSetXSLTWriter(getXsltExecutable(), getResolver(), getMessageDigest())); // writes (X)HTML responses final com.atomgraph.linkeddatahub.Application system = this; register(new AbstractBinder() @@ -1019,7 +997,7 @@ protected void configure() @Override protected void configure() { - bindFactory(OntologyFactory.class).to(new TypeLiteral>() {}). + bindFactory(OntologyFactory.class).to(new TypeLiteral>() {}). in(RequestScoped.class); } }); @@ -1028,15 +1006,7 @@ protected void configure() @Override protected void configure() { - bindFactory(new com.atomgraph.core.factory.DataManagerFactory(getDataManager())).to(com.atomgraph.core.util.jena.DataManager.class); - } - }); - register(new AbstractBinder() - { - @Override - protected void configure() - { - bindFactory(DataManagerFactory.class).to(com.atomgraph.client.util.DataManager.class). + bindFactory(SourceResolverFactory.class).to(com.atomgraph.client.util.RDFSourceResolver.class). in(RequestScoped.class); } }); @@ -1775,43 +1745,62 @@ public EventBus getEventBus() } /** - * Gets Jena's DataManager implementation. - * - * @return data manager instance + * Returns the global graph repository (bundled vocabularies/ontologies + URI mapping + cache). + * + * @return graph repository */ - public DataManager getDataManager() + public PrefixGraphRepository getRepository() { - return dataManager; + return repository; } - + /** - * Returns a map of application URIs to ontology specifications. - * - * @return URI to ontology specification map + * Returns the global XSLT source resolver. + * + * @return source resolver */ - protected Map getEndUserOntModelSpecs() + public SameSiteSourceResolver getResolver() { - return endUserOntModelSpecs; + return resolver; } /** - * Returns ontology specification for the specified end-user application. - * + * Returns a map of application URIs to ontology repositories. + * + * @return URI to ontology repository map + */ + protected Map getEndUserRepositories() + { + return endUserRepositories; + } + + /** + * Returns the SPARQL-first ontology repository for the specified end-user application. + * * @param app end-user application resource - * @return ontology specification + * @return ontology repository */ - public OntModelSpec getOntModelSpec(EndUserApplication app) + public OntologyRepository getRepository(EndUserApplication app) { - if (!getEndUserOntModelSpecs().containsKey(app.getURI())) - { - OntModelSpec appOntModelSpec = new OntModelSpec(OntModelSpec.OWL_MEM_RDFS_INF); - appOntModelSpec.setDocumentManager(new OntDocumentManager()); - appOntModelSpec.getDocumentManager().setFileManager(new DataManagerImpl(LocationMapper.get(), new HashMap<>(), GraphStoreClient.create(getClient(), getMediaTypes()), true, isPreemptiveAuth(), isResolvingUncached())); - - getEndUserOntModelSpecs().put(app.getURI(), appOntModelSpec); - } - - return getEndUserOntModelSpecs().get(app.getURI()); + return getEndUserRepositories().computeIfAbsent(app.getURI(), uri -> createRepository(app)); + } + + /** + * Creates a fresh ontology repository for an application, seeded with the bundled + * vocabulary/ontology mappings from the global repository so that mapped URIs resolve to shipped + * files rather than being dereferenced over HTTP. + * + * @param app end-user application resource + * @return ontology repository + */ + public OntologyRepository createRepository(EndUserApplication app) + { + OntologyRepository appRepository = new OntologyRepository(app, this, GraphStoreClient.create(getClient(), getMediaTypes()), getOntologyQuery()); + // seed bundled vocabulary/ontology mappings from the global repository + getRepository().getLocationMappings().forEach(appRepository::addLocationMapping); + getRepository().getPrefixMappings().forEach(appRepository::addPrefixMapping); + + return appRepository; } /** @@ -2011,26 +2000,6 @@ public Integer getMaxGetRequestSize() return maxGetRequestSize; } - /** - * Returns true if HTTP Basic auth credentials should be sent preemptively. - * - * @return true if preemptively - */ - public boolean isPreemptiveAuth() - { - return preemptiveAuth; - } - - /** - * The default specification of ontology models. - * - * @return spec object - */ - public OntModelSpec getOntModelSpec() - { - return ontModelSpec; - } - /** * Returns Saxon's XSLT compiler. * diff --git a/src/main/java/com/atomgraph/linkeddatahub/resource/Generate.java b/src/main/java/com/atomgraph/linkeddatahub/resource/Generate.java index 34b68d81fd..27e2124ffc 100644 --- a/src/main/java/com/atomgraph/linkeddatahub/resource/Generate.java +++ b/src/main/java/com/atomgraph/linkeddatahub/resource/Generate.java @@ -42,7 +42,7 @@ import jakarta.ws.rs.core.Response.Status; import jakarta.ws.rs.core.UriBuilder; import jakarta.ws.rs.core.UriInfo; -import org.apache.jena.ontology.Ontology; +import org.apache.jena.ontapi.model.OntModel; import org.apache.jena.query.ParameterizedSparqlString; import org.apache.jena.query.Query; import org.apache.jena.query.QueryFactory; @@ -69,7 +69,7 @@ public class Generate private final UriInfo uriInfo; private final MediaTypes mediaTypes; private final Application application; - private final Ontology ontology; + private final OntModel ontology; private final Optional agentContext; private final com.atomgraph.linkeddatahub.Application system; private final ResourceContext resourceContext; @@ -88,7 +88,7 @@ public class Generate */ @Inject public Generate(@Context Request request, @Context UriInfo uriInfo, MediaTypes mediaTypes, - com.atomgraph.linkeddatahub.apps.model.Application application, Optional ontology, Optional agentContext, + com.atomgraph.linkeddatahub.apps.model.Application application, Optional ontology, Optional agentContext, com.atomgraph.linkeddatahub.Application system, @Context ResourceContext resourceContext) { if (ontology.isEmpty()) throw new InternalServerErrorException("Ontology is not specified"); @@ -134,7 +134,7 @@ public Response post(Model model) if (queryRes == null) throw new BadRequestException("Container query string (spin:query) not provided"); // Lookup query in ontology - Resource queryResource = getOntology().getOntModel().getResource(queryRes.getURI()); + Resource queryResource = getOntology().getResource(queryRes.getURI()); if (queryResource == null || !queryResource.hasProperty(SP.text)) throw new BadRequestException("Query resource not found in ontology: " + queryRes.getURI()); @@ -265,7 +265,7 @@ public Application getApplication() * * @return the ontology */ - public Ontology getOntology() + public OntModel getOntology() { return ontology; } diff --git a/src/main/java/com/atomgraph/linkeddatahub/resource/Namespace.java b/src/main/java/com/atomgraph/linkeddatahub/resource/Namespace.java index 4ccefa319f..5ce68929e5 100644 --- a/src/main/java/com/atomgraph/linkeddatahub/resource/Namespace.java +++ b/src/main/java/com/atomgraph/linkeddatahub/resource/Namespace.java @@ -31,7 +31,7 @@ import com.atomgraph.core.model.impl.dataset.ServiceImpl; import com.atomgraph.linkeddatahub.apps.model.Application; import com.atomgraph.linkeddatahub.apps.model.EndUserApplication; -import com.atomgraph.linkeddatahub.server.util.OntologyModelGetter; +import com.atomgraph.linkeddatahub.server.util.OntologyRepository; import java.net.URI; import java.util.List; import java.util.Optional; @@ -47,7 +47,7 @@ import jakarta.ws.rs.core.SecurityContext; import jakarta.ws.rs.core.UriInfo; import org.apache.jena.irix.IRIx; -import org.apache.jena.ontology.Ontology; +import org.apache.jena.ontapi.model.OntModel; import org.apache.jena.query.DatasetFactory; import org.apache.jena.query.Query; import org.apache.jena.query.QueryFactory; @@ -70,7 +70,7 @@ public class Namespace extends com.atomgraph.core.model.impl.SPARQLEndpointImpl private final URI uri; private final UriInfo uriInfo; private final Application application; - private final Ontology ontology; + private final OntModel ontology; private final com.atomgraph.linkeddatahub.Application system; /** @@ -86,10 +86,10 @@ public class Namespace extends com.atomgraph.core.model.impl.SPARQLEndpointImpl */ @Inject public Namespace(@Context Request request, @Context UriInfo uriInfo, - Application application, Optional ontology, MediaTypes mediaTypes, + Application application, Optional ontology, MediaTypes mediaTypes, @Context SecurityContext securityContext, com.atomgraph.linkeddatahub.Application system) { - super(request, new ServiceImpl(DatasetFactory.create(ontology.get().getOntModel()), mediaTypes), mediaTypes); + super(request, new ServiceImpl(DatasetFactory.create(ontology.get()), mediaTypes), mediaTypes); this.uri = uriInfo.getAbsolutePath(); this.uriInfo = uriInfo; this.application = application; @@ -128,7 +128,7 @@ public Response get(@QueryParam(QUERY) Query query, Model instances = ModelFactory.createDefaultModel(); forClasses.stream(). - map(forClass -> Optional.ofNullable(getOntology().getOntModel().getOntClass(checkURI(forClass).toString()))). + map(forClass -> Optional.ofNullable(getOntology().getOntClass(checkURI(forClass).toString()))). flatMap(Optional::stream). forEach(forClass -> new Constructor().construct(forClass, instances, getApplication().getBase().getURI())); @@ -139,10 +139,11 @@ public Response get(@QueryParam(QUERY) Query query, { // the application ontology MUST use a URI! This is the URI this ontology endpoint is deployed on by the Dispatcher class String ontologyURI = getApplication().getOntology().getURI(); - if (log.isDebugEnabled()) log.debug("Returning namespace ontology from OntDocumentManager: {}", ontologyURI); - // not returning the injected in-memory ontology because it has inferences applied to it - OntologyModelGetter modelGetter = new OntologyModelGetter(getApplication().as(EndUserApplication.class), getSystem(), getSystem().getOntModelSpec(), getSystem().getOntologyQuery()); - return getResponseBuilder(modelGetter.getModel(ontologyURI)).build(); + if (log.isDebugEnabled()) log.debug("Returning raw namespace ontology: {}", ontologyURI); + // not returning the injected in-memory ontology because it has inferences applied to it; + // a fresh, mapping-seeded repository serves the raw SPARQL-loaded ontology + OntologyRepository repository = getSystem().createRepository(getApplication().as(EndUserApplication.class)); + return getResponseBuilder(org.apache.jena.rdf.model.ModelFactory.createModelForGraph(repository.get(ontologyURI))).build(); } else throw new BadRequestException("SPARQL query string not provided"); } @@ -223,7 +224,7 @@ public Application getApplication() * * @return application ontology */ - public Ontology getOntology() + public OntModel getOntology() { return ontology; } diff --git a/src/main/java/com/atomgraph/linkeddatahub/resource/admin/ClearOntology.java b/src/main/java/com/atomgraph/linkeddatahub/resource/admin/ClearOntology.java index 202f724291..9ce40f1a68 100644 --- a/src/main/java/com/atomgraph/linkeddatahub/resource/admin/ClearOntology.java +++ b/src/main/java/com/atomgraph/linkeddatahub/resource/admin/ClearOntology.java @@ -18,9 +18,8 @@ import com.atomgraph.linkeddatahub.apps.model.AdminApplication; import com.atomgraph.linkeddatahub.apps.model.EndUserApplication; -import static com.atomgraph.linkeddatahub.server.filter.request.OntologyFilter.addDocumentModel; import com.atomgraph.linkeddatahub.server.filter.response.CacheInvalidationFilter; -import com.atomgraph.linkeddatahub.server.util.OntologyModelGetter; +import com.atomgraph.linkeddatahub.server.util.OntologyRepository; import java.net.URI; import jakarta.inject.Inject; import jakarta.ws.rs.BadRequestException; @@ -31,11 +30,7 @@ import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; import jakarta.ws.rs.core.UriBuilder; -import org.apache.jena.ontology.OntModel; -import org.apache.jena.ontology.OntModelSpec; -import org.apache.jena.rdf.model.Model; -import org.apache.jena.rdf.model.ModelFactory; -import org.apache.jena.rdf.model.Resource; +import com.atomgraph.linkeddatahub.server.filter.request.OntologyFilter; import org.glassfish.jersey.uri.UriComponent; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -82,11 +77,11 @@ public Response post(@FormParam("uri") String ontologyURI, @HeaderParam("Referer if (ontologyURI == null) throw new BadRequestException("Ontology URI not specified"); EndUserApplication endUserApp = getApplication().as(AdminApplication.class).getEndUserApplication(); // we're assuming the current app is admin - OntModelSpec ontModelSpec = new OntModelSpec(getSystem().getOntModelSpec(endUserApp)); - if (ontModelSpec.getDocumentManager().getFileManager().hasCachedModel(ontologyURI)) + OntologyRepository repository = getSystem().getRepository(endUserApp); + if (repository.isCached(ontologyURI)) { if (log.isDebugEnabled()) log.debug("Clearing ontology with URI '{}' from memory", ontologyURI); - ontModelSpec.getDocumentManager().getFileManager().removeCacheModel(ontologyURI); + repository.remove(ontologyURI); URI ontologyDocURI = UriBuilder.fromUri(ontologyURI).fragment(null).build(); // skip fragment from the ontology URI to get its graph URI // frontend proxy still uses URL-pattern BAN for direct document GETs (until Stage 3 brings xkey tagging to varnish-frontend). @@ -96,7 +91,6 @@ public Response post(@FormParam("uri") String ontologyURI, @HeaderParam("Referer { if (log.isDebugEnabled()) log.debug("Purge ontology document with URI '{}' from frontend proxy cache", ontologyDocURI); ban(frontendProxy, ontologyDocURI.toString(), false); - xkeyPurge(frontendProxy, ontologyURI); } URI adminBackendProxy = getSystem().getServiceContext(getApplication().getService()).getBackendProxy(); if (adminBackendProxy != null) @@ -116,19 +110,7 @@ public Response post(@FormParam("uri") String ontologyURI, @HeaderParam("Referer } // !!! we need to reload the ontology model before returning a response, to make sure the next request already gets the new version !!! - // same logic as in OntologyFilter. TO-DO: encapsulate? - OntologyModelGetter modelGetter = new OntologyModelGetter(endUserApp, getSystem(), ontModelSpec, getSystem().getOntologyQuery()); - ontModelSpec.setImportModelGetter(modelGetter); - if (log.isDebugEnabled()) log.debug("Started loading ontology with URI '{}' from the admin dataset", ontologyURI); - Model baseModel = modelGetter.getModel(ontologyURI); - OntModel ontModel = ModelFactory.createOntologyModel(ontModelSpec, baseModel); - // materialize OntModel inferences to avoid invoking rules engine on every request - OntModel materializedModel = ModelFactory.createOntologyModel(OntModelSpec.OWL_MEM); // no inference - materializedModel.add(ontModel); - ontModel.getDocumentManager().addModel(ontologyURI, materializedModel, true); // make immutable and add as OntModel so that imports do not need to be reloaded during retrieval - // make sure to cache imported models not only by ontology URI but also by document URI - ontModel.listImportedOntologyURIs(true).forEach((String importURI) -> addDocumentModel(ontModel.getDocumentManager(), importURI)); - if (log.isDebugEnabled()) log.debug("Finished loading ontology with URI '{}' from the admin dataset", ontologyURI); + OntologyFilter.loadOntology(repository, ontologyURI); } if (referer != null) return Response.seeOther(referer).build(); diff --git a/src/main/java/com/atomgraph/linkeddatahub/resource/admin/SignUp.java b/src/main/java/com/atomgraph/linkeddatahub/resource/admin/SignUp.java index bb0119ec21..1fd7057e7f 100644 --- a/src/main/java/com/atomgraph/linkeddatahub/resource/admin/SignUp.java +++ b/src/main/java/com/atomgraph/linkeddatahub/resource/admin/SignUp.java @@ -72,7 +72,7 @@ import jakarta.ws.rs.core.UriInfo; import jakarta.ws.rs.ext.Providers; import static org.apache.jena.datatypes.xsd.XSDDatatype.XSDhexBinary; -import org.apache.jena.ontology.Ontology; +import org.apache.jena.ontapi.model.OntModel; import org.apache.jena.query.ParameterizedSparqlString; import org.apache.jena.query.Query; import org.apache.jena.query.ResultSet; @@ -144,7 +144,7 @@ public class SignUp extends DocumentHierarchyGraphStoreImpl // TO-DO: move to AuthenticationExceptionMapper and handle as state instead of URI resource? @Inject public SignUp(@Context Request request, @Context UriInfo uriInfo, MediaTypes mediaTypes, - com.atomgraph.linkeddatahub.apps.model.Application application, Optional ontology, Optional service, + com.atomgraph.linkeddatahub.apps.model.Application application, Optional ontology, Optional service, @Context SecurityContext securityContext, Optional agentContext, @Context Providers providers, com.atomgraph.linkeddatahub.Application system, @Context ServletConfig servletConfig) { diff --git a/src/main/java/com/atomgraph/linkeddatahub/resource/admin/pkg/InstallPackage.java b/src/main/java/com/atomgraph/linkeddatahub/resource/admin/pkg/InstallPackage.java index 9e760cd0b1..e178f6dc67 100644 --- a/src/main/java/com/atomgraph/linkeddatahub/resource/admin/pkg/InstallPackage.java +++ b/src/main/java/com/atomgraph/linkeddatahub/resource/admin/pkg/InstallPackage.java @@ -17,7 +17,6 @@ package com.atomgraph.linkeddatahub.resource.admin.pkg; import static com.atomgraph.client.MediaType.TEXT_XSL; -import com.atomgraph.client.util.DataManager; import com.atomgraph.linkeddatahub.apps.model.AdminApplication; import com.atomgraph.linkeddatahub.apps.model.EndUserApplication; import com.atomgraph.linkeddatahub.client.GraphStoreClient; @@ -60,7 +59,6 @@ import org.apache.jena.ontology.ConversionException; import org.apache.jena.update.UpdateFactory; import org.apache.jena.update.UpdateRequest; -import org.apache.jena.util.FileManager; import com.atomgraph.linkeddatahub.vocabulary.DH; import com.atomgraph.linkeddatahub.vocabulary.FOAF; import com.atomgraph.linkeddatahub.vocabulary.SIOC; @@ -88,7 +86,6 @@ public class InstallPackage private final com.atomgraph.linkeddatahub.apps.model.Application application; private final com.atomgraph.linkeddatahub.Application system; - private final DataManager dataManager; private final Optional agentContext; @Context ServletContext servletContext; @@ -105,12 +102,10 @@ public class InstallPackage @Inject public InstallPackage(com.atomgraph.linkeddatahub.apps.model.Application application, com.atomgraph.linkeddatahub.Application system, - DataManager dataManager, Optional agentContext) { this.application = application; this.system = system; - this.dataManager = dataManager; this.agentContext = agentContext; } @@ -242,12 +237,12 @@ private com.atomgraph.linkeddatahub.apps.model.Package getPackage(String package final Model model; // check if we have the model in the cache first and if yes, return it from there instead making an HTTP request - if (((FileManager)getDataManager()).hasCachedModel(packageURI) || - (getDataManager().isResolvingMapped() && getDataManager().isMapped(packageURI))) // read mapped URIs (such as system ontologies) from a file + if (getSystem().getRepository().isCached(packageURI) || + (getSystem().getRepository().isMapped(packageURI))) // read mapped URIs (such as system ontologies) from a file { - if (log.isDebugEnabled()) log.debug("hasCachedModel({}): {}", packageURI, ((FileManager)getDataManager()).hasCachedModel(packageURI)); - if (log.isDebugEnabled()) log.debug("isMapped({}): {}", packageURI, getDataManager().isMapped(packageURI)); - model = getDataManager().loadModel(packageURI); + if (log.isDebugEnabled()) log.debug("hasCachedModel({}): {}", packageURI, getSystem().getRepository().isCached(packageURI)); + if (log.isDebugEnabled()) log.debug("isMapped({}): {}", packageURI, getSystem().getRepository().isMapped(packageURI)); + model = ModelFactory.createModelForGraph(getSystem().getRepository().get(packageURI)); } else { @@ -284,12 +279,12 @@ private Model downloadOntology(String uri) if (log.isDebugEnabled()) log.debug("Downloading ontology from: {}", uri); // check if we have the model in the cache first and if yes, return it from there instead making an HTTP request - if (((FileManager)getDataManager()).hasCachedModel(uri) || - (getDataManager().isResolvingMapped() && getDataManager().isMapped(uri))) // read mapped URIs (such as system ontologies) from a file + if (getSystem().getRepository().isCached(uri) || + (getSystem().getRepository().isMapped(uri))) // read mapped URIs (such as system ontologies) from a file { - if (log.isDebugEnabled()) log.debug("hasCachedModel({}): {}", uri, ((FileManager)getDataManager()).hasCachedModel(uri)); - if (log.isDebugEnabled()) log.debug("isMapped({}): {}", uri, getDataManager().isMapped(uri)); - return getDataManager().loadModel(uri); + if (log.isDebugEnabled()) log.debug("hasCachedModel({}): {}", uri, getSystem().getRepository().isCached(uri)); + if (log.isDebugEnabled()) log.debug("isMapped({}): {}", uri, getSystem().getRepository().isMapped(uri)); + return ModelFactory.createModelForGraph(getSystem().getRepository().get(uri)); } else { @@ -522,15 +517,6 @@ public ServletContext getServletContext() return servletContext; } - /** - * Returns RDF data manager. - * - * @return RDF data manager - */ - public DataManager getDataManager() - { - return dataManager; - } /** * Returns JAX-RS resource context. diff --git a/src/main/java/com/atomgraph/linkeddatahub/resource/admin/pkg/UninstallPackage.java b/src/main/java/com/atomgraph/linkeddatahub/resource/admin/pkg/UninstallPackage.java index fb0cb5f3c7..28d37ca9fa 100644 --- a/src/main/java/com/atomgraph/linkeddatahub/resource/admin/pkg/UninstallPackage.java +++ b/src/main/java/com/atomgraph/linkeddatahub/resource/admin/pkg/UninstallPackage.java @@ -16,7 +16,6 @@ */ package com.atomgraph.linkeddatahub.resource.admin.pkg; -import com.atomgraph.client.util.DataManager; import com.atomgraph.linkeddatahub.apps.model.AdminApplication; import com.atomgraph.linkeddatahub.apps.model.EndUserApplication; import com.atomgraph.linkeddatahub.client.GraphStoreClient; @@ -39,10 +38,10 @@ import jakarta.ws.rs.core.UriBuilder; import org.apache.commons.codec.binary.Hex; import org.apache.jena.rdf.model.Model; +import org.apache.jena.rdf.model.ModelFactory; import org.apache.jena.rdf.model.Resource; import org.apache.jena.update.UpdateFactory; import org.apache.jena.update.UpdateRequest; -import org.apache.jena.util.FileManager; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; @@ -73,7 +72,6 @@ public class UninstallPackage private final com.atomgraph.linkeddatahub.apps.model.Application application; private final com.atomgraph.linkeddatahub.Application system; - private final DataManager dataManager; private final Optional agentContext; @Context ServletContext servletContext; @@ -90,12 +88,10 @@ public class UninstallPackage @Inject public UninstallPackage(com.atomgraph.linkeddatahub.apps.model.Application application, com.atomgraph.linkeddatahub.Application system, - DataManager dataManager, Optional agentContext) { this.application = application; this.system = system; - this.dataManager = dataManager; this.agentContext = agentContext; } @@ -317,12 +313,12 @@ private com.atomgraph.linkeddatahub.apps.model.Package getPackage(String package final Model model; // check if we have the model in the cache first and if yes, return it from there instead making an HTTP request - if (((FileManager)getDataManager()).hasCachedModel(packageURI) || - (getDataManager().isResolvingMapped() && getDataManager().isMapped(packageURI))) // read mapped URIs (such as system ontologies) from a file + if (getSystem().getRepository().isCached(packageURI) || + (getSystem().getRepository().isMapped(packageURI))) // read mapped URIs (such as system ontologies) from a file { - if (log.isDebugEnabled()) log.debug("hasCachedModel({}): {}", packageURI, ((FileManager)getDataManager()).hasCachedModel(packageURI)); - if (log.isDebugEnabled()) log.debug("isMapped({}): {}", packageURI, getDataManager().isMapped(packageURI)); - model = getDataManager().loadModel(packageURI); + if (log.isDebugEnabled()) log.debug("hasCachedModel({}): {}", packageURI, getSystem().getRepository().isCached(packageURI)); + if (log.isDebugEnabled()) log.debug("isMapped({}): {}", packageURI, getSystem().getRepository().isMapped(packageURI)); + model = ModelFactory.createModelForGraph(getSystem().getRepository().get(packageURI)); } else { @@ -350,15 +346,6 @@ public com.atomgraph.linkeddatahub.Application getSystem() return system; } - /** - * Returns RDF data manager. - * - * @return RDF data manager - */ - public DataManager getDataManager() - { - return dataManager; - } /** * Returns JAX-RS resource context. diff --git a/src/main/java/com/atomgraph/linkeddatahub/server/event/AuthorizationCreated.java b/src/main/java/com/atomgraph/linkeddatahub/server/event/AuthorizationCreated.java index 7353e02978..3b02124d24 100644 --- a/src/main/java/com/atomgraph/linkeddatahub/server/event/AuthorizationCreated.java +++ b/src/main/java/com/atomgraph/linkeddatahub/server/event/AuthorizationCreated.java @@ -16,7 +16,6 @@ */ package com.atomgraph.linkeddatahub.server.event; -import com.atomgraph.core.util.jena.DataManager; import com.atomgraph.linkeddatahub.apps.model.Application; import com.atomgraph.linkeddatahub.client.GraphStoreClient; import org.apache.jena.rdf.model.Resource; diff --git a/src/main/java/com/atomgraph/linkeddatahub/server/factory/OntologyFactory.java b/src/main/java/com/atomgraph/linkeddatahub/server/factory/OntologyFactory.java index 39fe494b6f..8fc56c6a24 100644 --- a/src/main/java/com/atomgraph/linkeddatahub/server/factory/OntologyFactory.java +++ b/src/main/java/com/atomgraph/linkeddatahub/server/factory/OntologyFactory.java @@ -20,7 +20,7 @@ import jakarta.ws.rs.container.ContainerRequestContext; import jakarta.ws.rs.core.Context; import jakarta.ws.rs.ext.Provider; -import org.apache.jena.ontology.Ontology; +import org.apache.jena.ontapi.model.OntModel; import org.apache.jena.vocabulary.OWL; import org.glassfish.hk2.api.Factory; import org.glassfish.hk2.api.ServiceLocator; @@ -31,19 +31,19 @@ * @author Martynas Jusevičius {@literal } */ @Provider -public class OntologyFactory implements Factory> +public class OntologyFactory implements Factory> { @Context private ServiceLocator serviceLocator; @Override - public Optional provide() + public Optional provide() { return getOntology(); } @Override - public void dispose(Optional t) + public void dispose(Optional t) { } @@ -52,9 +52,9 @@ public void dispose(Optional t) * * @return ontology */ - public Optional getOntology() + public Optional getOntology() { - return (Optional)getContainerRequestContext().getProperty(OWL.Ontology.getURI()); + return (Optional)getContainerRequestContext().getProperty(OWL.Ontology.getURI()); } /** 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 03ef836621..e9bd7af715 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 @@ -18,27 +18,28 @@ import com.atomgraph.linkeddatahub.apps.model.Application; import com.atomgraph.linkeddatahub.apps.model.EndUserApplication; +import com.atomgraph.client.util.jena.PrefixGraphRepository; import com.atomgraph.linkeddatahub.vocabulary.LAPP; import com.atomgraph.server.exception.OntologyException; -import com.atomgraph.linkeddatahub.server.util.OntologyModelGetter; import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; +import java.util.HashSet; import java.util.Optional; +import java.util.Set; import jakarta.annotation.Priority; import jakarta.inject.Inject; -import jakarta.ws.rs.InternalServerErrorException; import jakarta.ws.rs.container.ContainerRequestContext; import jakarta.ws.rs.container.ContainerRequestFilter; import jakarta.ws.rs.container.PreMatching; -import org.apache.jena.ontology.OntDocumentManager; -import org.apache.jena.ontology.OntModel; -import org.apache.jena.ontology.OntModelSpec; -import org.apache.jena.ontology.Ontology; +import org.apache.jena.ontapi.OntModelFactory; +import org.apache.jena.ontapi.OntSpecification; +import org.apache.jena.ontapi.model.OntModel; import org.apache.jena.rdf.model.Model; import org.apache.jena.rdf.model.ModelFactory; -import org.apache.jena.util.FileManager; import org.apache.jena.vocabulary.OWL; +import org.apache.jena.vocabulary.RDF; +import org.apache.jena.vocabulary.RDFS; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -100,7 +101,7 @@ public void filter(ContainerRequestContext crc) throws IOException * @param crc request context * @return optional ontology */ - public Optional getOntology(ContainerRequestContext crc) + public Optional getOntology(ContainerRequestContext crc) { Optional appOpt = getApplication(crc); @@ -115,99 +116,112 @@ public Optional getOntology(ContainerRequestContext crc) return Optional.empty(); } } - + /** * Gets ontology of the specified application. - * + * * @param app application resource - * @return ontology resource + * @return ontology model */ - public Ontology getOntology(Application app) + public OntModel getOntology(Application app) { if (app.getOntology() == null) return null; return getOntology(app, app.getOntology().getURI()); } - + /** - * Loads ontology using the specified ontology URI. - * + * Loads the ontology model for the specified ontology URI, building its owl:imports closure with + * RDFS inference and materializing the inferences into the repository cache. + * * @param app application resource * @param uri ontology URI - * @return ontology resource + * @return ontology model */ - public Ontology getOntology(Application app, String uri) + public OntModel getOntology(Application app, String uri) { - if (app == null) throw new IllegalArgumentException("Application string cannot be null"); - if (uri == null) throw new IllegalArgumentException("Ontology URI string cannot be null"); + if (app == null) throw new IllegalArgumentException("Application cannot be null"); + if (uri == null) throw new IllegalArgumentException("Ontology URI cannot be null"); - final OntModelSpec ontModelSpec; - if (app.canAs(EndUserApplication.class)) + final PrefixGraphRepository repository = app.canAs(EndUserApplication.class) ? + getSystem().getRepository(app.as(EndUserApplication.class)) : getSystem().getRepository(); + + // only build the materialized model if the ontology is not already cached; the double check under the + // repository lock ensures a single thread materializes it (loadOntology is a compound load + inference + + // put, not atomic), so concurrent cold requests don't duplicate the work or race each other's writes + if (!repository.isCached(uri)) { - ontModelSpec = new OntModelSpec(getSystem().getOntModelSpec(app.as(EndUserApplication.class))); - // only create InfModel if ontology is not already cached - if (!ontModelSpec.getDocumentManager().getFileManager().hasCachedModel(uri)) + synchronized (repository) { - OntologyModelGetter modelGetter = new OntologyModelGetter(app.as(EndUserApplication.class), getSystem(), ontModelSpec, getSystem().getOntologyQuery()); - ontModelSpec.setImportModelGetter(modelGetter); - if (log.isDebugEnabled()) log.debug("Started loading ontology with URI '{}' from the admin dataset", uri); - Model baseModel = modelGetter.getModel(uri); - OntModel ontModel = ModelFactory.createOntologyModel(ontModelSpec, baseModel); - // materialize OntModel inferences to avoid invoking rules engine on every request - OntModel materializedModel = ModelFactory.createOntologyModel(OntModelSpec.OWL_MEM); // no inference - materializedModel.add(ontModel); - ontModel.getDocumentManager().addModel(uri, materializedModel, true); // make immutable and add as OntModel so that imports do not need to be reloaded during retrieval - // make sure to cache imported models not only by ontology URI but also by document URI - ontModel.listImportedOntologyURIs(true).forEach((String importURI) -> addDocumentModel(ontModel.getDocumentManager(), importURI)); - if (log.isDebugEnabled()) log.debug("Finished loading ontology with URI '{}' from the admin dataset", uri); + if (!repository.isCached(uri)) loadOntology(repository, uri); } } - else + + return OntModelFactory.createModel(repository.get(uri), OntSpecification.OWL2_FULL_MEM); + } + + /** + * Builds and caches the materialized ontology model. Assembles the owl:imports closure into a single + * graph (so ontapi never manages a union-graph hierarchy over the shared repository), applies RDFS + * inference over the flattened closure, and materializes the inferences into the repository cache so + * the rules engine is not invoked on every request. + * + * @param repository graph repository + * @param uri ontology URI + */ + public static void loadOntology(PrefixGraphRepository repository, String uri) + { + if (log.isDebugEnabled()) log.debug("Started loading ontology with URI '{}'", uri); + Model union = ModelFactory.createDefaultModel(); + Set closure = new HashSet<>(); + loadClosure(repository, uri, union, closure); + OntModel inferred = OntModelFactory.createModel(union.getGraph(), OntSpecification.OWL2_FULL_MEM_RDFS_INF); + OntModel materialized = OntModelFactory.createModel(OntSpecification.OWL2_FULL_MEM); + materialized.add(inferred); + // promote rdfs:Class to owl:Class so OWL2 profiles recognise third-party vocab terms (e.g. sp:Describe in sp.ttl) + inferred.listSubjectsWithProperty(RDF.type, RDFS.Class).forEach(r -> materialized.add(r, RDF.type, OWL.Class)); + repository.put(uri, materialized.getGraph()); + // cache imported graphs under their fragment-stripped document URIs too + closure.stream().filter(closureURI -> !closureURI.equals(uri)).forEach(importURI -> addDocumentModel(repository, importURI)); + if (log.isDebugEnabled()) log.debug("Finished loading ontology with URI '{}'", uri); + } + + /** + * Recursively loads the transitive owl:imports closure of an ontology into a single union model, + * fetching each graph via the repository (SPARQL-first / bundled mappings). + * + * @param repository graph repository + * @param uri ontology URI + * @param union accumulator model + * @param seen accumulator of visited URIs (prevents cycles) + */ + public static void loadClosure(PrefixGraphRepository repository, String uri, Model union, Set seen) + { + if (!seen.add(uri)) return; + Model model = ModelFactory.createModelForGraph(repository.get(uri)); + union.add(model); + model.listObjectsOfProperty(OWL.imports).toList().forEach(imp -> { - ontModelSpec = new OntModelSpec(getSystem().getOntModelSpec()); - FileManager fileManager = ontModelSpec.getDocumentManager().getFileManager(); - if (!fileManager.hasCachedModel(uri)) - { - try - { - URI ontologyURI = URI.create(uri); - // remove fragment and normalize - URI ontDocURI = new URI(ontologyURI.getScheme(), ontologyURI.getSchemeSpecificPart(), null).normalize(); - Model baseModel = fileManager.loadModel(ontDocURI.toString()); - OntModel ontModel = ModelFactory.createOntologyModel(ontModelSpec, baseModel); - ontModel.getDocumentManager().addModel(uri, ontModel, true); - } - catch (URISyntaxException ex) - { - if (log.isErrorEnabled()) log.error("Ontology URI syntax error: {}", ex.getInput()); - throw new InternalServerErrorException(ex); - } - } - } - return ontModelSpec.getDocumentManager().getOntology(uri, ontModelSpec).getOntology(uri); // reloads the imports using ModelGetter. TO-DO: optimize? + if (imp.isURIResource()) loadClosure(repository, imp.asResource().getURI(), union, seen); + }); } /** - * Extracts document URI from ontology import URI and uses it as a secondary cache key. - * - * @param odm document manager + * Caches an imported graph under its fragment-stripped document URI as a secondary cache key. + * + * @param repository graph repository * @param importURI ontology URI */ - public static void addDocumentModel(OntDocumentManager odm, String importURI) + public static void addDocumentModel(PrefixGraphRepository repository, String importURI) { try { URI ontologyURI = URI.create(importURI); // remove fragment and normalize URI docURI = new URI(ontologyURI.getScheme(), ontologyURI.getSchemeSpecificPart(), null).normalize(); - String mappedURI = odm.getFileManager().mapURI(docURI.toString()); - // only cache import document URI if it's not already cached or mapped - if (!odm.getFileManager().hasCachedModel(docURI.toString()) && mappedURI.equals(docURI.toString())) - { - Model importModel = odm.getModel(importURI); - if (importModel == null) throw new IllegalArgumentException("Import model is not cached"); - odm.addModel(docURI.toString(), importModel, true); - } + // only cache the document URI if it is not already cached or mapped to a different location + if (!repository.isCached(docURI.toString()) && repository.resolve(docURI.toString()).equals(docURI.toString())) + repository.put(docURI.toString(), repository.get(importURI)); } catch (URISyntaxException ex) { diff --git a/src/main/java/com/atomgraph/linkeddatahub/server/filter/request/ProxyRequestFilter.java b/src/main/java/com/atomgraph/linkeddatahub/server/filter/request/ProxyRequestFilter.java index e42e43a937..9166b28ea9 100644 --- a/src/main/java/com/atomgraph/linkeddatahub/server/filter/request/ProxyRequestFilter.java +++ b/src/main/java/com/atomgraph/linkeddatahub/server/filter/request/ProxyRequestFilter.java @@ -22,7 +22,7 @@ import com.atomgraph.core.exception.BadGatewayException; import com.atomgraph.core.util.ModelUtils; import com.atomgraph.linkeddatahub.apps.model.Dataset; -import org.apache.jena.ontology.Ontology; +import org.apache.jena.ontapi.model.OntModel; import com.atomgraph.linkeddatahub.client.GraphStoreClient; import com.atomgraph.linkeddatahub.client.filter.auth.IDTokenDelegationFilter; import com.atomgraph.linkeddatahub.client.filter.auth.WebIDDelegationFilter; @@ -38,8 +38,8 @@ import java.util.List; import java.util.Optional; import java.util.Set; +import org.apache.jena.query.ParameterizedSparqlString; import org.apache.jena.query.QueryExecution; -import org.apache.jena.query.QueryFactory; import jakarta.annotation.Priority; import jakarta.inject.Inject; import jakarta.ws.rs.NotAllowedException; @@ -50,6 +50,7 @@ import jakarta.ws.rs.client.WebTarget; import jakarta.ws.rs.container.ContainerRequestContext; import jakarta.ws.rs.container.ContainerRequestFilter; +import jakarta.ws.rs.HttpMethod; import jakarta.ws.rs.container.PreMatching; import jakarta.ws.rs.core.Context; import jakarta.ws.rs.core.EntityTag; @@ -132,7 +133,7 @@ public class ProxyRequestFilter implements ContainerRequestFilter "Age"); @Inject com.atomgraph.linkeddatahub.Application system; - @Inject jakarta.inject.Provider> ontology; + @Inject jakarta.inject.Provider> ontology; @Inject MediaTypes mediaTypes; @Context Request request; @@ -177,10 +178,10 @@ public void filter(ContainerRequestContext requestContext) throws IOException || "HEAD".equalsIgnoreCase(requestContext.getMethod()); // serve mapped URIs (e.g. system ontologies) directly from the DataManager cache - if (isSafeMethod && getSystem().getDataManager().isMapped(targetURI.toString())) + if (isSafeMethod && getSystem().getRepository().isMapped(targetURI.toString())) { if (log.isDebugEnabled()) log.debug("Serving mapped URI from DataManager cache: {}", targetURI); - Model model = getSystem().getDataManager().loadModel(targetURI.toString()); + Model model = org.apache.jena.rdf.model.ModelFactory.createModelForGraph(getSystem().getRepository().get(targetURI.toString())); requestContext.abortWith(getResponse(model, Response.Status.OK)); return; } @@ -191,9 +192,10 @@ public void filter(ContainerRequestContext requestContext) throws IOException // ?term where STR(?term) starts with "#") if (isSafeMethod && getOntology().isPresent()) { - String describeQueryStr = "DESCRIBE <" + targetURI + "> ?term " + - "WHERE { ?term ?p ?o FILTER(STRSTARTS(STR(?term), CONCAT(STR(<" + targetURI + ">), \"#\"))) }"; - try (QueryExecution qe = QueryExecution.create(QueryFactory.create(describeQueryStr), getOntology().get().getOntModel())) + ParameterizedSparqlString pss = new ParameterizedSparqlString( + "DESCRIBE ?doc ?term WHERE { ?term ?p ?o FILTER(STRSTARTS(STR(?term), CONCAT(STR(?doc), \"#\"))) }"); + pss.setIri("doc", targetURI.toString()); + try (QueryExecution qe = QueryExecution.create(pss.asQuery(), getOntology().get())) { Model description = qe.execDescribe(); if (!description.isEmpty()) @@ -240,7 +242,7 @@ else if (agentContext instanceof IDTokenSecurityContext idTokenSecurityContext) try (clientResponse) { - requestContext.abortWith(getResponse(clientResponse, targetURI)); + requestContext.abortWith(getResponse(clientResponse, targetURI, requestContext.getMethod())); } } catch (MessageBodyProviderNotFoundException ex) @@ -274,9 +276,7 @@ protected Optional resolveTargetURI(ContainerRequestContext requestContext) if (proxyTarget != null) return Optional.of(proxyTarget); // Case 2: lapp:Dataset proxy - @SuppressWarnings("unchecked") - Optional datasetOpt = - (Optional) requestContext.getProperty(LAPP.Dataset.getURI()); + Optional datasetOpt = (Optional) requestContext.getProperty(LAPP.Dataset.getURI()); if (datasetOpt != null && datasetOpt.isPresent()) { URI proxied = datasetOpt.get().getProxied(requestContext.getUriInfo().getAbsolutePath()); @@ -320,10 +320,23 @@ protected Optional resolveTargetURI(ContainerRequestContext requestContext) * * @param clientResponse response from the proxy target * @param targetURI upstream URI (used as the parse base URI hint for {@code ModelProvider}) + * @param method HTTP method * @return JAX-RS response to return to the original caller */ - protected Response getResponse(Response clientResponse, URI targetURI) + protected Response getResponse(Response clientResponse, URI targetURI, String method) { + // HEAD responses have no body by HTTP semantics. Routing them through + // the typed branches below would parse an empty entity into an empty + // Model/ResultSet, then re-stamp ETag/Last-Modified off that empty + // value — producing validators that disagree with the upstream GET. + // Forward the upstream headers (including ETag) verbatim instead. + if (HttpMethod.HEAD.equalsIgnoreCase(method)) + { + Response.ResponseBuilder rb = Response.status(clientResponse.getStatus()); + if (clientResponse.getMediaType() != null) rb.type(clientResponse.getMediaType()); + return overlayHeaders(rb.build(), clientResponse, true); + } + if (clientResponse.getMediaType() == null) { Response.ResponseBuilder rb = Response.status(clientResponse.getStatus()); @@ -473,7 +486,7 @@ public com.atomgraph.linkeddatahub.Application getSystem() * * @return optional ontology */ - public Optional getOntology() + public Optional getOntology() { return ontology.get(); } diff --git a/src/main/java/com/atomgraph/linkeddatahub/server/filter/request/auth/WebIDFilter.java b/src/main/java/com/atomgraph/linkeddatahub/server/filter/request/auth/WebIDFilter.java index b42e1fbe49..7dbfa8baf8 100644 --- a/src/main/java/com/atomgraph/linkeddatahub/server/filter/request/auth/WebIDFilter.java +++ b/src/main/java/com/atomgraph/linkeddatahub/server/filter/request/auth/WebIDFilter.java @@ -39,7 +39,6 @@ import jakarta.annotation.PostConstruct; import jakarta.annotation.Priority; import jakarta.servlet.http.HttpServletRequest; -import jakarta.ws.rs.BadRequestException; import jakarta.ws.rs.Priorities; import jakarta.ws.rs.ProcessingException; import jakarta.ws.rs.client.Client; diff --git a/src/main/java/com/atomgraph/linkeddatahub/server/io/ValidatingModelProvider.java b/src/main/java/com/atomgraph/linkeddatahub/server/io/ValidatingModelProvider.java index 206228401f..018bca6c29 100644 --- a/src/main/java/com/atomgraph/linkeddatahub/server/io/ValidatingModelProvider.java +++ b/src/main/java/com/atomgraph/linkeddatahub/server/io/ValidatingModelProvider.java @@ -240,7 +240,7 @@ public Resource processRead(Resource resource) // this logic really belongs in a if (getApplication().isPresent() && getApplication().get().canAs(AdminApplication.class) && resource.hasProperty(RDF.type, OWL.Ontology)) { // clear cached OntModel if ontology is updated. TO-DO: send event instead - getSystem().getOntModelSpec().getDocumentManager().getFileManager().removeCacheModel(resource.getURI()); + getSystem().getRepository().remove(resource.getURI()); } if (getApplication().isPresent() && resource.hasProperty(RDF.type, ACL.Authorization)) 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 0b8b65674f..f82fc0c66f 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 @@ -89,7 +89,7 @@ import org.apache.commons.lang3.StringUtils; import org.apache.jena.atlas.RuntimeIOException; import org.apache.jena.datatypes.xsd.XSDDateTime; -import org.apache.jena.ontology.Ontology; +import org.apache.jena.ontapi.model.OntModel; import org.apache.jena.query.Dataset; import org.apache.jena.query.DatasetFactory; import org.apache.jena.rdf.model.Model; @@ -132,7 +132,7 @@ public class DocumentHierarchyGraphStoreImpl extends com.atomgraph.core.model.im public static final String UPLOADS_PATH = "uploads"; private final com.atomgraph.linkeddatahub.apps.model.Application application; - private final Ontology ontology; + private final OntModel ontology; private final Service service; private final Providers providers; private final com.atomgraph.linkeddatahub.Application system; @@ -160,7 +160,7 @@ public class DocumentHierarchyGraphStoreImpl extends com.atomgraph.core.model.im */ @Inject public DocumentHierarchyGraphStoreImpl(@Context Request request, @Context UriInfo uriInfo, MediaTypes mediaTypes, - com.atomgraph.linkeddatahub.apps.model.Application application, Optional ontology, Optional service, + com.atomgraph.linkeddatahub.apps.model.Application application, Optional ontology, Optional service, @Context SecurityContext securityContext, Optional agentContext, @Context Providers providers, com.atomgraph.linkeddatahub.Application system) { @@ -394,6 +394,7 @@ public Response patch(UpdateRequest updateRequest) Model beforeUpdateModel = ModelFactory.createDefaultModel().add(existingModel); dataset = DatasetFactory.wrap(existingModel); UpdateAction.execute(updateRequest, dataset); // update model in memory + new Skolemizer(getURI().toString()).apply(existingModel); // skolemize blank nodes introduced by INSERT // if PATCH results in an empty graph, treat it as a DELETE request if (existingModel.isEmpty()) return delete(); @@ -1030,7 +1031,7 @@ public com.atomgraph.linkeddatahub.apps.model.Application getApplication() * * @return ontology resource */ - public Ontology getOntology() + public OntModel getOntology() { return ontology; } diff --git a/src/main/java/com/atomgraph/linkeddatahub/server/util/OntologyModelGetter.java b/src/main/java/com/atomgraph/linkeddatahub/server/util/OntologyRepository.java similarity index 55% rename from src/main/java/com/atomgraph/linkeddatahub/server/util/OntologyModelGetter.java rename to src/main/java/com/atomgraph/linkeddatahub/server/util/OntologyRepository.java index 63c8d659d7..5229c38222 100644 --- a/src/main/java/com/atomgraph/linkeddatahub/server/util/OntologyModelGetter.java +++ b/src/main/java/com/atomgraph/linkeddatahub/server/util/OntologyRepository.java @@ -17,56 +17,82 @@ package com.atomgraph.linkeddatahub.server.util; import com.atomgraph.client.vocabulary.LDT; +import com.atomgraph.core.client.GraphStoreClient; +import com.atomgraph.client.util.jena.PrefixGraphRepository; import com.atomgraph.linkeddatahub.apps.model.EndUserApplication; -import com.atomgraph.server.exception.OntologyException; import jakarta.ws.rs.core.MultivaluedHashMap; import jakarta.ws.rs.core.MultivaluedMap; import jakarta.ws.rs.core.Response; -import org.apache.jena.ontology.OntModelSpec; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import net.jodah.expiringmap.ExpirationPolicy; +import net.jodah.expiringmap.ExpiringMap; +import org.apache.jena.graph.Graph; import org.apache.jena.query.ParameterizedSparqlString; import org.apache.jena.query.Query; import org.apache.jena.rdf.model.Model; -import org.apache.jena.rdf.model.ModelFactory; -import org.apache.jena.rdf.model.ModelReader; -import org.apache.jena.util.FileManager; import org.slf4j.Logger; import org.slf4j.LoggerFactory; - /** - * Application's ontology model getter. - * Loads ontology model using the configured ontology query + * Ontology graph repository that resolves graphs SPARQL-first: it runs the configured ontology query + * against the admin endpoint and, only if that returns nothing, falls back to the bundled mappings / + * HTTP loading of the superclass. Replaces the legacy {@code ModelGetter} plugged into {@code OntModelSpec}. + * + * @author Martynas Jusevičius {@literal } */ -public class OntologyModelGetter implements org.apache.jena.ontology.models.ModelGetter +public class OntologyRepository extends PrefixGraphRepository { - private static final Logger log = LoggerFactory.getLogger(OntologyModelGetter.class); + private static final Logger log = LoggerFactory.getLogger(OntologyRepository.class); private final EndUserApplication app; private final com.atomgraph.linkeddatahub.Application system; - private final OntModelSpec ontModelSpec; private final Query ontologyQuery; /** - * Constructs ontology getter for application. + * Constructs the repository for an application. * * @param app end-user application resource * @param system system application - * @param ontModelSpec ontology specification + * @param gsc Graph Store client for HTTP fallback loading * @param ontologyQuery SPARQL query that loads ontology terms */ - public OntologyModelGetter(EndUserApplication app, com.atomgraph.linkeddatahub.Application system, OntModelSpec ontModelSpec, Query ontologyQuery) + public OntologyRepository(EndUserApplication app, com.atomgraph.linkeddatahub.Application system, GraphStoreClient gsc, Query ontologyQuery) { + super(gsc); this.app = app; this.system = system; - this.ontModelSpec = ontModelSpec; this.ontologyQuery = ontologyQuery; } + /** + * Backs the dynamically loaded graphs (materialized ontologies and their imports) with a bounded, + * idle-expiring map instead of an unbounded one. Evicted entries are transparently reloaded — the + * imports closure is re-materialized by {@code OntologyFilter} on the next cache miss — so this is a + * memory backstop, not a correctness dependency. Must not reference instance state (called during + * construction). + * + * @return evicting dynamic-graph store + */ @Override - public Model getModel(String uri) + protected Map createStore() { - // attempt to load ontology model from the admin endpoint. TO-DO: is that necessary if ontologies terms are now stored in a single graph? + return ExpiringMap.builder(). + maxSize(1000). + expirationPolicy(ExpirationPolicy.ACCESSED). // idle-based: keep actively-used ontologies warm + expiration(1, TimeUnit.HOURS). + build(); + } + + @Override + public Graph get(String uri) + { + // bundled system vocabularies (mapped to shipped files) and already-materialized ontologies bypass the + // admin SPARQL query — the bundled file is authoritative, and querying it would be a wasted round-trip + if (isMapped(uri) || isCached(uri)) return super.get(uri); + + // attempt to load the ontology from the admin endpoint ParameterizedSparqlString ontologyPss = new ParameterizedSparqlString(getOntologyQuery().toString()); ontologyPss.setIri(LDT.ontology.getLocalName(), uri); @@ -82,24 +108,15 @@ public Model getModel(String uri) model = cr.readEntity(Model.class); } - if (!model.isEmpty()) return model; - - // if SPARQL result model is empty, fallback to using FileManager - FileManager fileManager = getOntModelSpec().getDocumentManager().getFileManager(); - return fileManager.loadModel(uri); - } - - @Override - public Model getModel(String uri, ModelReader loadIfAbsent) - { - try - { - return getModel(uri); - } - catch (OntologyException ex) + if (!model.isEmpty()) { - return loadIfAbsent.readModel(ModelFactory.createDefaultModel(), uri); + Graph graph = model.getGraph(); + put(uri, graph); + return graph; } + + // if the SPARQL result is empty, fall back to bundled mappings / HTTP loading + return super.get(uri); } /** @@ -122,24 +139,14 @@ public com.atomgraph.linkeddatahub.Application getSystem() return system; } - /** - * Returns ontology specification. - * - * @return ontology specification - */ - public OntModelSpec getOntModelSpec() - { - return ontModelSpec; - } - /** * Returns the SPARQL query used to load ontology terms. - * + * * @return SPARQL query */ public Query getOntologyQuery() { return ontologyQuery; } - -} \ No newline at end of file + +} diff --git a/src/main/java/com/atomgraph/linkeddatahub/vocabulary/ACL.java b/src/main/java/com/atomgraph/linkeddatahub/vocabulary/ACL.java index 259d34a0ed..c31bca2089 100644 --- a/src/main/java/com/atomgraph/linkeddatahub/vocabulary/ACL.java +++ b/src/main/java/com/atomgraph/linkeddatahub/vocabulary/ACL.java @@ -16,11 +16,11 @@ */ package com.atomgraph.linkeddatahub.vocabulary; -import org.apache.jena.ontology.ObjectProperty; -import org.apache.jena.ontology.OntClass; -import org.apache.jena.ontology.OntModel; -import org.apache.jena.ontology.OntModelSpec; -import org.apache.jena.rdf.model.ModelFactory; +import org.apache.jena.ontapi.OntModelFactory; +import org.apache.jena.ontapi.OntSpecification; +import org.apache.jena.ontapi.model.OntModel; +import org.apache.jena.rdf.model.Property; + import org.apache.jena.rdf.model.Resource; /** @@ -30,8 +30,13 @@ */ public class ACL { + + static + { + org.apache.jena.sys.JenaSystem.init(); // ensure Jena (RDFS vocab) is initialized before ontapi touches it + } /** The RDF model that holds the vocabulary terms */ - private static OntModel m_model = ModelFactory.createOntologyModel(OntModelSpec.OWL_MEM, null); + private static OntModel m_model = OntModelFactory.createModel(OntSpecification.OWL2_FULL_MEM); /** The namespace of the vocabulary as a string */ public static final String NS = "http://www.w3.org/ns/auth/acl#"; @@ -49,45 +54,45 @@ public static String getURI() public static final Resource NAMESPACE = m_model.createResource( NS ); /** acl:Authorization class */ - public static final OntClass Authorization = m_model.createClass( NS + "Authorization" ); + public static final Resource Authorization = m_model.createOntClass( NS + "Authorization" ); /** acl:Read access mode */ - public static final OntClass Read = m_model.createClass( NS + "Read" ); + public static final Resource Read = m_model.createOntClass( NS + "Read" ); /** acl:Write access mode */ - public static final OntClass Write = m_model.createClass( NS + "Write" ); + public static final Resource Write = m_model.createOntClass( NS + "Write" ); /** acl:Append access mode */ - public static final OntClass Append = m_model.createClass( NS + "Append" ); + public static final Resource Append = m_model.createOntClass( NS + "Append" ); /** acl:Control access mode */ - public static final OntClass Control = m_model.createClass( NS + "Control" ); + public static final Resource Control = m_model.createOntClass( NS + "Control" ); /** acl:AuthenticatedAgent class */ - public static final OntClass AuthenticatedAgent = m_model.createClass( NS + "AuthenticatedAgent" ); + public static final Resource AuthenticatedAgent = m_model.createOntClass( NS + "AuthenticatedAgent" ); /** acl:delegates property **/ - public static final ObjectProperty delegates = m_model.createObjectProperty( NS + "delegates" ); + public static final Property delegates = m_model.createObjectProperty( NS + "delegates" ); /** acl:owner property */ - public static final ObjectProperty owner = m_model.createObjectProperty( NS + "owner" ); + public static final Property owner = m_model.createObjectProperty( NS + "owner" ); /** acl:agent property */ - public static final ObjectProperty agent = m_model.createObjectProperty( NS + "agent" ); + public static final Property agent = m_model.createObjectProperty( NS + "agent" ); /** acl:agentClass property */ - public static final ObjectProperty agentClass = m_model.createObjectProperty( NS + "agentClass" ); + public static final Property agentClass = m_model.createObjectProperty( NS + "agentClass" ); /** acl:agentGroup property */ - public static final ObjectProperty agentGroup = m_model.createObjectProperty( NS + "agentGroup" ); + public static final Property agentGroup = m_model.createObjectProperty( NS + "agentGroup" ); /** acl:mode property */ - public static final ObjectProperty mode = m_model.createObjectProperty( NS + "mode" ); + public static final Property mode = m_model.createObjectProperty( NS + "mode" ); /** acl:accessTo property */ - public static final ObjectProperty accessTo = m_model.createObjectProperty( NS + "accessTo" ); + public static final Property accessTo = m_model.createObjectProperty( NS + "accessTo" ); /** acl:accessToClass property */ - public static final ObjectProperty accessToClass = m_model.createObjectProperty( NS + "accessToClass" ); + public static final Property accessToClass = m_model.createObjectProperty( NS + "accessToClass" ); } diff --git a/src/main/java/com/atomgraph/linkeddatahub/vocabulary/Admin.java b/src/main/java/com/atomgraph/linkeddatahub/vocabulary/Admin.java index d2bda94534..8c463d9ca6 100644 --- a/src/main/java/com/atomgraph/linkeddatahub/vocabulary/Admin.java +++ b/src/main/java/com/atomgraph/linkeddatahub/vocabulary/Admin.java @@ -16,9 +16,10 @@ */ package com.atomgraph.linkeddatahub.vocabulary; -import org.apache.jena.ontology.OntModel; -import org.apache.jena.ontology.OntModelSpec; -import org.apache.jena.rdf.model.ModelFactory; +import org.apache.jena.ontapi.OntModelFactory; +import org.apache.jena.ontapi.OntSpecification; +import org.apache.jena.ontapi.model.OntModel; + import org.apache.jena.rdf.model.Resource; /** @@ -28,9 +29,14 @@ */ public class Admin { + + static + { + org.apache.jena.sys.JenaSystem.init(); // ensure Jena (RDFS vocab) is initialized before ontapi touches it + } /** The RDF model that holds the vocabulary terms */ - private static OntModel m_model = ModelFactory.createOntologyModel(OntModelSpec.OWL_MEM, null); + private static OntModel m_model = OntModelFactory.createModel(OntSpecification.OWL2_FULL_MEM); /** The namespace of the vocabulary as a string */ public static final String NS = "https://w3id.org/atomgraph/linkeddatahub/admin#"; diff --git a/src/main/java/com/atomgraph/linkeddatahub/vocabulary/Cert.java b/src/main/java/com/atomgraph/linkeddatahub/vocabulary/Cert.java index c0906f3767..ad869bae69 100644 --- a/src/main/java/com/atomgraph/linkeddatahub/vocabulary/Cert.java +++ b/src/main/java/com/atomgraph/linkeddatahub/vocabulary/Cert.java @@ -16,12 +16,11 @@ */ package com.atomgraph.linkeddatahub.vocabulary; -import org.apache.jena.ontology.DatatypeProperty; -import org.apache.jena.ontology.ObjectProperty; -import org.apache.jena.ontology.OntClass; -import org.apache.jena.ontology.OntModel; -import org.apache.jena.ontology.OntModelSpec; -import org.apache.jena.rdf.model.ModelFactory; +import org.apache.jena.ontapi.OntModelFactory; +import org.apache.jena.ontapi.OntSpecification; +import org.apache.jena.ontapi.model.OntModel; +import org.apache.jena.rdf.model.Property; + import org.apache.jena.rdf.model.Resource; /** @@ -32,8 +31,13 @@ public class Cert { + + static + { + org.apache.jena.sys.JenaSystem.init(); // ensure Jena (RDFS vocab) is initialized before ontapi touches it + } /** The RDF model that holds the vocabulary terms */ - private static OntModel m_model = ModelFactory.createOntologyModel(OntModelSpec.OWL_MEM, null); + private static OntModel m_model = OntModelFactory.createModel(OntSpecification.OWL2_FULL_MEM); /** The namespace of the vocabulary as a string */ public static final String NS = "http://www.w3.org/ns/auth/cert#"; @@ -50,18 +54,18 @@ public static String getURI() public static final Resource NAMESPACE = m_model.createResource( NS ); /** Public key class */ - public static final OntClass PublicKey = m_model.createClass(NS + "PublicKey"); + public static final Resource PublicKey = m_model.createOntClass(NS + "PublicKey"); /** RSA public key class */ - public static final OntClass RSAPublicKey = m_model.createClass(NS + "RSAPublicKey"); + public static final Resource RSAPublicKey = m_model.createOntClass(NS + "RSAPublicKey"); /** Key property */ - public static final ObjectProperty key = m_model.createObjectProperty( NS + "key" ); + public static final Property key = m_model.createObjectProperty( NS + "key" ); /** Modulus property */ - public static final DatatypeProperty modulus = m_model.createDatatypeProperty( NS + "modulus" ); + public static final Property modulus = m_model.createDataProperty( NS + "modulus" ); /** Exponent property */ - public static final DatatypeProperty exponent = m_model.createDatatypeProperty( NS + "exponent" ); + public static final Property exponent = m_model.createDataProperty( NS + "exponent" ); } \ No newline at end of file diff --git a/src/main/java/com/atomgraph/linkeddatahub/vocabulary/DH.java b/src/main/java/com/atomgraph/linkeddatahub/vocabulary/DH.java index 7ea41b3058..140b2e511e 100644 --- a/src/main/java/com/atomgraph/linkeddatahub/vocabulary/DH.java +++ b/src/main/java/com/atomgraph/linkeddatahub/vocabulary/DH.java @@ -15,12 +15,11 @@ */ package com.atomgraph.linkeddatahub.vocabulary; -import org.apache.jena.ontology.DatatypeProperty; -import org.apache.jena.ontology.ObjectProperty; -import org.apache.jena.ontology.OntClass; -import org.apache.jena.ontology.OntModel; -import org.apache.jena.ontology.OntModelSpec; -import org.apache.jena.rdf.model.ModelFactory; +import org.apache.jena.ontapi.OntModelFactory; +import org.apache.jena.ontapi.OntSpecification; +import org.apache.jena.ontapi.model.OntModel; +import org.apache.jena.rdf.model.Property; + import org.apache.jena.rdf.model.Resource; /** @@ -31,8 +30,13 @@ public class DH { + static + { + org.apache.jena.sys.JenaSystem.init(); // ensure Jena (RDFS vocab) is initialized before ontapi touches it + } + /**

The RDF model that holds the vocabulary terms

*/ - private static OntModel m_model = ModelFactory.createOntologyModel(OntModelSpec.OWL_MEM, null); + private static OntModel m_model = OntModelFactory.createModel(OntSpecification.OWL2_FULL_MEM); /**

The namespace of the vocabulary as a string

*/ public static final String NS = "https://www.w3.org/ns/ldt/document-hierarchy#"; @@ -49,15 +53,15 @@ public static String getURI() public static final Resource NAMESPACE = m_model.createResource( NS ); /** Document class */ - public static final OntClass Document = m_model.createClass( NS + "Document" ); + public static final Resource Document = m_model.createOntClass( NS + "Document" ); /** Container class */ - public static final OntClass Container = m_model.createClass( NS + "Container" ); + public static final Resource Container = m_model.createOntClass( NS + "Container" ); /** Item class */ - public static final OntClass Item = m_model.createClass( NS + "Item" ); + public static final Resource Item = m_model.createOntClass( NS + "Item" ); /** Slug property */ - public static final DatatypeProperty slug = m_model.createDatatypeProperty( NS + "slug" ); + public static final Property slug = m_model.createDataProperty( NS + "slug" ); } diff --git a/src/main/java/com/atomgraph/linkeddatahub/vocabulary/Default.java b/src/main/java/com/atomgraph/linkeddatahub/vocabulary/Default.java index 03d36f4293..47ba21532c 100644 --- a/src/main/java/com/atomgraph/linkeddatahub/vocabulary/Default.java +++ b/src/main/java/com/atomgraph/linkeddatahub/vocabulary/Default.java @@ -16,10 +16,10 @@ */ package com.atomgraph.linkeddatahub.vocabulary; -import org.apache.jena.ontology.OntClass; -import org.apache.jena.ontology.OntModel; -import org.apache.jena.ontology.OntModelSpec; -import org.apache.jena.rdf.model.ModelFactory; +import org.apache.jena.ontapi.OntModelFactory; +import org.apache.jena.ontapi.OntSpecification; +import org.apache.jena.ontapi.model.OntModel; + import org.apache.jena.rdf.model.Resource; /** @@ -29,8 +29,13 @@ */ public class Default { + + static + { + org.apache.jena.sys.JenaSystem.init(); // ensure Jena (RDFS vocab) is initialized before ontapi touches it + } /** The RDF model that holds the vocabulary terms */ - private static OntModel m_model = ModelFactory.createOntologyModel(OntModelSpec.OWL_MEM, null); + private static OntModel m_model = OntModelFactory.createModel(OntSpecification.OWL2_FULL_MEM); /** The namespace of the vocabulary as a string */ public static final String NS = "https://w3id.org/atomgraph/linkeddatahub/default#"; @@ -50,6 +55,6 @@ public static String getURI() // DOMAIN /** Root document class */ - public static final OntClass Root = m_model.createClass(NS + "Root"); + public static final Resource Root = m_model.createOntClass(NS + "Root"); } diff --git a/src/main/java/com/atomgraph/linkeddatahub/vocabulary/FOAF.java b/src/main/java/com/atomgraph/linkeddatahub/vocabulary/FOAF.java index a71875fe53..c2bfcc475e 100644 --- a/src/main/java/com/atomgraph/linkeddatahub/vocabulary/FOAF.java +++ b/src/main/java/com/atomgraph/linkeddatahub/vocabulary/FOAF.java @@ -16,12 +16,11 @@ */ package com.atomgraph.linkeddatahub.vocabulary; -import org.apache.jena.ontology.DatatypeProperty; -import org.apache.jena.ontology.ObjectProperty; -import org.apache.jena.ontology.OntClass; -import org.apache.jena.ontology.OntModel; -import org.apache.jena.ontology.OntModelSpec; -import org.apache.jena.rdf.model.ModelFactory; +import org.apache.jena.ontapi.OntModelFactory; +import org.apache.jena.ontapi.OntSpecification; +import org.apache.jena.ontapi.model.OntModel; +import org.apache.jena.rdf.model.Property; + import org.apache.jena.rdf.model.Resource; /** @@ -31,8 +30,13 @@ */ public class FOAF { + + static + { + org.apache.jena.sys.JenaSystem.init(); // ensure Jena (RDFS vocab) is initialized before ontapi touches it + } /** The RDF model that holds the vocabulary terms */ - private static OntModel m_model = ModelFactory.createOntologyModel(OntModelSpec.OWL_MEM, null); + private static OntModel m_model = OntModelFactory.createModel(OntSpecification.OWL2_FULL_MEM); /** The namespace of the vocabulary as a string */ public static final String NS = "http://xmlns.com/foaf/0.1/"; @@ -52,45 +56,45 @@ public static String getURI() public static final Resource NAMESPACE = m_model.createResource( NS ); /** Agent class */ - public static final OntClass Agent = m_model.createClass( NS + "Agent" ); + public static final Resource Agent = m_model.createOntClass( NS + "Agent" ); /** Person class */ - public static final OntClass Person = m_model.createClass( NS + "Person" ); + public static final Resource Person = m_model.createOntClass( NS + "Person" ); /** Document class */ - public static final OntClass Document = m_model.createClass( NS + "Document" ); + public static final Resource Document = m_model.createOntClass( NS + "Document" ); /** Name property */ - public static final DatatypeProperty name = m_model.createDatatypeProperty( NS + "name" ); + public static final Property name = m_model.createDataProperty( NS + "name" ); /** Given name property */ - public static final DatatypeProperty givenName = m_model.createDatatypeProperty( NS + "givenName" ); + public static final Property givenName = m_model.createDataProperty( NS + "givenName" ); /** Family name property */ - public static final DatatypeProperty familyName = m_model.createDatatypeProperty( NS + "familyName" ); + public static final Property familyName = m_model.createDataProperty( NS + "familyName" ); /** Mailbox property */ - public static final ObjectProperty mbox = m_model.createObjectProperty( NS + "mbox" ); + public static final Property mbox = m_model.createObjectProperty( NS + "mbox" ); /** Based near property */ - public static final ObjectProperty based_near = m_model.createObjectProperty( NS + "based_near" ); + public static final Property based_near = m_model.createObjectProperty( NS + "based_near" ); /** Member property */ - public static final ObjectProperty member = m_model.createObjectProperty( NS + "member" ); + public static final Property member = m_model.createObjectProperty( NS + "member" ); /** Primary topic property */ - public static final ObjectProperty primaryTopic = m_model.createObjectProperty( NS + "primaryTopic" ); + public static final Property primaryTopic = m_model.createObjectProperty( NS + "primaryTopic" ); /** Is primary topic of property */ - public static final ObjectProperty isPrimaryTopicOf = m_model.createObjectProperty( NS + "isPrimaryTopicOf" ); + public static final Property isPrimaryTopicOf = m_model.createObjectProperty( NS + "isPrimaryTopicOf" ); /** Maker property */ - public static final ObjectProperty maker = m_model.createObjectProperty( NS + "maker" ); + public static final Property maker = m_model.createObjectProperty( NS + "maker" ); /** Account property */ - public static final ObjectProperty account = m_model.createObjectProperty( NS + "account" ); + public static final Property account = m_model.createObjectProperty( NS + "account" ); /** Image property */ - public static final ObjectProperty img = m_model.createObjectProperty( NS + "img" ); + public static final Property img = m_model.createObjectProperty( NS + "img" ); } diff --git a/src/main/java/com/atomgraph/linkeddatahub/vocabulary/Google.java b/src/main/java/com/atomgraph/linkeddatahub/vocabulary/Google.java index cd68c45f5e..6a21142b4e 100644 --- a/src/main/java/com/atomgraph/linkeddatahub/vocabulary/Google.java +++ b/src/main/java/com/atomgraph/linkeddatahub/vocabulary/Google.java @@ -1,9 +1,10 @@ package com.atomgraph.linkeddatahub.vocabulary; -import org.apache.jena.ontology.DatatypeProperty; -import org.apache.jena.ontology.OntModel; -import org.apache.jena.ontology.OntModelSpec; -import org.apache.jena.rdf.model.ModelFactory; +import org.apache.jena.ontapi.OntModelFactory; +import org.apache.jena.ontapi.OntSpecification; +import org.apache.jena.ontapi.model.OntModel; +import org.apache.jena.rdf.model.Property; + import org.apache.jena.rdf.model.Resource; /** @@ -14,8 +15,13 @@ public class Google { + static + { + org.apache.jena.sys.JenaSystem.init(); // ensure Jena (RDFS vocab) is initialized before ontapi touches it + } + /** The RDF model that holds the vocabulary terms */ - private static OntModel m_model = ModelFactory.createOntologyModel(OntModelSpec.OWL_MEM, null); + private static OntModel m_model = OntModelFactory.createModel(OntSpecification.OWL2_FULL_MEM); /** The namespace of the vocabulary as a string */ public static final String NS = "https://w3id.org/atomgraph/linkeddatahub/services/google#"; @@ -35,9 +41,9 @@ public static String getURI() public static final Resource NAMESPACE = m_model.createResource( NS ); /** Client ID property */ - public static final DatatypeProperty clientID = m_model.createDatatypeProperty( NS + "clientID" ); + public static final Property clientID = m_model.createDataProperty( NS + "clientID" ); /** Client secret property */ - public static final DatatypeProperty clientSecret = m_model.createDatatypeProperty( NS + "clientSecret" ); + public static final Property clientSecret = m_model.createDataProperty( NS + "clientSecret" ); } diff --git a/src/main/java/com/atomgraph/linkeddatahub/vocabulary/LACL.java b/src/main/java/com/atomgraph/linkeddatahub/vocabulary/LACL.java index fd2af9e58f..bf4123de90 100644 --- a/src/main/java/com/atomgraph/linkeddatahub/vocabulary/LACL.java +++ b/src/main/java/com/atomgraph/linkeddatahub/vocabulary/LACL.java @@ -16,12 +16,11 @@ */ package com.atomgraph.linkeddatahub.vocabulary; -import org.apache.jena.ontology.DatatypeProperty; -import org.apache.jena.ontology.ObjectProperty; -import org.apache.jena.ontology.OntClass; -import org.apache.jena.ontology.OntModel; -import org.apache.jena.ontology.OntModelSpec; -import org.apache.jena.rdf.model.ModelFactory; +import org.apache.jena.ontapi.OntModelFactory; +import org.apache.jena.ontapi.OntSpecification; +import org.apache.jena.ontapi.model.OntModel; +import org.apache.jena.rdf.model.Property; + import org.apache.jena.rdf.model.Resource; /** @@ -31,8 +30,13 @@ */ public class LACL { + + static + { + org.apache.jena.sys.JenaSystem.init(); // ensure Jena (RDFS vocab) is initialized before ontapi touches it + } /** The RDF model that holds the vocabulary terms */ - private static OntModel m_model = ModelFactory.createOntologyModel(OntModelSpec.OWL_MEM, null); + private static OntModel m_model = OntModelFactory.createModel(OntSpecification.OWL2_FULL_MEM); /** The namespace of the vocabulary as a string */ public static final String NS = "https://w3id.org/atomgraph/linkeddatahub/admin/acl#"; @@ -52,33 +56,33 @@ public static String getURI() public static final Resource NAMESPACE = m_model.createResource( NS ); /** Authorization request class */ - public static final OntClass AuthorizationRequest = m_model.createClass( NS + "AuthorizationRequest" ); + public static final Resource AuthorizationRequest = m_model.createOntClass( NS + "AuthorizationRequest" ); /** Authorization request class */ - public static final OntClass OwnerAuthorization = m_model.createClass( NS + "OwnerAuthorization" ); + public static final Resource OwnerAuthorization = m_model.createOntClass( NS + "OwnerAuthorization" ); /** Password property */ - public static final DatatypeProperty password = m_model.createDatatypeProperty( NS + "password" ); + public static final Property password = m_model.createDataProperty( NS + "password" ); /** Issuer property */ - public static final DatatypeProperty issuer = m_model.createDatatypeProperty( NS + "issuer" ); + public static final Property issuer = m_model.createDataProperty( NS + "issuer" ); /** Request agent property **/ - public static final ObjectProperty requestMode = m_model.createObjectProperty( NS + "requestMode" ); + public static final Property requestMode = m_model.createObjectProperty( NS + "requestMode" ); /** Request agent property **/ - public static final ObjectProperty requestAgent = m_model.createObjectProperty( NS + "requestAgent" ); + public static final Property requestAgent = m_model.createObjectProperty( NS + "requestAgent" ); /** Request agent group property **/ - public static final ObjectProperty requestAgentGroup = m_model.createObjectProperty( NS + "requestAgentGroup" ); + public static final Property requestAgentGroup = m_model.createObjectProperty( NS + "requestAgentGroup" ); /** Request access to property */ - public static final ObjectProperty requestAccessTo = m_model.createObjectProperty( NS + "requestAccessTo" ); + public static final Property requestAccessTo = m_model.createObjectProperty( NS + "requestAccessTo" ); /** Request access to class property */ - public static final ObjectProperty requestAccessToClass = m_model.createObjectProperty( NS + "requestAccessToClass" ); + public static final Property requestAccessToClass = m_model.createObjectProperty( NS + "requestAccessToClass" ); /** Request access property */ - public static final ObjectProperty requestAccess = m_model.createObjectProperty( NS + "requestAccess" ); + public static final Property requestAccess = m_model.createObjectProperty( NS + "requestAccess" ); } diff --git a/src/main/java/com/atomgraph/linkeddatahub/vocabulary/LAPP.java b/src/main/java/com/atomgraph/linkeddatahub/vocabulary/LAPP.java index 7ef0a28f12..9e991e04fe 100644 --- a/src/main/java/com/atomgraph/linkeddatahub/vocabulary/LAPP.java +++ b/src/main/java/com/atomgraph/linkeddatahub/vocabulary/LAPP.java @@ -16,12 +16,11 @@ */ package com.atomgraph.linkeddatahub.vocabulary; -import org.apache.jena.ontology.DatatypeProperty; -import org.apache.jena.ontology.ObjectProperty; -import org.apache.jena.ontology.OntClass; -import org.apache.jena.ontology.OntModel; -import org.apache.jena.ontology.OntModelSpec; -import org.apache.jena.rdf.model.ModelFactory; +import org.apache.jena.ontapi.OntModelFactory; +import org.apache.jena.ontapi.OntSpecification; +import org.apache.jena.ontapi.model.OntModel; +import org.apache.jena.rdf.model.Property; + import org.apache.jena.rdf.model.Resource; /** @@ -32,8 +31,13 @@ public class LAPP { + static + { + org.apache.jena.sys.JenaSystem.init(); // ensure Jena (RDFS vocab) is initialized before ontapi touches it + } + /** The RDF model that holds the vocabulary terms */ - private static OntModel m_model = ModelFactory.createOntologyModel(OntModelSpec.OWL_MEM, null); + private static OntModel m_model = OntModelFactory.createModel(OntSpecification.OWL2_FULL_MEM); /** The namespace of the vocabulary as a string */ public static final String NS = "https://w3id.org/atomgraph/linkeddatahub/apps#"; @@ -53,45 +57,45 @@ public static String getURI() public static final Resource NAMESPACE = m_model.createResource( NS ); /** Application class */ - public static final OntClass Context = m_model.createClass( NS + "Context" ); + public static final Resource Context = m_model.createOntClass( NS + "Context" ); /** Dataset class */ - public static final OntClass Dataset = m_model.createClass( NS + "Dataset" ); + public static final Resource Dataset = m_model.createOntClass( NS + "Dataset" ); /** Application class */ - public static final OntClass Application = m_model.createClass( NS + "Application" ); + public static final Resource Application = m_model.createOntClass( NS + "Application" ); /** Admin application class */ - public static final OntClass AdminApplication = m_model.createClass( NS + "AdminApplication" ); + public static final Resource AdminApplication = m_model.createOntClass( NS + "AdminApplication" ); /** End-user application class */ - public static final OntClass EndUserApplication = m_model.createClass( NS + "EndUserApplication" ); + public static final Resource EndUserApplication = m_model.createOntClass( NS + "EndUserApplication" ); /** Package class */ - public static final OntClass Package = m_model.createClass( NS + "Package" ); + public static final Resource Package = m_model.createOntClass( NS + "Package" ); /** Admin application class */ -// public static final ObjectProperty adminApplication = m_model.createObjectProperty( NS + "adminApplication" ); +// public static final Property adminApplication = m_model.createObjectProperty( NS + "adminApplication" ); // // /** End-user application class */ -// public static final ObjectProperty endUserApplication = m_model.createObjectProperty( NS + "endUserApplication" ); +// public static final Property endUserApplication = m_model.createObjectProperty( NS + "endUserApplication" ); /** Frontend proxy property */ - public static final ObjectProperty frontendProxy = m_model.createObjectProperty( NS + "frontendProxy" ); + public static final Property frontendProxy = m_model.createObjectProperty( NS + "frontendProxy" ); /** Backend proxy property */ - public static final ObjectProperty backendProxy = m_model.createObjectProperty( NS + "backendProxy" ); + public static final Property backendProxy = m_model.createObjectProperty( NS + "backendProxy" ); /** Prefix property */ - public static final ObjectProperty prefix = m_model.createObjectProperty( NS + "prefix" ); + public static final Property prefix = m_model.createObjectProperty( NS + "prefix" ); /** Read-only property */ - public static final DatatypeProperty allowRead = m_model.createDatatypeProperty( NS + "allowRead" ); + public static final Property allowRead = m_model.createDataProperty( NS + "allowRead" ); /** Origin property for subdomain-based application matching */ - public static final ObjectProperty origin = m_model.createObjectProperty(NS + "origin"); + public static final Property origin = m_model.createObjectProperty(NS + "origin"); /** Application property (for Link header rel) */ - public static final ObjectProperty application = m_model.createObjectProperty( NS + "application" ); + public static final Property application = m_model.createObjectProperty( NS + "application" ); } diff --git a/src/main/java/com/atomgraph/linkeddatahub/vocabulary/LDH.java b/src/main/java/com/atomgraph/linkeddatahub/vocabulary/LDH.java index 629fe4d4af..a55c1fbce7 100644 --- a/src/main/java/com/atomgraph/linkeddatahub/vocabulary/LDH.java +++ b/src/main/java/com/atomgraph/linkeddatahub/vocabulary/LDH.java @@ -16,12 +16,11 @@ */ package com.atomgraph.linkeddatahub.vocabulary; -import org.apache.jena.ontology.DatatypeProperty; -import org.apache.jena.ontology.ObjectProperty; -import org.apache.jena.ontology.OntClass; -import org.apache.jena.ontology.OntModel; -import org.apache.jena.ontology.OntModelSpec; -import org.apache.jena.rdf.model.ModelFactory; +import org.apache.jena.ontapi.OntModelFactory; +import org.apache.jena.ontapi.OntSpecification; +import org.apache.jena.ontapi.model.OntModel; +import org.apache.jena.rdf.model.Property; + import org.apache.jena.rdf.model.Resource; /** @@ -31,8 +30,13 @@ */ public class LDH { + + static + { + org.apache.jena.sys.JenaSystem.init(); // ensure Jena (RDFS vocab) is initialized before ontapi touches it + } /** The RDF model that holds the vocabulary terms */ - private static OntModel m_model = ModelFactory.createOntologyModel(OntModelSpec.OWL_MEM, null); + private static OntModel m_model = OntModelFactory.createModel(OntSpecification.OWL2_FULL_MEM); /** The namespace of the vocabulary as a string */ public static final String NS = "https://w3id.org/atomgraph/linkeddatahub#"; @@ -51,62 +55,62 @@ public static String getURI() public static final Resource NAMESPACE = m_model.createResource( NS ); /** Dataset class */ - public static final OntClass Dataset = m_model.createClass(NS + "Dataset"); + public static final Resource Dataset = m_model.createOntClass(NS + "Dataset"); /** Generic service class */ - public static final OntClass GenericService = m_model.createClass(NS + "GenericService"); + public static final Resource GenericService = m_model.createOntClass(NS + "GenericService"); /** Import class */ - public static final OntClass Import = m_model.createClass(NS + "Import"); + public static final Resource Import = m_model.createOntClass(NS + "Import"); /** CSV import class */ - public static final OntClass CSVImport = m_model.createClass(NS + "CSVImport"); + public static final Resource CSVImport = m_model.createOntClass(NS + "CSVImport"); /** RDF import class */ - public static final OntClass RDFImport = m_model.createClass(NS + "RDFImport"); + public static final Resource RDFImport = m_model.createOntClass(NS + "RDFImport"); /** File class */ - public static final OntClass File = m_model.createClass(NS + "File"); + public static final Resource File = m_model.createOntClass(NS + "File"); /** Object class */ - public static final OntClass Object = m_model.createClass(NS + "Object"); + public static final Resource Object = m_model.createOntClass(NS + "Object"); /** View class */ - public static final OntClass View = m_model.createClass(NS + "View"); + public static final Resource View = m_model.createOntClass(NS + "View"); /** URI syntax violation class */ - public static final OntClass URISyntaxViolation = m_model.createClass(NS + "URISyntaxViolation"); + public static final Resource URISyntaxViolation = m_model.createOntClass(NS + "URISyntaxViolation"); /** Base property */ - public static final ObjectProperty base = m_model.createObjectProperty( NS + "base" ); + public static final Property base = m_model.createObjectProperty( NS + "base" ); /** File property */ - public static final ObjectProperty file = m_model.createObjectProperty( NS + "file" ); + public static final Property file = m_model.createObjectProperty( NS + "file" ); /** Action property */ - public static final ObjectProperty action = m_model.createObjectProperty( NS + "action" ); + public static final Property action = m_model.createObjectProperty( NS + "action" ); /** Delimiter property */ - public static final DatatypeProperty delimiter = m_model.createDatatypeProperty( NS + "delimiter" ); + public static final Property delimiter = m_model.createDataProperty( NS + "delimiter" ); /** Violation value property */ - public static final DatatypeProperty violationValue = m_model.createDatatypeProperty( NS + "violationValue" ); + public static final Property violationValue = m_model.createDataProperty( NS + "violationValue" ); /** Request URI property */ - public static final ObjectProperty requestUri = m_model.createObjectProperty(NS + "requestUri"); + public static final Property requestUri = m_model.createObjectProperty(NS + "requestUri"); /** HTTP headers property */ - public static final ObjectProperty httpHeaders = m_model.createObjectProperty(NS + "httpHeaders"); + public static final Property httpHeaders = m_model.createObjectProperty(NS + "httpHeaders"); /** Service property */ - public static final ObjectProperty service = m_model.createObjectProperty( NS + "service" ); + public static final Property service = m_model.createObjectProperty( NS + "service" ); /** * For shape property */ - public static final ObjectProperty forShape = m_model.createObjectProperty( NS + "forShape" ); + public static final Property forShape = m_model.createObjectProperty( NS + "forShape" ); /** * Import property - used to import packages into an application */ - public static final ObjectProperty importPackage = m_model.createObjectProperty( NS + "import" ); + public static final Property importPackage = m_model.createObjectProperty( NS + "import" ); } diff --git a/src/main/java/com/atomgraph/linkeddatahub/vocabulary/LDHC.java b/src/main/java/com/atomgraph/linkeddatahub/vocabulary/LDHC.java index 5daf5c3899..e808098ae1 100644 --- a/src/main/java/com/atomgraph/linkeddatahub/vocabulary/LDHC.java +++ b/src/main/java/com/atomgraph/linkeddatahub/vocabulary/LDHC.java @@ -16,11 +16,11 @@ */ package com.atomgraph.linkeddatahub.vocabulary; -import org.apache.jena.ontology.DatatypeProperty; -import org.apache.jena.ontology.ObjectProperty; -import org.apache.jena.ontology.OntModel; -import org.apache.jena.ontology.OntModelSpec; -import org.apache.jena.rdf.model.ModelFactory; +import org.apache.jena.ontapi.OntModelFactory; +import org.apache.jena.ontapi.OntSpecification; +import org.apache.jena.ontapi.model.OntModel; +import org.apache.jena.rdf.model.Property; + import org.apache.jena.rdf.model.Resource; /** @@ -30,8 +30,13 @@ */ public class LDHC { + + static + { + org.apache.jena.sys.JenaSystem.init(); // ensure Jena (RDFS vocab) is initialized before ontapi touches it + } /** The RDF model that holds the vocabulary terms */ - private static OntModel m_model = ModelFactory.createOntologyModel(OntModelSpec.OWL_MEM, null); + private static OntModel m_model = OntModelFactory.createModel(OntSpecification.OWL2_FULL_MEM); /** The namespace of the vocabulary as a string */ public static final String NS = "https://w3id.org/atomgraph/linkeddatahub/config#"; @@ -51,138 +56,138 @@ public static String getURI() public static final Resource NAMESPACE = m_model.createResource( NS ); /** Base URI property */ - public static final ObjectProperty baseUri = m_model.createObjectProperty( NS + "baseUri" ); + public static final Property baseUri = m_model.createObjectProperty( NS + "baseUri" ); /** Proxy scheme property */ - public static final DatatypeProperty proxyScheme = m_model.createDatatypeProperty( NS + "proxyScheme" ); + public static final Property proxyScheme = m_model.createDataProperty( NS + "proxyScheme" ); /** Proxy host property */ - public static final DatatypeProperty proxyHost = m_model.createDatatypeProperty( NS + "proxyHost" ); + public static final Property proxyHost = m_model.createDataProperty( NS + "proxyHost" ); /** Proxy port property */ - public static final DatatypeProperty proxyPort = m_model.createDatatypeProperty( NS + "proxyPort" ); + public static final Property proxyPort = m_model.createDataProperty( NS + "proxyPort" ); /** Document type query property */ - public static final DatatypeProperty documentTypeQuery = m_model.createDatatypeProperty( NS + "documentTypeQuery" ); + public static final Property documentTypeQuery = m_model.createDataProperty( NS + "documentTypeQuery" ); /** Document owner query property */ - public static final DatatypeProperty documentOwnerQuery = m_model.createDatatypeProperty( NS + "documentOwnerQuery" ); + public static final Property documentOwnerQuery = m_model.createDataProperty( NS + "documentOwnerQuery" ); /** ACL query property */ - public static final DatatypeProperty aclQuery = m_model.createDatatypeProperty( NS + "aclQuery" ); + public static final Property aclQuery = m_model.createDataProperty( NS + "aclQuery" ); /** Owner's ACL property */ - public static final DatatypeProperty ownerAclQuery = m_model.createDatatypeProperty( NS + "ownerAclQuery" ); + public static final Property ownerAclQuery = m_model.createDataProperty( NS + "ownerAclQuery" ); /** WebID query property */ - public static final DatatypeProperty webIDQuery = m_model.createDatatypeProperty( NS + "webIDQuery" ); + public static final Property webIDQuery = m_model.createDataProperty( NS + "webIDQuery" ); /** Agent query property */ - public static final DatatypeProperty agentQuery = m_model.createDatatypeProperty( NS + "agentQuery" ); + public static final Property agentQuery = m_model.createDataProperty( NS + "agentQuery" ); /** User account query property */ - public static final DatatypeProperty userAccountQuery = m_model.createDatatypeProperty( NS + "userAccountQuery" ); + public static final Property userAccountQuery = m_model.createDataProperty( NS + "userAccountQuery" ); /** Ontology query property */ - public static final DatatypeProperty ontologyQuery = m_model.createDatatypeProperty( NS + "ontologyQuery" ); + public static final Property ontologyQuery = m_model.createDataProperty( NS + "ontologyQuery" ); /** Upload root property */ - public static final ObjectProperty uploadRoot = m_model.createObjectProperty( NS + "uploadRoot" ); + public static final Property uploadRoot = m_model.createObjectProperty( NS + "uploadRoot" ); /** Invalidate cache property */ - public static final DatatypeProperty invalidateCache = m_model.createDatatypeProperty( NS + "invalidateCache" ); + public static final Property invalidateCache = m_model.createDataProperty( NS + "invalidateCache" ); /** Cookie max age property */ - public static final DatatypeProperty cookieMaxAge = m_model.createDatatypeProperty( NS + "cookieMaxAge" ); + public static final Property cookieMaxAge = m_model.createDataProperty( NS + "cookieMaxAge" ); /** Client keystore property */ - public static final ObjectProperty clientKeyStore = m_model.createObjectProperty( NS + "clientKeyStore" ); + public static final Property clientKeyStore = m_model.createObjectProperty( NS + "clientKeyStore" ); /** Client keystore password property */ - public static final DatatypeProperty clientKeyStorePassword = m_model.createDatatypeProperty( NS + "clientKeyStorePassword" ); + public static final Property clientKeyStorePassword = m_model.createDataProperty( NS + "clientKeyStorePassword" ); /** Secretary cert alias property */ - public static final DatatypeProperty secretaryCertAlias = m_model.createDatatypeProperty( NS + "secretaryCertAlias" ); + public static final Property secretaryCertAlias = m_model.createDataProperty( NS + "secretaryCertAlias" ); /** Client truststore property */ - public static final ObjectProperty clientTrustStore = m_model.createObjectProperty( NS + "clientTrustStore" ); + public static final Property clientTrustStore = m_model.createObjectProperty( NS + "clientTrustStore" ); /** Client truststore password property */ - public static final DatatypeProperty clientTrustStorePassword = m_model.createDatatypeProperty( NS + "clientTrustStorePassword" ); + public static final Property clientTrustStorePassword = m_model.createDataProperty( NS + "clientTrustStorePassword" ); /** Signup email subject property */ - public static final DatatypeProperty signUpEMailSubject = m_model.createDatatypeProperty( NS + "signUpEMailSubject" ); + public static final Property signUpEMailSubject = m_model.createDataProperty( NS + "signUpEMailSubject" ); /** WebID signup email text property */ - public static final DatatypeProperty webIDSignUpEMailText = m_model.createDatatypeProperty( NS + "webIDSignUpEMailText" ); + public static final Property webIDSignUpEMailText = m_model.createDataProperty( NS + "webIDSignUpEMailText" ); /** OAuth signup email text property */ - public static final DatatypeProperty oAuthSignUpEMailText = m_model.createDatatypeProperty( NS + "oAuthSignUpEMailText" ); + public static final Property oAuthSignUpEMailText = m_model.createDataProperty( NS + "oAuthSignUpEMailText" ); /** Notification address property */ - public static final DatatypeProperty notificationAddress = m_model.createDatatypeProperty( NS + "notificationAddress" ); + public static final Property notificationAddress = m_model.createDataProperty( NS + "notificationAddress" ); /** Request access email subject property */ - public static final DatatypeProperty requestAccessEMailSubject = m_model.createDatatypeProperty( NS + "requestAccessEMailSubject" ); + public static final Property requestAccessEMailSubject = m_model.createDataProperty( NS + "requestAccessEMailSubject" ); /** Request access email text property */ - public static final DatatypeProperty requestAccessEMailText = m_model.createDatatypeProperty( NS + "requestAccessEMailText" ); + public static final Property requestAccessEMailText = m_model.createDataProperty( NS + "requestAccessEMailText" ); /** Authorization email subject property */ - public static final DatatypeProperty authorizationEMailSubject = m_model.createDatatypeProperty( NS + "authorizationEMailSubject" ); + public static final Property authorizationEMailSubject = m_model.createDataProperty( NS + "authorizationEMailSubject" ); /** Authorization email text property */ - public static final DatatypeProperty authorizationEMailText = m_model.createDatatypeProperty( NS + "authorizationEMailText" ); + public static final Property authorizationEMailText = m_model.createDataProperty( NS + "authorizationEMailText" ); /** Signup cert validity property */ - public static final DatatypeProperty signUpCertValidity = m_model.createDatatypeProperty( NS + "signUpCertValidity" ); + public static final Property signUpCertValidity = m_model.createDataProperty( NS + "signUpCertValidity" ); /** Context dataset property */ - public static final ObjectProperty contextDataset = m_model.createObjectProperty( NS + "contextDataset" ); + public static final Property contextDataset = m_model.createObjectProperty( NS + "contextDataset" ); /** Max connections per route property */ - public static final DatatypeProperty maxConnPerRoute = m_model.createDatatypeProperty( NS + "maxConnPerRoute" ); + public static final Property maxConnPerRoute = m_model.createDataProperty( NS + "maxConnPerRoute" ); /** Max total connections property */ - public static final DatatypeProperty maxTotalConn = m_model.createDatatypeProperty( NS + "maxTotalConn" ); + public static final Property maxTotalConn = m_model.createDataProperty( NS + "maxTotalConn" ); /** Import keep-alive property */ - public static final DatatypeProperty importKeepAlive = m_model.createDatatypeProperty( NS + "importKeepAlive" ); + public static final Property importKeepAlive = m_model.createDataProperty( NS + "importKeepAlive" ); /** HTTP client request retry count */ - public static final DatatypeProperty maxRequestRetries = m_model.createDatatypeProperty( NS + "maxRequestRetries" ); + public static final Property maxRequestRetries = m_model.createDataProperty( NS + "maxRequestRetries" ); /** Timeout in milliseconds waiting for a connection from the HTTP client pool */ - public static final DatatypeProperty connectionRequestTimeout = m_model.createDatatypeProperty( NS + "connectionRequestTimeout" ); + public static final Property connectionRequestTimeout = m_model.createDataProperty( NS + "connectionRequestTimeout" ); /** Max content length property */ - public static final DatatypeProperty maxContentLength = m_model.createDatatypeProperty( NS + "maxContentLength" ); + public static final Property maxContentLength = m_model.createDataProperty( NS + "maxContentLength" ); /** Support languages property */ - public static final DatatypeProperty supportedLanguages = m_model.createDatatypeProperty( NS + "supportedLanguages" ); + public static final Property supportedLanguages = m_model.createDataProperty( NS + "supportedLanguages" ); /** Max import threads property */ - public static final DatatypeProperty maxImportThreads = m_model.createDatatypeProperty( NS + "maxImportThreads" ); + public static final Property maxImportThreads = m_model.createDataProperty( NS + "maxImportThreads" ); /** Enable WebID signup property **/ - public static final DatatypeProperty enableWebIDSignUp = m_model.createDatatypeProperty( NS + "enableWebIDSignUp" ); + public static final Property enableWebIDSignUp = m_model.createDataProperty( NS + "enableWebIDSignUp" ); /** Enable Linked Data proxy property */ - public static final DatatypeProperty enableLinkedDataProxy = m_model.createDatatypeProperty( NS + "enableLinkedDataProxy" ); + public static final Property enableLinkedDataProxy = m_model.createDataProperty( NS + "enableLinkedDataProxy" ); /** Allow internal URLs property */ - public static final DatatypeProperty allowInternalUrls = m_model.createDatatypeProperty( NS + "allowInternalUrls" ); + public static final Property allowInternalUrls = m_model.createDataProperty( NS + "allowInternalUrls" ); /** OIDC refresh token properties property */ - public static final DatatypeProperty oidcRefreshTokens = m_model.createDatatypeProperty( NS + "oidcRefreshTokens" ); + public static final Property oidcRefreshTokens = m_model.createDataProperty( NS + "oidcRefreshTokens" ); /** Frontend proxy URI property (Varnish frontend cache, used for cache invalidation) */ - public static final ObjectProperty frontendProxy = m_model.createObjectProperty( NS + "frontendProxy" ); + public static final Property frontendProxy = m_model.createObjectProperty( NS + "frontendProxy" ); /** Backend proxy URI for the admin SPARQL service (used for cache invalidation and endpoint URI rewriting) */ - public static final ObjectProperty backendProxyAdmin = m_model.createObjectProperty( NS + "backendProxyAdmin" ); + public static final Property backendProxyAdmin = m_model.createObjectProperty( NS + "backendProxyAdmin" ); /** Backend proxy URI for the end-user SPARQL service (used for cache invalidation and endpoint URI rewriting) */ - public static final ObjectProperty backendProxyEndUser = m_model.createObjectProperty( NS + "backendProxyEndUser" ); + public static final Property backendProxyEndUser = m_model.createObjectProperty( NS + "backendProxyEndUser" ); } diff --git a/src/main/java/com/atomgraph/linkeddatahub/vocabulary/LDHT.java b/src/main/java/com/atomgraph/linkeddatahub/vocabulary/LDHT.java index e43185459d..e2fdfc4182 100644 --- a/src/main/java/com/atomgraph/linkeddatahub/vocabulary/LDHT.java +++ b/src/main/java/com/atomgraph/linkeddatahub/vocabulary/LDHT.java @@ -16,11 +16,11 @@ */ package com.atomgraph.linkeddatahub.vocabulary; -import org.apache.jena.ontology.DatatypeProperty; -import org.apache.jena.ontology.ObjectProperty; -import org.apache.jena.ontology.OntModel; -import org.apache.jena.ontology.OntModelSpec; -import org.apache.jena.rdf.model.ModelFactory; +import org.apache.jena.ontapi.OntModelFactory; +import org.apache.jena.ontapi.OntSpecification; +import org.apache.jena.ontapi.model.OntModel; +import org.apache.jena.rdf.model.Property; + import org.apache.jena.rdf.model.Resource; /** @@ -31,8 +31,13 @@ @Deprecated public class LDHT { + + static + { + org.apache.jena.sys.JenaSystem.init(); // ensure Jena (RDFS vocab) is initialized before ontapi touches it + } /** The RDF model that holds the vocabulary terms */ - private static OntModel m_model = ModelFactory.createOntologyModel(OntModelSpec.OWL_MEM, null); + private static OntModel m_model = OntModelFactory.createModel(OntSpecification.OWL2_FULL_MEM); /** The namespace of the vocabulary as a string */ public static final String NS = "https://w3id.org/atomgraph/linkeddatahub/templates#"; @@ -52,9 +57,9 @@ public static String getURI() public static final Resource NAMESPACE = m_model.createResource( NS ); /** For class property */ - //public static final ObjectProperty forClass = m_model.createObjectProperty( NS + "forClass" ); + //public static final Property forClass = m_model.createObjectProperty( NS + "forClass" ); /** Ban property */ - public static final DatatypeProperty ban = m_model.createDatatypeProperty( NS + "ban" ); + public static final Property ban = m_model.createDataProperty( NS + "ban" ); } diff --git a/src/main/java/com/atomgraph/linkeddatahub/vocabulary/NFO.java b/src/main/java/com/atomgraph/linkeddatahub/vocabulary/NFO.java index 6aba1781b3..d828638b6b 100644 --- a/src/main/java/com/atomgraph/linkeddatahub/vocabulary/NFO.java +++ b/src/main/java/com/atomgraph/linkeddatahub/vocabulary/NFO.java @@ -16,11 +16,11 @@ */ package com.atomgraph.linkeddatahub.vocabulary; -import org.apache.jena.ontology.DatatypeProperty; -import org.apache.jena.ontology.OntClass; -import org.apache.jena.ontology.OntModel; -import org.apache.jena.ontology.OntModelSpec; -import org.apache.jena.rdf.model.ModelFactory; +import org.apache.jena.ontapi.OntModelFactory; +import org.apache.jena.ontapi.OntSpecification; +import org.apache.jena.ontapi.model.OntModel; +import org.apache.jena.rdf.model.Property; + import org.apache.jena.rdf.model.Resource; /** @@ -30,8 +30,13 @@ */ public class NFO { + + static + { + org.apache.jena.sys.JenaSystem.init(); // ensure Jena (RDFS vocab) is initialized before ontapi touches it + } /** The RDF model that holds the vocabulary terms */ - private static OntModel m_model = ModelFactory.createOntologyModel(OntModelSpec.OWL_MEM, null); + private static OntModel m_model = OntModelFactory.createModel(OntSpecification.OWL2_FULL_MEM); /** The namespace of the vocabulary as a string */ public static final String NS = "http://www.semanticdesktop.org/ontologies/2007/03/22/nfo#"; @@ -51,9 +56,9 @@ public static String getURI() public static final Resource NAMESPACE = m_model.createResource( NS ); /** File data object class */ - public static final OntClass FileDataObject = m_model.createClass(NS + "FileDataObject"); + public static final Resource FileDataObject = m_model.createOntClass(NS + "FileDataObject"); /** Filename property */ - public static final DatatypeProperty fileName = m_model.createDatatypeProperty( NS + "fileName" ); + public static final Property fileName = m_model.createDataProperty( NS + "fileName" ); } diff --git a/src/main/java/com/atomgraph/linkeddatahub/vocabulary/ORCID.java b/src/main/java/com/atomgraph/linkeddatahub/vocabulary/ORCID.java index 7998fc9073..c64923c8e6 100644 --- a/src/main/java/com/atomgraph/linkeddatahub/vocabulary/ORCID.java +++ b/src/main/java/com/atomgraph/linkeddatahub/vocabulary/ORCID.java @@ -1,9 +1,10 @@ package com.atomgraph.linkeddatahub.vocabulary; -import org.apache.jena.ontology.DatatypeProperty; -import org.apache.jena.ontology.OntModel; -import org.apache.jena.ontology.OntModelSpec; -import org.apache.jena.rdf.model.ModelFactory; +import org.apache.jena.ontapi.OntModelFactory; +import org.apache.jena.ontapi.OntSpecification; +import org.apache.jena.ontapi.model.OntModel; +import org.apache.jena.rdf.model.Property; + import org.apache.jena.rdf.model.Resource; /** @@ -14,8 +15,13 @@ public class ORCID { + static + { + org.apache.jena.sys.JenaSystem.init(); // ensure Jena (RDFS vocab) is initialized before ontapi touches it + } + /** The RDF model that holds the vocabulary terms */ - private static OntModel m_model = ModelFactory.createOntologyModel(OntModelSpec.OWL_MEM, null); + private static OntModel m_model = OntModelFactory.createModel(OntSpecification.OWL2_FULL_MEM); /** The namespace of the vocabulary as a string */ public static final String NS = "https://w3id.org/atomgraph/linkeddatahub/services/orcid#"; @@ -35,9 +41,9 @@ public static String getURI() public static final Resource NAMESPACE = m_model.createResource( NS ); /** Client ID property */ - public static final DatatypeProperty clientID = m_model.createDatatypeProperty( NS + "clientID" ); + public static final Property clientID = m_model.createDataProperty( NS + "clientID" ); /** Client secret property */ - public static final DatatypeProperty clientSecret = m_model.createDatatypeProperty( NS + "clientSecret" ); + public static final Property clientSecret = m_model.createDataProperty( NS + "clientSecret" ); } diff --git a/src/main/java/com/atomgraph/linkeddatahub/vocabulary/PROV.java b/src/main/java/com/atomgraph/linkeddatahub/vocabulary/PROV.java index efd61c6693..fb4655c533 100644 --- a/src/main/java/com/atomgraph/linkeddatahub/vocabulary/PROV.java +++ b/src/main/java/com/atomgraph/linkeddatahub/vocabulary/PROV.java @@ -16,12 +16,11 @@ */ package com.atomgraph.linkeddatahub.vocabulary; -import org.apache.jena.ontology.DatatypeProperty; -import org.apache.jena.ontology.ObjectProperty; -import org.apache.jena.ontology.OntClass; -import org.apache.jena.ontology.OntModel; -import org.apache.jena.ontology.OntModelSpec; -import org.apache.jena.rdf.model.ModelFactory; +import org.apache.jena.ontapi.OntModelFactory; +import org.apache.jena.ontapi.OntSpecification; +import org.apache.jena.ontapi.model.OntModel; +import org.apache.jena.rdf.model.Property; + import org.apache.jena.rdf.model.Resource; /** @@ -30,9 +29,14 @@ * @author Martynas Jusevičius {@literal } */ public class PROV { + + static + { + org.apache.jena.sys.JenaSystem.init(); // ensure Jena (RDFS vocab) is initialized before ontapi touches it + } /** The RDF model that holds the vocabulary terms */ - private static OntModel m_model = ModelFactory.createOntologyModel(OntModelSpec.OWL_MEM, null); + private static OntModel m_model = OntModelFactory.createModel(OntSpecification.OWL2_FULL_MEM); /** The namespace of the vocabulary as a string */ public static final String NS = "http://www.w3.org/ns/prov#"; @@ -52,33 +56,33 @@ public static String getURI() public static final Resource NAMESPACE = m_model.createResource( NS ); /** Entity class */ - public static final OntClass Entity = m_model.createClass( NS + "Entity" ); + public static final Resource Entity = m_model.createOntClass( NS + "Entity" ); /** Activity class */ - public static final OntClass Activity = m_model.createClass( NS + "Activity" ); + public static final Resource Activity = m_model.createOntClass( NS + "Activity" ); /** Agent class */ - public static final OntClass Agent = m_model.createClass( NS + "Agent" ); + public static final Resource Agent = m_model.createOntClass( NS + "Agent" ); /** Was attributed to property */ - public static final ObjectProperty wasAttributedTo = m_model.createObjectProperty( NS + "wasAttributedTo" ); + public static final Property wasAttributedTo = m_model.createObjectProperty( NS + "wasAttributedTo" ); /** Was derived from property */ - public static final ObjectProperty wasDerivedFrom = m_model.createObjectProperty( NS + "wasDerivedFrom" ); + public static final Property wasDerivedFrom = m_model.createObjectProperty( NS + "wasDerivedFrom" ); /** Was generated by property */ - public static final ObjectProperty wasGeneratedBy = m_model.createObjectProperty( NS + "wasGeneratedBy" ); + public static final Property wasGeneratedBy = m_model.createObjectProperty( NS + "wasGeneratedBy" ); /** Was started by property */ - public static final ObjectProperty wasStartedBy = m_model.createObjectProperty( NS + "wasStartedBy" ); + public static final Property wasStartedBy = m_model.createObjectProperty( NS + "wasStartedBy" ); /** Started at time property */ - public static final DatatypeProperty startedAtTime = m_model.createDatatypeProperty( NS + "startedAtTime" ); + public static final Property startedAtTime = m_model.createDataProperty( NS + "startedAtTime" ); /** Ended at time property */ - public static final DatatypeProperty endedAtTime = m_model.createDatatypeProperty( NS + "endedAtTime" ); + public static final Property endedAtTime = m_model.createDataProperty( NS + "endedAtTime" ); /** Generated at time property */ - public static final DatatypeProperty generatedAtTime = m_model.createDatatypeProperty( NS + "generatedAtTime" ); + public static final Property generatedAtTime = m_model.createDataProperty( NS + "generatedAtTime" ); } diff --git a/src/main/java/com/atomgraph/linkeddatahub/vocabulary/SIOC.java b/src/main/java/com/atomgraph/linkeddatahub/vocabulary/SIOC.java index 302ef9f397..6e2f11f0bc 100644 --- a/src/main/java/com/atomgraph/linkeddatahub/vocabulary/SIOC.java +++ b/src/main/java/com/atomgraph/linkeddatahub/vocabulary/SIOC.java @@ -16,8 +16,11 @@ */ package com.atomgraph.linkeddatahub.vocabulary; -import org.apache.jena.ontology.*; -import org.apache.jena.rdf.model.ModelFactory; +import org.apache.jena.ontapi.OntModelFactory; +import org.apache.jena.ontapi.OntSpecification; +import org.apache.jena.ontapi.model.OntModel; +import org.apache.jena.rdf.model.Property; + import org.apache.jena.rdf.model.Resource; /** @@ -27,12 +30,17 @@ */ public class SIOC { + + static + { + org.apache.jena.sys.JenaSystem.init(); // ensure Jena (RDFS vocab) is initialized before ontapi touches it + } /** *

* The ontology model that holds the vocabulary terms *

*/ - private static OntModel m_model = ModelFactory.createOntologyModel(OntModelSpec.OWL_MEM, null); + private static OntModel m_model = OntModelFactory.createModel(OntSpecification.OWL2_FULL_MEM); /** *

@@ -74,7 +82,7 @@ public static String getURI() { * etc. *

*/ - public static final ObjectProperty ABOUT = m_model + public static final Property ABOUT = m_model .createObjectProperty("http://rdfs.org/sioc/ns#about"); /** @@ -82,7 +90,7 @@ public static String getURI() { * Refers to the foaf:Agent or foaf:Person who owns this sioc:User online account. *

*/ - public static final ObjectProperty ACCOUNT_OF = m_model + public static final Property ACCOUNT_OF = m_model .createObjectProperty("http://rdfs.org/sioc/ns#account_of"); /** @@ -90,7 +98,7 @@ public static String getURI() { * A Site that the User is an administrator of. *

*/ - public static final ObjectProperty ADMINISTRATOR_OF = m_model + public static final Property ADMINISTRATOR_OF = m_model .createObjectProperty("http://rdfs.org/sioc/ns#administrator_of"); /** @@ -98,7 +106,7 @@ public static String getURI() { * The URI of a file attached to an Item. *

*/ - public static final ObjectProperty ATTACHMENT = m_model + public static final Property ATTACHMENT = m_model .createObjectProperty("http://rdfs.org/sioc/ns#attachment"); /** @@ -106,7 +114,7 @@ public static String getURI() { * An image or depiction used to represent this User. *

*/ - public static final ObjectProperty AVATAR = m_model + public static final Property AVATAR = m_model .createObjectProperty("http://rdfs.org/sioc/ns#avatar"); /** @@ -114,7 +122,7 @@ public static String getURI() { * An Item that this Container contains. *

*/ - public static final ObjectProperty CONTAINER_OF = m_model + public static final Property CONTAINER_OF = m_model .createObjectProperty("http://rdfs.org/sioc/ns#container_of"); /** @@ -122,7 +130,7 @@ public static String getURI() { * A resource that the User is a creator of. *

*/ - public static final ObjectProperty CREATOR_OF = m_model + public static final Property CREATOR_OF = m_model .createObjectProperty("http://rdfs.org/sioc/ns#creator_of"); /** @@ -130,7 +138,7 @@ public static String getURI() { * An electronic mail address of the User. *

*/ - public static final ObjectProperty EMAIL = m_model + public static final Property EMAIL = m_model .createObjectProperty("http://rdfs.org/sioc/ns#email"); /** @@ -138,7 +146,7 @@ public static String getURI() { * A feed (e.g. RSS, Atom, etc.) pertaining to this resource (e.g. for a Forum, Site, User, etc.). *

*/ - public static final ObjectProperty FEED = m_model + public static final Property FEED = m_model .createObjectProperty("http://rdfs.org/sioc/ns#feed"); /** @@ -147,7 +155,7 @@ public static String getURI() { * updates). *

*/ - public static final ObjectProperty FOLLOWS = m_model + public static final Property FOLLOWS = m_model .createObjectProperty("http://rdfs.org/sioc/ns#follows"); /** @@ -155,13 +163,13 @@ public static String getURI() { * A User who has this Role. *

*/ - public static final ObjectProperty FUNCTION_OF = m_model + public static final Property FUNCTION_OF = m_model .createObjectProperty("http://rdfs.org/sioc/ns#function_of"); /** * This property has been renamed. Use sioc:sioc:usergroup_of instead. */ - public static final ObjectProperty GROUP_OF = m_model + public static final Property GROUP_OF = m_model .createObjectProperty("http://rdfs.org/sioc/ns#group_of"); /** @@ -169,7 +177,7 @@ public static String getURI() { * A User who is an administrator of this Site. *

*/ - public static final ObjectProperty HAS_ADMINISTRATOR = m_model + public static final Property HAS_ADMINISTRATOR = m_model .createObjectProperty("http://rdfs.org/sioc/ns#has_administrator"); /** @@ -177,7 +185,7 @@ public static String getURI() { * The Container to which this Item belongs. *

*/ - public static final ObjectProperty HAS_CONTAINER = m_model + public static final Property HAS_CONTAINER = m_model .createObjectProperty("http://rdfs.org/sioc/ns#has_container"); /** @@ -185,7 +193,7 @@ public static String getURI() { * This is the User who made this resource. *

*/ - public static final ObjectProperty HAS_CREATOR = m_model + public static final Property HAS_CREATOR = m_model .createObjectProperty("http://rdfs.org/sioc/ns#has_creator"); /** @@ -193,7 +201,7 @@ public static String getURI() { * The discussion that is related to this Item. *

*/ - public static final ObjectProperty HAS_DISCUSSION = m_model + public static final Property HAS_DISCUSSION = m_model .createObjectProperty("http://rdfs.org/sioc/ns#has_discussion"); /** @@ -201,13 +209,13 @@ public static String getURI() { * A Role that this User has. *

*/ - public static final ObjectProperty HAS_FUNCTION = m_model + public static final Property HAS_FUNCTION = m_model .createObjectProperty("http://rdfs.org/sioc/ns#has_function"); /** * This property has been renamed. Use sioc:has_usergroup instead. */ - public static final ObjectProperty HAS_GROUP = m_model + public static final Property HAS_GROUP = m_model .createObjectProperty("http://rdfs.org/sioc/ns#has_group"); /** @@ -215,7 +223,7 @@ public static String getURI() { * The Site that hosts this Forum. *

*/ - public static final ObjectProperty HAS_HOST = m_model + public static final Property HAS_HOST = m_model .createObjectProperty("http://rdfs.org/sioc/ns#has_host"); /** @@ -223,7 +231,7 @@ public static String getURI() { * A User who is a member of this Usergroup. *

*/ - public static final ObjectProperty HAS_MEMBER = m_model + public static final Property HAS_MEMBER = m_model .createObjectProperty("http://rdfs.org/sioc/ns#has_member"); /** @@ -231,7 +239,7 @@ public static String getURI() { * A User who is a moderator of this Forum. *

*/ - public static final ObjectProperty HAS_MODERATOR = m_model + public static final Property HAS_MODERATOR = m_model .createObjectProperty("http://rdfs.org/sioc/ns#has_moderator"); /** @@ -239,7 +247,7 @@ public static String getURI() { * A User who modified this Item. *

*/ - public static final ObjectProperty HAS_MODIFIER = m_model + public static final Property HAS_MODIFIER = m_model .createObjectProperty("http://rdfs.org/sioc/ns#has_modifier"); /** @@ -247,7 +255,7 @@ public static String getURI() { * A User that this resource is owned by. *

*/ - public static final ObjectProperty HAS_OWNER = m_model + public static final Property HAS_OWNER = m_model .createObjectProperty("http://rdfs.org/sioc/ns#has_owner"); /** @@ -255,7 +263,7 @@ public static String getURI() { * A Container or Forum that this Container or Forum is a child of. *

*/ - public static final ObjectProperty HAS_PARENT = m_model + public static final Property HAS_PARENT = m_model .createObjectProperty("http://rdfs.org/sioc/ns#has_parent"); /** @@ -263,7 +271,7 @@ public static String getURI() { * An resource that is a part of this subject. *

*/ - public static final ObjectProperty HAS_PART = m_model + public static final Property HAS_PART = m_model .createObjectProperty("http://rdfs.org/sioc/ns#has_part"); /** @@ -271,7 +279,7 @@ public static String getURI() { * Points to an Item or Post that is a reply or response to this Item or Post. *

*/ - public static final ObjectProperty HAS_REPLY = m_model + public static final Property HAS_REPLY = m_model .createObjectProperty("http://rdfs.org/sioc/ns#has_reply"); /** @@ -279,7 +287,7 @@ public static String getURI() { * A resource that this Role applies to. *

*/ - public static final ObjectProperty HAS_SCOPE = m_model + public static final Property HAS_SCOPE = m_model .createObjectProperty("http://rdfs.org/sioc/ns#has_scope"); /** @@ -287,7 +295,7 @@ public static String getURI() { * A data Space which this resource is a part of. *

*/ - public static final ObjectProperty HAS_SPACE = m_model + public static final Property HAS_SPACE = m_model .createObjectProperty("http://rdfs.org/sioc/ns#has_space"); /** @@ -295,7 +303,7 @@ public static String getURI() { * A User who is subscribed to this Container. *

*/ - public static final ObjectProperty HAS_SUBSCRIBER = m_model + public static final Property HAS_SUBSCRIBER = m_model .createObjectProperty("http://rdfs.org/sioc/ns#has_subscriber"); /** @@ -303,7 +311,7 @@ public static String getURI() { * Points to a Usergroup that has certain access to this Space. *

*/ - public static final ObjectProperty HAS_USERGROUP = m_model + public static final Property HAS_USERGROUP = m_model .createObjectProperty("http://rdfs.org/sioc/ns#has_usergroup"); /** @@ -311,7 +319,7 @@ public static String getURI() { * A Forum that is hosted on this Site. *

*/ - public static final ObjectProperty HOST_OF = m_model + public static final Property HOST_OF = m_model .createObjectProperty("http://rdfs.org/sioc/ns#host_of"); /** @@ -319,7 +327,7 @@ public static String getURI() { * Links to the latest revision of this Item or Post. *

*/ - public static final ObjectProperty LATEST_VERSION = m_model + public static final Property LATEST_VERSION = m_model .createObjectProperty("http://rdfs.org/sioc/ns#latest_version"); /** @@ -327,7 +335,7 @@ public static String getURI() { * A URI of a document which contains this SIOC object. *

*/ - public static final ObjectProperty LINK = m_model + public static final Property LINK = m_model .createObjectProperty("http://rdfs.org/sioc/ns#link"); /** @@ -335,7 +343,7 @@ public static String getURI() { * Links extracted from hyperlinks within a SIOC concept, e.g. Post or Site. *

*/ - public static final ObjectProperty LINKS_TO = m_model + public static final Property LINKS_TO = m_model .createObjectProperty("http://rdfs.org/sioc/ns#links_to"); /** @@ -343,7 +351,7 @@ public static String getURI() { * A Usergroup that this User is a member of. *

*/ - public static final ObjectProperty MEMBER_OF = m_model + public static final Property MEMBER_OF = m_model .createObjectProperty("http://rdfs.org/sioc/ns#member_of"); /** @@ -351,7 +359,7 @@ public static String getURI() { * A Forum that User is a moderator of. *

*/ - public static final ObjectProperty MODERATOR_OF = m_model + public static final Property MODERATOR_OF = m_model .createObjectProperty("http://rdfs.org/sioc/ns#moderator_of"); /** @@ -359,7 +367,7 @@ public static String getURI() { * An Item that this User has modified. *

*/ - public static final ObjectProperty MODIFIER_OF = m_model + public static final Property MODIFIER_OF = m_model .createObjectProperty("http://rdfs.org/sioc/ns#modifier_of"); /** @@ -367,7 +375,7 @@ public static String getURI() { * Next Item or Post in a given Container sorted by date. *

*/ - public static final ObjectProperty NEXT_BY_DATE = m_model + public static final Property NEXT_BY_DATE = m_model .createObjectProperty("http://rdfs.org/sioc/ns#next_by_date"); /** @@ -375,7 +383,7 @@ public static String getURI() { * Links to the next revision of this Item or Post. *

*/ - public static final ObjectProperty NEXT_VERSION = m_model + public static final Property NEXT_VERSION = m_model .createObjectProperty("http://rdfs.org/sioc/ns#next_version"); /** @@ -383,7 +391,7 @@ public static String getURI() { * A resource owned by a particular User, for example, a weblog or image gallery. *

*/ - public static final ObjectProperty OWNER_OF = m_model + public static final Property OWNER_OF = m_model .createObjectProperty("http://rdfs.org/sioc/ns#owner_of"); /** @@ -391,7 +399,7 @@ public static String getURI() { * A child Container or Forum that this Container or Forum is a parent of. *

*/ - public static final ObjectProperty PARENT_OF = m_model + public static final Property PARENT_OF = m_model .createObjectProperty("http://rdfs.org/sioc/ns#parent_of"); /** @@ -399,7 +407,7 @@ public static String getURI() { * A resource that the subject is a part of. *

*/ - public static final ObjectProperty PART_OF = m_model + public static final Property PART_OF = m_model .createObjectProperty("http://rdfs.org/sioc/ns#part_of"); /** @@ -407,7 +415,7 @@ public static String getURI() { * Previous Item or Post in a given Container sorted by date. *

*/ - public static final ObjectProperty PREVIOUS_BY_DATE = m_model + public static final Property PREVIOUS_BY_DATE = m_model .createObjectProperty("http://rdfs.org/sioc/ns#previous_by_date"); /** @@ -415,7 +423,7 @@ public static String getURI() { * Links to the previous revision of this Item or Post. *

*/ - public static final ObjectProperty PREVIOUS_VERSION = m_model + public static final Property PREVIOUS_VERSION = m_model .createObjectProperty("http://rdfs.org/sioc/ns#previous_version"); /** @@ -423,7 +431,7 @@ public static String getURI() { * Links either created explicitly or extracted implicitly on the HTML level from the Post. *

*/ - public static final ObjectProperty REFERENCE = m_model + public static final Property REFERENCE = m_model .createObjectProperty("http://rdfs.org/sioc/ns#reference"); /** @@ -431,7 +439,7 @@ public static String getURI() { * Related Posts for this Post, perhaps determined implicitly from topics or references. *

*/ - public static final ObjectProperty RELATED_TO = m_model + public static final Property RELATED_TO = m_model .createObjectProperty("http://rdfs.org/sioc/ns#related_to"); /** @@ -439,7 +447,7 @@ public static String getURI() { * Links to an Item or Post which this Item or Post is a reply to. *

*/ - public static final ObjectProperty REPLY_OF = m_model + public static final Property REPLY_OF = m_model .createObjectProperty("http://rdfs.org/sioc/ns#reply_of"); /** @@ -447,7 +455,7 @@ public static String getURI() { * A Role that has a scope of this resource. *

*/ - public static final ObjectProperty SCOPE_OF = m_model + public static final Property SCOPE_OF = m_model .createObjectProperty("http://rdfs.org/sioc/ns#scope_of"); /** @@ -455,7 +463,7 @@ public static String getURI() { * A resource which belongs to this data Space. *

*/ - public static final ObjectProperty SPACE_OF = m_model + public static final Property SPACE_OF = m_model .createObjectProperty("http://rdfs.org/sioc/ns#space_of"); /** @@ -463,7 +471,7 @@ public static String getURI() { * A Container that a User is subscribed to. *

*/ - public static final ObjectProperty SUBSCRIBER_OF = m_model + public static final Property SUBSCRIBER_OF = m_model .createObjectProperty("http://rdfs.org/sioc/ns#subscriber_of"); /** @@ -472,7 +480,7 @@ public static String getURI() { * SKOS category. *

*/ - public static final ObjectProperty TOPIC = m_model + public static final Property TOPIC = m_model .createObjectProperty("http://rdfs.org/sioc/ns#topic"); /** @@ -480,7 +488,7 @@ public static String getURI() { * A Space that the Usergroup has access to. *

*/ - public static final ObjectProperty USERGROUP_OF = m_model + public static final Property USERGROUP_OF = m_model .createObjectProperty("http://rdfs.org/sioc/ns#usergroup_of"); /** @@ -488,48 +496,48 @@ public static String getURI() { * The content of the Item in plain text format. *

*/ - public static final DatatypeProperty CONTENT = m_model - .createDatatypeProperty("http://rdfs.org/sioc/ns#content"); + public static final Property CONTENT = m_model + .createDataProperty("http://rdfs.org/sioc/ns#content"); /** *

* The encoded content of the Post, contained in CDATA areas. *

*/ - public static final DatatypeProperty CONTENT_ENCODED = m_model - .createDatatypeProperty("http://rdfs.org/sioc/ns#content_encoded"); + public static final Property CONTENT_ENCODED = m_model + .createDataProperty("http://rdfs.org/sioc/ns#content_encoded"); /** *

* When this was created, in ISO 8601 format. *

*/ - public static final DatatypeProperty CREATED_AT = m_model - .createDatatypeProperty("http://rdfs.org/sioc/ns#created_at"); + public static final Property CREATED_AT = m_model + .createDataProperty("http://rdfs.org/sioc/ns#created_at"); /** *

* The content of the Post. *

*/ - public static final DatatypeProperty DESCRIPTION = m_model - .createDatatypeProperty("http://rdfs.org/sioc/ns#description"); + public static final Property DESCRIPTION = m_model + .createDataProperty("http://rdfs.org/sioc/ns#description"); /** *

* An electronic mail address of the User, encoded using SHA1. *

*/ - public static final DatatypeProperty EMAIL_SHA1 = m_model - .createDatatypeProperty("http://rdfs.org/sioc/ns#email_sha1"); + public static final Property EMAIL_SHA1 = m_model + .createDataProperty("http://rdfs.org/sioc/ns#email_sha1"); /** *

* First (real) name of this User. Synonyms include given name or christian name. *

*/ - public static final DatatypeProperty FIRST_NAME = m_model - .createDatatypeProperty("http://rdfs.org/sioc/ns#first_name"); + public static final Property FIRST_NAME = m_model + .createDataProperty("http://rdfs.org/sioc/ns#first_name"); /** *

@@ -537,8 +545,8 @@ public static String getURI() { * of each type of SIOC concept within the same site. *

*/ - public static final DatatypeProperty ID = m_model - .createDatatypeProperty("http://rdfs.org/sioc/ns#id"); + public static final Property ID = m_model + .createDataProperty("http://rdfs.org/sioc/ns#id"); /** *

@@ -546,47 +554,47 @@ public static String getURI() { * articles list the IP addresses for the creator or modifiers when the usernames are absent. *

*/ - public static final DatatypeProperty IP_ADDRESS = m_model - .createDatatypeProperty("http://rdfs.org/sioc/ns#ip_address"); + public static final Property IP_ADDRESS = m_model + .createDataProperty("http://rdfs.org/sioc/ns#ip_address"); /** *

* Last (real) name of this user. Synonyms include surname or family name. *

*/ - public static final DatatypeProperty LAST_NAME = m_model - .createDatatypeProperty("http://rdfs.org/sioc/ns#last_name"); + public static final Property LAST_NAME = m_model + .createDataProperty("http://rdfs.org/sioc/ns#last_name"); /** *

* When this was modified, in ISO 8601 format. *

*/ - public static final DatatypeProperty MODIFIED_AT = m_model - .createDatatypeProperty("http://rdfs.org/sioc/ns#modified_at"); + public static final Property MODIFIED_AT = m_model + .createDataProperty("http://rdfs.org/sioc/ns#modified_at"); /** *

* The name of a SIOC instance, e.g. a username for a User, group name for a Usergroup, etc. *

*/ - public static final DatatypeProperty NAME = m_model - .createDatatypeProperty("http://rdfs.org/sioc/ns#name"); + public static final Property NAME = m_model + .createDataProperty("http://rdfs.org/sioc/ns#name"); /** *

* A note associated with this resource, for example, if it has been edited by a User. *

*/ - public static final DatatypeProperty NOTE = m_model - .createDatatypeProperty("http://rdfs.org/sioc/ns#note"); + public static final Property NOTE = m_model + .createDataProperty("http://rdfs.org/sioc/ns#note"); /** *

* The number of posts that this person has posted. *

*/ - public static final ObjectProperty NUM_POSTS = m_model + public static final Property NUM_POSTS = m_model .createObjectProperty("http://rdfs.org/sioc/ns#num_posts"); /** @@ -595,24 +603,24 @@ public static String getURI() { * structure is absent. *

*/ - public static final DatatypeProperty NUM_REPLIES = m_model - .createDatatypeProperty("http://rdfs.org/sioc/ns#num_replies"); + public static final Property NUM_REPLIES = m_model + .createDataProperty("http://rdfs.org/sioc/ns#num_replies"); /** *

* The number of times this Item, Thread, User profile, etc. has been viewed. *

*/ - public static final DatatypeProperty NUM_VIEWS = m_model - .createDatatypeProperty("http://rdfs.org/sioc/ns#num_views"); + public static final Property NUM_VIEWS = m_model + .createDataProperty("http://rdfs.org/sioc/ns#num_views"); /** *

* Keyword(s) describing subject of the Post. *

*/ - public static final DatatypeProperty SUBJECT = m_model - .createDatatypeProperty("http://rdfs.org/sioc/ns#subject"); + public static final Property SUBJECT = m_model + .createDataProperty("http://rdfs.org/sioc/ns#subject"); /** *

@@ -620,8 +628,8 @@ public static String getURI() { * that has no parents, it would detail the topic thread. *

*/ - public static final DatatypeProperty TITLE = m_model - .createDatatypeProperty("http://rdfs.org/sioc/ns#title"); + public static final Property TITLE = m_model + .createDataProperty("http://rdfs.org/sioc/ns#title"); // Vocabulary classes // ///////////////////////// @@ -631,42 +639,42 @@ public static String getURI() { * Community is a high-level concept that defines an online community and what it consists of. *

*/ - public static final OntClass COMMUNITY = m_model.createClass("http://rdfs.org/sioc/ns#Community"); + public static final Resource COMMUNITY = m_model.createOntClass("http://rdfs.org/sioc/ns#Community"); /** *

* An area in which content Items are contained. *

*/ - public static final OntClass CONTAINER = m_model.createClass("http://rdfs.org/sioc/ns#Container"); + public static final Resource CONTAINER = m_model.createOntClass("http://rdfs.org/sioc/ns#Container"); /** *

* A discussion area on which Posts or entries are made. *

*/ - public static final OntClass FORUM = m_model.createClass("http://rdfs.org/sioc/ns#Forum"); + public static final Resource FORUM = m_model.createOntClass("http://rdfs.org/sioc/ns#Forum"); /** *

* An Item is something which can be in a Container. *

*/ - public static final OntClass ITEM = m_model.createClass("http://rdfs.org/sioc/ns#Item"); + public static final Resource ITEM = m_model.createOntClass("http://rdfs.org/sioc/ns#Item"); /** *

* An article or message that can be posted to a Forum. *

*/ - public static final OntClass POST = m_model.createClass("http://rdfs.org/sioc/ns#Post"); + public static final Resource POST = m_model.createOntClass("http://rdfs.org/sioc/ns#Post"); /** *

* A Role is a function of a User within a scope of a particular Forum, Site, etc. *

*/ - public static final OntClass ROLE = m_model.createClass("http://rdfs.org/sioc/ns#Role"); + public static final Resource ROLE = m_model.createOntClass("http://rdfs.org/sioc/ns#Role"); /** *

@@ -675,28 +683,28 @@ public static String getURI() { * Space. *

*/ - public static final OntClass SITE = m_model.createClass("http://rdfs.org/sioc/ns#Site"); + public static final Resource SITE = m_model.createOntClass("http://rdfs.org/sioc/ns#Site"); /** *

* A Space is a place where data resides, e.g. on a website, desktop, fileshare, etc. *

*/ - public static final OntClass SPACE = m_model.createClass("http://rdfs.org/sioc/ns#Space"); + public static final Resource SPACE = m_model.createOntClass("http://rdfs.org/sioc/ns#Space"); /** *

* A container for a series of threaded discussion Posts or Items. *

*/ - public static final OntClass THREAD = m_model.createClass("http://rdfs.org/sioc/ns#Thread"); + public static final Resource THREAD = m_model.createOntClass("http://rdfs.org/sioc/ns#Thread"); /** *

* A User account in an online community site. *

*/ - public static final OntClass USER_ACCOUNT = m_model.createClass("http://rdfs.org/sioc/ns#UserAccount"); + public static final Resource USER_ACCOUNT = m_model.createOntClass("http://rdfs.org/sioc/ns#UserAccount"); /** *

@@ -704,7 +712,7 @@ public static String getURI() { * control purposes. *

*/ - public static final OntClass USERGROUP = m_model.createClass("http://rdfs.org/sioc/ns#Usergroup"); + public static final Resource USERGROUP = m_model.createOntClass("http://rdfs.org/sioc/ns#Usergroup"); // Vocabulary individuals // ///////////////////////// diff --git a/src/main/java/com/atomgraph/linkeddatahub/vocabulary/VoID.java b/src/main/java/com/atomgraph/linkeddatahub/vocabulary/VoID.java index 9d31bc7a97..12d04d9168 100644 --- a/src/main/java/com/atomgraph/linkeddatahub/vocabulary/VoID.java +++ b/src/main/java/com/atomgraph/linkeddatahub/vocabulary/VoID.java @@ -16,12 +16,11 @@ */ package com.atomgraph.linkeddatahub.vocabulary; -import org.apache.jena.ontology.DatatypeProperty; -import org.apache.jena.ontology.ObjectProperty; -import org.apache.jena.ontology.OntClass; -import org.apache.jena.ontology.OntModel; -import org.apache.jena.ontology.OntModelSpec; -import org.apache.jena.rdf.model.ModelFactory; +import org.apache.jena.ontapi.OntModelFactory; +import org.apache.jena.ontapi.OntSpecification; +import org.apache.jena.ontapi.model.OntModel; +import org.apache.jena.rdf.model.Property; + import org.apache.jena.rdf.model.Resource; /** @@ -30,9 +29,14 @@ * @author Martynas Jusevičius {@literal } */ public class VoID { + + static + { + org.apache.jena.sys.JenaSystem.init(); // ensure Jena (RDFS vocab) is initialized before ontapi touches it + } /** The RDF model that holds the vocabulary terms */ - private static OntModel m_model = ModelFactory.createOntologyModel(OntModelSpec.OWL_MEM, null); + private static OntModel m_model = OntModelFactory.createModel(OntSpecification.OWL2_FULL_MEM); /** The namespace of the vocabulary as a string */ public static final String NS = "http://rdfs.org/ns/void#"; @@ -52,18 +56,18 @@ public static String getURI() public static final Resource NAMESPACE = m_model.createResource( NS ); /** Dataset class */ - public static final OntClass Dataset = m_model.createClass( NS + "Dataset" ); + public static final Resource Dataset = m_model.createOntClass( NS + "Dataset" ); /** Triples property */ - public static final DatatypeProperty triples = m_model.createDatatypeProperty( NS + "triples" ); + public static final Property triples = m_model.createDataProperty( NS + "triples" ); /** Distinct subject property */ - public static final DatatypeProperty distinctSubjects = m_model.createDatatypeProperty( NS + "distinctSubjects" ); + public static final Property distinctSubjects = m_model.createDataProperty( NS + "distinctSubjects" ); /** In dataset property */ - public static final ObjectProperty inDataset = m_model.createObjectProperty( NS + "inDataset" ); + public static final Property inDataset = m_model.createObjectProperty( NS + "inDataset" ); /** Class property */ - public static final ObjectProperty _class = m_model.createObjectProperty( NS + "class" ); + public static final Property _class = m_model.createObjectProperty( NS + "class" ); } diff --git a/src/main/java/com/atomgraph/linkeddatahub/writer/ModelXSLTWriter.java b/src/main/java/com/atomgraph/linkeddatahub/writer/ModelXSLTWriter.java index bdf1ebb5c1..eb7b2daaf8 100644 --- a/src/main/java/com/atomgraph/linkeddatahub/writer/ModelXSLTWriter.java +++ b/src/main/java/com/atomgraph/linkeddatahub/writer/ModelXSLTWriter.java @@ -15,7 +15,7 @@ */ package com.atomgraph.linkeddatahub.writer; -import com.atomgraph.client.util.DataManager; +import com.atomgraph.client.util.RDFSourceResolver; import com.atomgraph.linkeddatahub.apps.model.EndUserApplication; import com.atomgraph.linkeddatahub.model.auth.Agent; import com.atomgraph.linkeddatahub.server.io.ValidatingModelProvider; @@ -41,7 +41,6 @@ import net.sf.saxon.s9api.SaxonApiException; import net.sf.saxon.s9api.XsltExecutable; import org.apache.http.HttpHeaders; -import org.apache.jena.ontology.OntModelSpec; import org.apache.jena.rdf.model.Model; import org.apache.jena.riot.RDFFormat; import org.apache.jena.riot.RDFWriter; @@ -70,9 +69,9 @@ public class ModelXSLTWriter extends XSLTWriterBase implements MessageBodyWriter * @param dataManager RDF data manager * @param messageDigest message digest */ - public ModelXSLTWriter(XsltExecutable xsltExec, OntModelSpec ontModelSpec, DataManager dataManager, MessageDigest messageDigest) + public ModelXSLTWriter(XsltExecutable xsltExec, RDFSourceResolver resolver, MessageDigest messageDigest) { - super(xsltExec, ontModelSpec, dataManager, messageDigest); + super(xsltExec, resolver, messageDigest); } @Override diff --git a/src/main/java/com/atomgraph/linkeddatahub/writer/ResultSetXSLTWriter.java b/src/main/java/com/atomgraph/linkeddatahub/writer/ResultSetXSLTWriter.java index fd7b24cfe0..df0d42d64b 100644 --- a/src/main/java/com/atomgraph/linkeddatahub/writer/ResultSetXSLTWriter.java +++ b/src/main/java/com/atomgraph/linkeddatahub/writer/ResultSetXSLTWriter.java @@ -15,7 +15,7 @@ */ package com.atomgraph.linkeddatahub.writer; -import com.atomgraph.client.util.DataManager; +import com.atomgraph.client.util.RDFSourceResolver; import com.atomgraph.linkeddatahub.model.auth.Agent; import jakarta.inject.Singleton; import jakarta.ws.rs.Produces; @@ -38,7 +38,6 @@ import net.sf.saxon.s9api.SaxonApiException; import net.sf.saxon.s9api.XsltExecutable; import org.apache.http.HttpHeaders; -import org.apache.jena.ontology.OntModelSpec; import org.apache.jena.query.ResultSetFormatter; import org.apache.jena.query.ResultSetRewindable; import org.slf4j.Logger; @@ -65,9 +64,9 @@ public class ResultSetXSLTWriter extends XSLTWriterBase implements MessageBodyWr * @param dataManager RDF data manager * @param messageDigest message digest */ - public ResultSetXSLTWriter(XsltExecutable xsltExec, OntModelSpec ontModelSpec, DataManager dataManager, MessageDigest messageDigest) + public ResultSetXSLTWriter(XsltExecutable xsltExec, RDFSourceResolver resolver, MessageDigest messageDigest) { - super(xsltExec, ontModelSpec, dataManager, messageDigest); + super(xsltExec, resolver, messageDigest); } @Override diff --git a/src/main/java/com/atomgraph/linkeddatahub/writer/XSLTWriterBase.java b/src/main/java/com/atomgraph/linkeddatahub/writer/XSLTWriterBase.java index 50fc80af8f..bbdaf3442c 100644 --- a/src/main/java/com/atomgraph/linkeddatahub/writer/XSLTWriterBase.java +++ b/src/main/java/com/atomgraph/linkeddatahub/writer/XSLTWriterBase.java @@ -15,7 +15,7 @@ */ package com.atomgraph.linkeddatahub.writer; -import com.atomgraph.client.util.DataManager; +import com.atomgraph.client.util.RDFSourceResolver; import com.atomgraph.client.vocabulary.AC; import com.atomgraph.linkeddatahub.writer.factory.xslt.XsltExecutableSupplier; import com.atomgraph.linkeddatahub.model.auth.Agent; @@ -58,10 +58,10 @@ import net.sf.saxon.s9api.XdmValue; import net.sf.saxon.s9api.XsltExecutable; import org.apache.http.HttpHeaders; -import org.apache.jena.ontology.ObjectProperty; -import org.apache.jena.ontology.OntModelSpec; +import org.apache.jena.rdf.model.Property; import org.apache.jena.rdf.model.Model; -import org.apache.jena.riot.RDFLanguages; +import org.apache.jena.riot.RDFFormat; +import org.apache.jena.riot.RDFWriter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -88,7 +88,7 @@ public abstract class XSLTWriterBase extends com.atomgraph.client.writer.XSLTWri @Inject com.atomgraph.linkeddatahub.Application system; @Inject jakarta.inject.Provider> application; - @Inject jakarta.inject.Provider dataManager; + @Inject jakarta.inject.Provider resolver; @Inject jakarta.inject.Provider xsltExecSupplier; @Inject jakarta.inject.Provider crc; @Inject jakarta.inject.Provider> authorizationContext; @@ -97,15 +97,14 @@ public abstract class XSLTWriterBase extends com.atomgraph.client.writer.XSLTWri /** * Constructs XSLT writer. - * + * * @param xsltExec compiled XSLT stylesheet - * @param ontModelSpec ontology specification - * @param dataManager RDF data manager + * @param resolver XSLT source resolver * @param messageDigest message digest */ - public XSLTWriterBase(XsltExecutable xsltExec, OntModelSpec ontModelSpec, DataManager dataManager, MessageDigest messageDigest) + public XSLTWriterBase(XsltExecutable xsltExec, RDFSourceResolver resolver, MessageDigest messageDigest) { - super(xsltExec, ontModelSpec, dataManager); // this DataManager will be unused as we override getDataManager() with the injected (subclassed) one + super(xsltExec, resolver); // this resolver is unused as we override getResolver() with the injected (request-scoped) one this.messageDigest = messageDigest; } @@ -189,7 +188,7 @@ public Map getParameters(MultivaluedMap headerMap, ObjectProperty property) + public URI getLinkURI(MultivaluedMap headerMap, Property property) { if (headerMap.get(jakarta.ws.rs.core.HttpHeaders.LINK) == null) return null; @@ -231,7 +230,10 @@ public StreamSource getSource(Model model) throws IOException try (ByteArrayOutputStream stream = new ByteArrayOutputStream()) { - model.write(stream, RDFLanguages.RDFXML.getName(), null); + RDFWriter.create(). + format(RDFFormat.RDFXML_PLAIN). + source(model). + output(stream); return new StreamSource(new ByteArrayInputStream(stream.toByteArray())); } } @@ -257,36 +259,30 @@ public com.atomgraph.linkeddatahub.Application getSystem() return system; } - @Override - public OntModelSpec getOntModelSpec() - { - return getSystem().getOntModelSpec(); - } - /** * Returns JAX-RS security context. - * + * * @return security context */ public SecurityContext getSecurityContext() { return securityContext; } - + @Override - public DataManager getDataManager() + public RDFSourceResolver getResolver() { - return getDataManagerProvider().get(); + return getResolverProvider().get(); } /** - * Returns a JAX-RS provider for the RDF data manager. + * Returns a JAX-RS provider for the request-scoped source resolver. * * @return provider */ - public jakarta.inject.Provider getDataManagerProvider() + public jakarta.inject.Provider getResolverProvider() { - return dataManager; + return resolver; } @Override diff --git a/src/main/java/com/atomgraph/linkeddatahub/writer/factory/ModeFactory.java b/src/main/java/com/atomgraph/linkeddatahub/writer/factory/ModeFactory.java index e7016b906f..1c70b6fb9b 100644 --- a/src/main/java/com/atomgraph/linkeddatahub/writer/factory/ModeFactory.java +++ b/src/main/java/com/atomgraph/linkeddatahub/writer/factory/ModeFactory.java @@ -18,7 +18,6 @@ import com.atomgraph.client.vocabulary.AC; import com.atomgraph.linkeddatahub.writer.Mode; -import java.util.Collections; import java.util.List; import jakarta.ws.rs.container.ContainerRequestContext; import jakarta.ws.rs.core.Context; diff --git a/src/main/java/com/atomgraph/linkeddatahub/writer/factory/DataManagerFactory.java b/src/main/java/com/atomgraph/linkeddatahub/writer/factory/SourceResolverFactory.java similarity index 70% rename from src/main/java/com/atomgraph/linkeddatahub/writer/factory/DataManagerFactory.java rename to src/main/java/com/atomgraph/linkeddatahub/writer/factory/SourceResolverFactory.java index a9774fd63a..daca9faeb5 100644 --- a/src/main/java/com/atomgraph/linkeddatahub/writer/factory/DataManagerFactory.java +++ b/src/main/java/com/atomgraph/linkeddatahub/writer/factory/SourceResolverFactory.java @@ -16,18 +16,16 @@ */ package com.atomgraph.linkeddatahub.writer.factory; -import org.apache.jena.util.LocationMapper; import jakarta.ws.rs.core.Context; import jakarta.ws.rs.ext.Provider; -import com.atomgraph.client.util.DataManager; +import com.atomgraph.client.util.RDFSourceResolver; +import com.atomgraph.client.util.jena.PrefixGraphRepository; import com.atomgraph.linkeddatahub.apps.model.Application; import com.atomgraph.linkeddatahub.apps.model.EndUserApplication; import com.atomgraph.linkeddatahub.client.GraphStoreClient; import com.atomgraph.linkeddatahub.server.security.AgentContext; import com.atomgraph.linkeddatahub.vocabulary.LAPP; -import com.atomgraph.linkeddatahub.writer.impl.DataManagerImpl; -import java.net.URI; -import java.util.HashMap; +import com.atomgraph.linkeddatahub.writer.impl.SameSiteSourceResolver; import java.util.Optional; import jakarta.inject.Inject; import jakarta.servlet.http.HttpServletRequest; @@ -40,15 +38,15 @@ import org.slf4j.LoggerFactory; /** - * JAX-RS provider for DataManager. - * + * JAX-RS provider for the request-scoped XSLT source resolver. + * * @author Martynas Jusevičius {@literal } - * @see com.atomgraph.client.util.DataManager + * @see com.atomgraph.client.util.RDFSourceResolver */ @Provider -public class DataManagerFactory implements Factory +public class SourceResolverFactory implements Factory { - private static final Logger log = LoggerFactory.getLogger(DataManagerFactory.class); + private static final Logger log = LoggerFactory.getLogger(SourceResolverFactory.class); @Context UriInfo uriInfo; @Context HttpServletRequest httpServletRequest; @@ -58,40 +56,37 @@ public class DataManagerFactory implements Factory @Inject com.atomgraph.linkeddatahub.Application system; @Override - public DataManager provide() + public RDFSourceResolver provide() { - // Always return DataManager, falling back to system DataManager if no Application (e.g., for error responses) - return getDataManager(getApplication()); + // falls back to the global repository if there is no application (e.g. for error responses) + return getResolver(getApplication()); } @Override - public void dispose(DataManager t) + public void dispose(RDFSourceResolver t) { } /** - * Returns RDF data manager. + * Returns the request-scoped source resolver, backed by the application's ontology repository + * (or the global repository) and a delegating Graph Store client. * - * @param appOpt optional end-user application (if empty, system DataManager is used) - * @return data manager + * @param appOpt optional end-user application (if empty, the global repository is used) + * @return source resolver */ - public DataManager getDataManager(Optional appOpt) + public RDFSourceResolver getResolver(Optional appOpt) { - final com.atomgraph.core.util.jena.DataManager baseManager; + final PrefixGraphRepository repository; if (appOpt.isPresent() && appOpt.get().canAs(EndUserApplication.class)) - baseManager = (com.atomgraph.core.util.jena.DataManager)getSystem().getOntModelSpec(appOpt.get().as(EndUserApplication.class)).getDocumentManager().getFileManager(); + repository = getSystem().getRepository(appOpt.get().as(EndUserApplication.class)); else - baseManager = getSystem().getDataManager(); + repository = getSystem().getRepository(); GraphStoreClient gsc = GraphStoreClient.create(getSystem().getClient(), getSystem().getMediaTypes()). delegation(getUriInfo().getBaseUri(), getAgentContext()); - // copy cached models over from the app's FileManager - return new DataManagerImpl(LocationMapper.get(), new HashMap<>(baseManager.getModelCache()), - gsc, true, getSystem().isPreemptiveAuth(), getSystem().isResolvingUncached(), - getSystem().getBaseURI(), - getAgentContext()); + return new SameSiteSourceResolver(repository, gsc, getSystem().isResolvingUncached(), getSystem().getBaseURI()); } /** diff --git a/src/main/java/com/atomgraph/linkeddatahub/writer/impl/DataManagerImpl.java b/src/main/java/com/atomgraph/linkeddatahub/writer/impl/SameSiteSourceResolver.java similarity index 59% rename from src/main/java/com/atomgraph/linkeddatahub/writer/impl/DataManagerImpl.java rename to src/main/java/com/atomgraph/linkeddatahub/writer/impl/SameSiteSourceResolver.java index b62cfefad9..72134d5afc 100644 --- a/src/main/java/com/atomgraph/linkeddatahub/writer/impl/DataManagerImpl.java +++ b/src/main/java/com/atomgraph/linkeddatahub/writer/impl/SameSiteSourceResolver.java @@ -16,67 +16,57 @@ */ package com.atomgraph.linkeddatahub.writer.impl; -import org.apache.jena.util.LocationMapper; -import java.net.URI; +import com.atomgraph.client.util.RDFSourceResolver; import com.atomgraph.core.client.GraphStoreClient; -import com.atomgraph.linkeddatahub.server.security.AgentContext; +import com.atomgraph.client.util.jena.PrefixGraphRepository; import com.google.common.net.InternetDomainName; -import java.util.Map; -import org.apache.jena.rdf.model.Model; +import java.net.URI; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** - * Manager for remote RDF dataset access. - * Documents can be mapped to local copies. - * + * XSLT source resolver that restricts dereferencing of uncached URIs to the same site as the + * application. Mapped (bundled) and same-site URIs resolve; arbitrary external URIs do not. + * Backed by a {@link PrefixGraphRepository}; authenticated delegation is handled by the supplied + * {@link GraphStoreClient}. + * * @author Martynas Jusevičius {@literal } */ -public class DataManagerImpl extends com.atomgraph.client.util.DataManagerImpl +public class SameSiteSourceResolver extends RDFSourceResolver { - private static final Logger log = LoggerFactory.getLogger(DataManagerImpl.class); - + private static final Logger log = LoggerFactory.getLogger(SameSiteSourceResolver.class); + private final URI rootContextURI; - private final AgentContext agentContext; /** - * Constructs RDF data manager. - * - * @param mapper location mapper - * @param modelCache model cache - * @param gsc Graph Store client - * @param cacheModelLoads true if loaded RDF models are cached - * @param preemptiveAuth true if HTTP basic auth is sent preemptively + * Constructs the resolver. + * + * @param repository graph repository (bundled/cached graphs + URI→location mapping) + * @param gsc Graph Store client (with delegation) * @param resolvingUncached true if uncached URLs are resolved * @param rootContextURI the root URI of the JAX-RS application - * @param agentContext agent context */ - public DataManagerImpl(LocationMapper mapper, Map modelCache, - GraphStoreClient gsc, - boolean cacheModelLoads, boolean preemptiveAuth, boolean resolvingUncached, - URI rootContextURI, - AgentContext agentContext) + public SameSiteSourceResolver(PrefixGraphRepository repository, GraphStoreClient gsc, boolean resolvingUncached, URI rootContextURI) { - super(mapper, modelCache, gsc, cacheModelLoads, preemptiveAuth, resolvingUncached); + super(repository, gsc, resolvingUncached); this.rootContextURI = rootContextURI; - this.agentContext = agentContext; } - + @Override - public boolean resolvingUncached(String filenameOrURI) + protected boolean resolvingUncached(String filenameOrURI) { - if (super.resolvingUncached(filenameOrURI) && !isMapped(filenameOrURI)) + if (super.resolvingUncached(filenameOrURI) && !getRepository().isMapped(filenameOrURI)) { // Allow resolving URIs from the same site (e.g., localhost:4443/static/..., admin.localhost:4443/ns) return isSameSite(getRootContextURI(), URI.create(filenameOrURI)); } - return false; // super.resolvingUncached(filenameOrURI); // configured in web.xml + return false; } /** * Checks if two URIs are from the same site (schemeful same-site). - * This allows subdomains like admin.localhost and localhost to be considered part of the same LinkedDataHub instance. + * Allows subdomains like admin.localhost and localhost to be considered part of the same instance. * Ports are ignored per the same-site definition. * * @param uri1 first URI @@ -99,12 +89,9 @@ private boolean isSameSite(URI uri1, URI uri2) InternetDomainName domain1 = InternetDomainName.from(host1); InternetDomainName domain2 = InternetDomainName.from(host2); - // For localhost domains, compare the full host (localhost == localhost, admin.localhost != localhost at domain level) - // But we want to treat them as same root domain, so just check if both end with "localhost" if (host1.equals("localhost") || host1.endsWith(".localhost")) return host2.equals("localhost") || host2.endsWith(".localhost"); - // For regular domains, compare top private domains if (domain1.isTopPrivateDomain() && domain2.isTopPrivateDomain()) return domain1.equals(domain2); if (domain1.hasPublicSuffix() && domain2.hasPublicSuffix()) @@ -114,7 +101,6 @@ private boolean isSameSite(URI uri1, URI uri2) } catch (IllegalArgumentException ex) { - // Invalid domain name, fall back to simple equality check if (log.isDebugEnabled()) log.debug("Could not parse domain names for comparison: {} and {}", host1, host2); return false; } @@ -122,7 +108,7 @@ private boolean isSameSite(URI uri1, URI uri2) /** * Returns the root URI of the JAX-RS application. - * + * * @return root URI */ public URI getRootContextURI() @@ -130,14 +116,4 @@ public URI getRootContextURI() return rootContextURI; } - /** - * Returns the agent context. - * - * @return agent context or null - */ - public AgentContext getAgentContext() - { - return agentContext; - } - -} \ No newline at end of file +} diff --git a/src/main/java/com/atomgraph/server/io/ValidatingDatasetProvider.java b/src/main/java/com/atomgraph/server/io/ValidatingDatasetProvider.java index 500fb2e5af..341e7c7eb3 100644 --- a/src/main/java/com/atomgraph/server/io/ValidatingDatasetProvider.java +++ b/src/main/java/com/atomgraph/server/io/ValidatingDatasetProvider.java @@ -30,7 +30,7 @@ import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.MultivaluedMap; import jakarta.ws.rs.ext.Providers; -import org.apache.jena.ontology.Ontology; +import org.apache.jena.ontapi.model.OntModel; import org.apache.jena.query.Dataset; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -52,7 +52,7 @@ public class ValidatingDatasetProvider extends DatasetProvider @Context private Providers providers; - @Inject jakarta.inject.Provider> ontology; + @Inject jakarta.inject.Provider> ontology; @Override public Dataset readFrom(Class type, Type genericType, Annotation[] annotations, MediaType mediaType, MultivaluedMap httpHeaders, InputStream entityStream) throws IOException @@ -70,7 +70,7 @@ public Dataset validate(Dataset dataset) if (getOntology().isPresent()) { // SPIN validation - Validator validator = new Validator(getOntology().get().getOntModel()); + Validator validator = new Validator(getOntology().get()); List cvs = validator.validate(dataset.getDefaultModel()); if (!cvs.isEmpty()) { @@ -79,7 +79,7 @@ public Dataset validate(Dataset dataset) } // SHACL validation - Shapes shapes = Shapes.parse(getOntology().get().getOntModel().getGraph()); + Shapes shapes = Shapes.parse(getOntology().get().getGraph()); ValidationReport report = ShaclValidator.get().validate(shapes, dataset.getDefaultModel().getGraph()); if (!report.conforms()) { @@ -113,7 +113,7 @@ public Dataset validate(Dataset dataset) return dataset; } - public Optional getOntology() + public Optional getOntology() { return ontology.get(); } diff --git a/src/main/java/com/atomgraph/server/io/ValidatingModelProvider.java b/src/main/java/com/atomgraph/server/io/ValidatingModelProvider.java index 416d8ada1c..1b348127b7 100644 --- a/src/main/java/com/atomgraph/server/io/ValidatingModelProvider.java +++ b/src/main/java/com/atomgraph/server/io/ValidatingModelProvider.java @@ -16,7 +16,7 @@ package com.atomgraph.server.io; -import org.apache.jena.ontology.Ontology; +import org.apache.jena.ontapi.model.OntModel; import org.apache.jena.rdf.model.Model; import java.io.IOException; import java.io.InputStream; @@ -51,7 +51,7 @@ public class ValidatingModelProvider extends BasedModelProvider @Context private Providers providers; - @Inject jakarta.inject.Provider> ontology; + @Inject jakarta.inject.Provider> ontology; @Override public Model readFrom(Class type, Type genericType, Annotation[] annotations, MediaType mediaType, MultivaluedMap httpHeaders, InputStream entityStream) throws IOException @@ -69,7 +69,7 @@ public Model validate(Model model) if (getOntology().isPresent()) { // SPIN validation - List cvs = new Validator(getOntology().get().getOntModel()).validate(model); + List cvs = new Validator(getOntology().get()).validate(model); if (!cvs.isEmpty()) { if (log.isDebugEnabled()) log.debug("SPIN constraint violations: {}", cvs); @@ -77,7 +77,7 @@ public Model validate(Model model) } // SHACL validation - Shapes shapes = Shapes.parse(getOntology().get().getOntModel().getGraph()); + Shapes shapes = Shapes.parse(getOntology().get().getGraph()); ValidationReport report = ShaclValidator.get().validate(shapes, model.getGraph()); if (!report.conforms()) { @@ -100,7 +100,7 @@ public Model processWrite(Model model) return model; } - public Optional getOntology() + public Optional getOntology() { return ontology.get(); } diff --git a/src/main/java/com/atomgraph/server/util/Validator.java b/src/main/java/com/atomgraph/server/util/Validator.java index fc66576e58..67a6afe98f 100644 --- a/src/main/java/com/atomgraph/server/util/Validator.java +++ b/src/main/java/com/atomgraph/server/util/Validator.java @@ -16,7 +16,7 @@ package com.atomgraph.server.util; -import org.apache.jena.ontology.OntModel; +import org.apache.jena.ontapi.model.OntModel; import org.apache.jena.rdf.model.Model; import java.util.List; import org.slf4j.Logger; diff --git a/src/main/java/com/atomgraph/server/vocabulary/HTTP.java b/src/main/java/com/atomgraph/server/vocabulary/HTTP.java index e9316e1eec..316d6e3b9d 100644 --- a/src/main/java/com/atomgraph/server/vocabulary/HTTP.java +++ b/src/main/java/com/atomgraph/server/vocabulary/HTTP.java @@ -16,12 +16,11 @@ package com.atomgraph.server.vocabulary; -import org.apache.jena.ontology.DatatypeProperty; -import org.apache.jena.ontology.ObjectProperty; -import org.apache.jena.ontology.OntClass; -import org.apache.jena.ontology.OntModel; -import org.apache.jena.ontology.OntModelSpec; -import org.apache.jena.rdf.model.ModelFactory; +import org.apache.jena.ontapi.OntModelFactory; +import org.apache.jena.ontapi.OntSpecification; +import org.apache.jena.ontapi.model.OntModel; +import org.apache.jena.rdf.model.Property; + import org.apache.jena.rdf.model.Resource; /** @@ -33,8 +32,13 @@ public class HTTP { + static + { + org.apache.jena.sys.JenaSystem.init(); // ensure Jena (RDFS vocab) is initialized before ontapi touches it + } + /**

The RDF model that holds the vocabulary terms

*/ - private static OntModel m_model = ModelFactory.createOntologyModel(OntModelSpec.OWL_MEM, null); + private static OntModel m_model = OntModelFactory.createModel(OntSpecification.OWL2_FULL_MEM); /**

The namespace of the vocabulary as a string

*/ public static final String NS = "http://www.w3.org/2011/http#"; @@ -49,16 +53,16 @@ public static String getURI() /**

The namespace of the vocabulary as a resource

*/ public static final Resource NAMESPACE = m_model.createResource( NS ); - public static final OntClass Response = m_model.createClass( NS + "Response" ); + public static final Resource Response = m_model.createOntClass( NS + "Response" ); - public static final ObjectProperty sc = m_model.createObjectProperty( NS + "sc" ); + public static final Property sc = m_model.createObjectProperty( NS + "sc" ); - public static final DatatypeProperty statusCodeValue = m_model.createDatatypeProperty( NS + "statusCodeValue" ); + public static final Property statusCodeValue = m_model.createDataProperty( NS + "statusCodeValue" ); - public static final DatatypeProperty reasonPhrase = m_model.createDatatypeProperty( NS + "reasonPhrase" ); + public static final Property reasonPhrase = m_model.createDataProperty( NS + "reasonPhrase" ); - public static final DatatypeProperty absoluteURI = m_model.createDatatypeProperty( NS + "absoluteURI" ); + public static final Property absoluteURI = m_model.createDataProperty( NS + "absoluteURI" ); - public static final DatatypeProperty absolutePath = m_model.createDatatypeProperty( NS + "absolutePath" ); + public static final Property absolutePath = m_model.createDataProperty( NS + "absolutePath" ); } diff --git a/src/main/java/com/atomgraph/server/vocabulary/LDT.java b/src/main/java/com/atomgraph/server/vocabulary/LDT.java index 5b59ee7f8a..28cb52bcf9 100644 --- a/src/main/java/com/atomgraph/server/vocabulary/LDT.java +++ b/src/main/java/com/atomgraph/server/vocabulary/LDT.java @@ -16,12 +16,11 @@ */ package com.atomgraph.server.vocabulary; -import org.apache.jena.ontology.DatatypeProperty; -import org.apache.jena.ontology.ObjectProperty; -import org.apache.jena.ontology.OntClass; -import org.apache.jena.ontology.OntModel; -import org.apache.jena.ontology.OntModelSpec; -import org.apache.jena.rdf.model.ModelFactory; +import org.apache.jena.ontapi.OntModelFactory; +import org.apache.jena.ontapi.OntSpecification; +import org.apache.jena.ontapi.model.OntModel; +import org.apache.jena.rdf.model.Property; + import org.apache.jena.rdf.model.Resource; /** @@ -31,8 +30,13 @@ */ public final class LDT { + + static + { + org.apache.jena.sys.JenaSystem.init(); // ensure Jena (RDFS vocab) is initialized before ontapi touches it + } /**

The RDF model that holds the vocabulary terms

*/ - private static OntModel m_model = ModelFactory.createOntologyModel(OntModelSpec.OWL_MEM, null); + private static OntModel m_model = OntModelFactory.createModel(OntSpecification.OWL2_FULL_MEM); /**

The namespace of the vocabulary as a string

*/ public static final String NS = "https://www.w3.org/ns/ldt#"; @@ -47,51 +51,51 @@ public static String getURI() /**

The namespace of the vocabulary as a resource

*/ public static final Resource NAMESPACE = m_model.createResource( NS ); - public static final OntClass Application = m_model.createClass( NS + "Application" ); + public static final Resource Application = m_model.createOntClass( NS + "Application" ); - public static final OntClass Ontology = m_model.createClass( NS + "Ontology" ); + public static final Resource Ontology = m_model.createOntClass( NS + "Ontology" ); - public static final OntClass Template = m_model.createClass( NS + "Template" ); + public static final Resource Template = m_model.createOntClass( NS + "Template" ); - public static final OntClass Parameter = m_model.createClass( NS + "Parameter" ); + public static final Resource Parameter = m_model.createOntClass( NS + "Parameter" ); - public static final OntClass TemplateCall = m_model.createClass( NS + "TemplateCall" ); + public static final Resource TemplateCall = m_model.createOntClass( NS + "TemplateCall" ); - public static final OntClass Argument = m_model.createClass( NS + "Argument" ); + public static final Resource Argument = m_model.createOntClass( NS + "Argument" ); - public static final ObjectProperty base = m_model.createObjectProperty( NS + "base" ); + public static final Property base = m_model.createObjectProperty( NS + "base" ); - public static final ObjectProperty ontology = m_model.createObjectProperty( NS + "ontology" ); + public static final Property ontology = m_model.createObjectProperty( NS + "ontology" ); - public static final ObjectProperty service = m_model.createObjectProperty( NS + "service" ); + public static final Property service = m_model.createObjectProperty( NS + "service" ); - public static final ObjectProperty arg = m_model.createObjectProperty( NS + "arg" ); + public static final Property arg = m_model.createObjectProperty( NS + "arg" ); - public static final DatatypeProperty paramName = m_model.createDatatypeProperty( NS + "paramName" ); + public static final Property paramName = m_model.createDataProperty( NS + "paramName" ); // "extends" is a reserved keyword in Java, obviously - public static final ObjectProperty extends_ = m_model.createObjectProperty( NS + "extends" ); + public static final Property extends_ = m_model.createObjectProperty( NS + "extends" ); - public static final DatatypeProperty path = m_model.createDatatypeProperty( NS + "path" ); + public static final Property path = m_model.createDataProperty( NS + "path" ); - public static final ObjectProperty query = m_model.createObjectProperty( NS + "query" ); + public static final Property query = m_model.createObjectProperty( NS + "query" ); - public static final ObjectProperty update = m_model.createObjectProperty( NS + "update" ); + public static final Property update = m_model.createObjectProperty( NS + "update" ); - public static final DatatypeProperty match = m_model.createDatatypeProperty( NS + "match" ); + public static final Property match = m_model.createDataProperty( NS + "match" ); - public static final DatatypeProperty priority = m_model.createDatatypeProperty( NS + "priority" ); + public static final Property priority = m_model.createDataProperty( NS + "priority" ); - public static final DatatypeProperty fragment = m_model.createDatatypeProperty( NS + "fragment" ); + public static final Property fragment = m_model.createDataProperty( NS + "fragment" ); - public static final ObjectProperty param = m_model.createObjectProperty( NS + "param" ); + public static final Property param = m_model.createObjectProperty( NS + "param" ); - public static final ObjectProperty loadClass = m_model.createObjectProperty( NS + "loadClass" ); + public static final Property loadClass = m_model.createObjectProperty( NS + "loadClass" ); - public static final DatatypeProperty cacheControl = m_model.createDatatypeProperty( NS + "cacheControl" ); + public static final Property cacheControl = m_model.createDataProperty( NS + "cacheControl" ); - public static final ObjectProperty lang = m_model.createObjectProperty( NS + "lang" ); + public static final Property lang = m_model.createObjectProperty( NS + "lang" ); - public static final ObjectProperty template = m_model.createObjectProperty( NS + "template" ); + 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 ae9068ccfd..a45a58977a 100644 --- a/src/main/resources/com/atomgraph/linkeddatahub/app/admin/lacl.ttl +++ b/src/main/resources/com/atomgraph/linkeddatahub/app/admin/lacl.ttl @@ -69,7 +69,7 @@ # authorization requests -:AuthorizationRequest a rdfs:Class ; +:AuthorizationRequest a rdfs:Class, owl:Class ; spin:constructor :AuthorizationRequestConstructor ; spin:constraint :MissingRequestMode, :MissingRequestAgent, :MissingRequestAccessTo ; rdfs:label "Authorization request" ; @@ -108,7 +108,7 @@ WHERE {}""" ; rdfs:isDefinedBy : . -:CreatorAuthorization a rdfs:Class ; +:CreatorAuthorization a rdfs:Class, owl:Class ; rdfs:subClassOf acl:Authorization ; rdfs:label "Creator access" ; rdfs:comment "Creators have full control of their created resources" ; diff --git a/src/main/resources/com/atomgraph/linkeddatahub/dh.ttl b/src/main/resources/com/atomgraph/linkeddatahub/dh.ttl index 2c80455940..a3dae50f24 100644 --- a/src/main/resources/com/atomgraph/linkeddatahub/dh.ttl +++ b/src/main/resources/com/atomgraph/linkeddatahub/dh.ttl @@ -34,21 +34,21 @@ # CLASSES -:Document a rdfs:Class ; +:Document a rdfs:Class, owl:Class ; rdfs:subClassOf foaf:Document ; # ldt:path "{slug}/" ; rdfs:label "Document" ; # spin:constructor :DocumentConstructor ; rdfs:isDefinedBy : . -:Container a rdfs:Class ; +:Container a rdfs:Class, owl:Class ; rdfs:subClassOf :Document, sioc:Container ; # spin:constructor :ContainerConstructor ; rdfs:label "Container" ; rdfs:comment "An area in which content Items are contained" ; rdfs:isDefinedBy : . -:Item a rdfs:Class ; +:Item a rdfs:Class, owl:Class ; rdfs:subClassOf :Document, sioc:Item ; # spin:constructor :ItemConstructor ; rdfs:label "Item" ; diff --git a/src/main/resources/com/atomgraph/linkeddatahub/ldt.ttl b/src/main/resources/com/atomgraph/linkeddatahub/ldt.ttl index 5c847a307a..120859f87f 100644 --- a/src/main/resources/com/atomgraph/linkeddatahub/ldt.ttl +++ b/src/main/resources/com/atomgraph/linkeddatahub/ldt.ttl @@ -155,29 +155,29 @@ # CLASSES -:InheritedProperty a rdfs:Class ; +:InheritedProperty a rdfs:Class, owl:Class ; rdfs:subClassOf owl:AnnotationProperty ; rdfs:label "Inherited property" ; rdfs:comment "Values of this property are inherited by subclasses that do not have this property" ; rdfs:isDefinedBy : . -:Application a rdfs:Class ; +:Application a rdfs:Class, owl:Class ; rdfs:label "Application" ; rdfs:comment "Declarative Linked Data application which structure is defined by its ontology and data is access via its SPARQL service" ; rdfs:isDefinedBy : . -:Ontology a rdfs:Class ; +:Ontology a rdfs:Class, owl:Class ; rdfs:subClassOf owl:Ontology ; rdfs:label "Ontology" ; rdfs:comment "Ontology annotated with Linked Data Templates" ; rdfs:isDefinedBy : . -:Template a rdfs:Class ; +:Template a rdfs:Class, owl:Class ; rdfs:label "Resource template" ; rdfs:comment "A class of RDF resources that share the same URI template and SPARQL query template" ; rdfs:isDefinedBy : . -:Query a rdfs:Class ; +:Query a rdfs:Class, owl:Class ; rdfs:subClassOf sp:Query, [ a owl:Class ; owl:unionOf (sp:Describe sp:Construct) ; rdfs:label "Graph query forms" @@ -185,12 +185,12 @@ rdfs:label "Query" ; rdfs:isDefinedBy : . -:Update a rdfs:Class ; +:Update a rdfs:Class, owl:Class ; rdfs:subClassOf sp:Update ; rdfs:label "Update" ; rdfs:isDefinedBy : . -:Parameter a rdfs:Class ; +:Parameter a rdfs:Class, owl:Class ; rdfs:subClassOf spl:Argument ; rdfs:label "Parameter" ; rdfs:comment "Represents a query parameter that has predicate, value type, default value etc." ; diff --git a/src/main/webapp/WEB-INF/web.xml b/src/main/webapp/WEB-INF/web.xml index 8bcc728831..7799b44a94 100644 --- a/src/main/webapp/WEB-INF/web.xml +++ b/src/main/webapp/WEB-INF/web.xml @@ -322,10 +322,6 @@ support@atomgraph.com]]> https://w3id.org/atomgraph/client#resolvingUncached true - - https://w3id.org/atomgraph/core#preemptiveAuth - true - https://w3id.org/atomgraph/linkeddatahub/config#invalidateCache true 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 bbfff9e59a..3ccab50832 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,6 +122,8 @@ WHERE 'graph-state': $graph-state }" as="map(*)"/> + + + + + + @@ -381,6 +387,8 @@ WHERE + + diff --git a/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/client.xsl b/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/client.xsl index ecf8eff43d..ddf605df99 100644 --- a/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/client.xsl +++ b/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/client.xsl @@ -559,8 +559,10 @@ WHERE + + @@ -718,6 +720,7 @@ WHERE + diff --git a/src/test/java/com/atomgraph/linkeddatahub/imports/stream/StreamRDFOutputWriterTest.java b/src/test/java/com/atomgraph/linkeddatahub/imports/stream/StreamRDFOutputWriterTest.java new file mode 100644 index 0000000000..0e3f2b6cb1 --- /dev/null +++ b/src/test/java/com/atomgraph/linkeddatahub/imports/stream/StreamRDFOutputWriterTest.java @@ -0,0 +1,100 @@ +/** + * Copyright 2025 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.imports.stream; + +import com.atomgraph.linkeddatahub.client.GraphStoreClient; +import com.atomgraph.linkeddatahub.model.Service; +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import jakarta.ws.rs.BadRequestException; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import org.apache.jena.query.Query; +import org.apache.jena.query.QueryFactory; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * Unit tests for {@link StreamRDFOutputWriter}. + * + * @author Martynas Jusevičius {@literal } + */ +@ExtendWith(MockitoExtension.class) +public class StreamRDFOutputWriterTest +{ + + @Mock private Service service; + @Mock private Service adminService; + @Mock private com.atomgraph.linkeddatahub.Application system; + @Mock private GraphStoreClient gsc; + + private static final String BASE_URI = "http://localhost/"; + private static final String GRAPH_URI = "http://localhost/graphs/import"; + + private Query query; + private StreamRDFOutputWriter writer; + + @BeforeEach + public void setUp() + { + query = QueryFactory.create("CONSTRUCT WHERE { ?s ?p ?o }"); + writer = new StreamRDFOutputWriter(service, adminService, system, gsc, BASE_URI, query, GRAPH_URI); + } + + @Test + public void testGettersRoundTrip() + { + assertSame(service, writer.getService()); + assertSame(adminService, writer.getAdminService()); + assertSame(system, writer.getSystem()); + assertSame(gsc, writer.getGraphStoreClient()); + assertEquals(BASE_URI, writer.getBaseURI()); + assertSame(query, writer.getQuery()); + assertEquals(GRAPH_URI, writer.getGraphURI()); + } + + @Test + public void testApplyNullResponse() + { + assertThrows(IllegalArgumentException.class, () -> writer.apply(null)); + } + + /** + * A response whose {@code Content-Type} does not map to an RDF language must be rejected with + * {@code 400 Bad Request} — after the body has been buffered to a temp file but before any + * Graph Store write is attempted. + */ + @Test + public void testApplyNonRdfMediaTypeThrowsBadRequest() + { + Response response = mock(Response.class); + lenient().when(response.readEntity(InputStream.class)).thenReturn((InputStream)new ByteArrayInputStream("not rdf".getBytes())); + when(response.getMediaType()).thenReturn(MediaType.valueOf("image/png")); + + assertThrows(BadRequestException.class, () -> writer.apply(response)); + } + +} diff --git a/src/test/java/com/atomgraph/linkeddatahub/server/filter/request/OntologyFilterTest.java b/src/test/java/com/atomgraph/linkeddatahub/server/filter/request/OntologyFilterTest.java new file mode 100644 index 0000000000..9478f75d2d --- /dev/null +++ b/src/test/java/com/atomgraph/linkeddatahub/server/filter/request/OntologyFilterTest.java @@ -0,0 +1,68 @@ +/** + * 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.filter.request; + +import com.atomgraph.client.util.jena.PrefixGraphRepository; +import org.apache.jena.graph.Graph; +import org.apache.jena.rdf.model.ModelFactory; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; + +/** + * Characterization tests for {@link OntologyFilter#addDocumentModel}, which caches an imported graph + * under a SECONDARY key: the fragment-stripped document URI. Pins the dual-key caching behavior + * retained from the legacy implementation. + * + * @author Martynas Jusevičius {@literal } + */ +public class OntologyFilterTest +{ + + /** An import URI with a fragment is also cached under its fragment-stripped document URI. */ + @Test + public void testAddDocumentModelCachesUnderStrippedDocumentURI() + { + PrefixGraphRepository repository = new PrefixGraphRepository(null); + String importURI = "http://example.org/onto#"; + String docURI = "http://example.org/onto"; + + Graph imported = ModelFactory.createDefaultModel().getGraph(); + repository.put(importURI, imported); + + OntologyFilter.addDocumentModel(repository, importURI); + + assertTrue(repository.isCached(docURI), "import graph should be cached under the document URI"); + assertSame(imported, repository.get(docURI)); + } + + /** If the document URI is mapped to a different location, the secondary cache write is skipped. */ + @Test + public void testAddDocumentModelSkipsWhenDocumentURIMapped() + { + PrefixGraphRepository repository = new PrefixGraphRepository(null); + String importURI = "http://example.org/mapped#"; + String docURI = "http://example.org/mapped"; + + repository.addLocationMapping(docURI, "file:elsewhere.ttl"); // resolve(docURI) != docURI -> skip + repository.put(importURI, ModelFactory.createDefaultModel().getGraph()); + + OntologyFilter.addDocumentModel(repository, importURI); + + assertFalse(repository.isCached(docURI), "mapped document URI should not be cached as a secondary key"); + } + +} diff --git a/src/test/java/com/atomgraph/linkeddatahub/server/filter/request/OntologyImportsCharacterizationTest.java b/src/test/java/com/atomgraph/linkeddatahub/server/filter/request/OntologyImportsCharacterizationTest.java new file mode 100644 index 0000000000..312a614151 --- /dev/null +++ b/src/test/java/com/atomgraph/linkeddatahub/server/filter/request/OntologyImportsCharacterizationTest.java @@ -0,0 +1,91 @@ +/** + * 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.filter.request; + +import com.atomgraph.client.util.jena.PrefixGraphRepository; +import org.apache.jena.ontapi.OntModelFactory; +import org.apache.jena.ontapi.OntSpecification; +import org.apache.jena.ontapi.model.OntModel; +import org.apache.jena.rdf.model.Model; +import org.apache.jena.rdf.model.ModelFactory; +import org.apache.jena.rdf.model.Resource; +import org.apache.jena.rdf.model.ResourceFactory; +import org.apache.jena.vocabulary.OWL; +import org.apache.jena.vocabulary.RDF; +import org.apache.jena.vocabulary.RDFS; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; + +/** + * Pins {@link OntologyFilter#loadOntology}: it flattens the owl:imports closure into one graph, + * applies RDFS inference, and materializes the inferences into the repository cache — without ontapi + * managing a union-graph hierarchy over the shared repository (which collides on duplicate ontology IDs). + * + * @author Martynas Jusevičius {@literal } + */ +public class OntologyImportsCharacterizationTest +{ + + private static final String BASE_URI = "http://example.org/base"; + private static final String IMPORT_URI = "http://example.org/imported"; + private static final String NS = "http://example.org/ns#"; + + @Test + public void testLoadOntologyFlattensClosureWithMaterializedRDFSInference() + { + PrefixGraphRepository repository = new PrefixGraphRepository(null); + + Resource a = ResourceFactory.createResource(NS + "A"); + Resource b = ResourceFactory.createResource(NS + "B"); + Resource x = ResourceFactory.createResource(NS + "x"); + + // imported ontology: A declared as owl:Class only; B declared as rdfs:Class only (mimicking third-party vocabs like sp.ttl); + // B rdfs:subClassOf A; individual x a B + Model imported = ModelFactory.createDefaultModel(); + imported.add(imported.createResource(IMPORT_URI), RDF.type, OWL.Ontology); + imported.add(a, RDF.type, OWL.Class); + imported.add(b, RDF.type, RDFS.Class); + imported.add(b, RDFS.subClassOf, a); + imported.add(x, RDF.type, b); + repository.put(IMPORT_URI, imported.getGraph()); + + // base ontology owl:imports the imported one + Model base = ModelFactory.createDefaultModel(); + Resource baseOnt = base.createResource(BASE_URI); + base.add(baseOnt, RDF.type, OWL.Ontology); + base.add(baseOnt, OWL.imports, base.createResource(IMPORT_URI)); + repository.put(BASE_URI, base.getGraph()); + + OntologyFilter.loadOntology(repository, BASE_URI); + + Model result = ModelFactory.createModelForGraph(repository.get(BASE_URI)); + // (a) imported terms flattened into the cached graph + assertTrue(result.contains(b, RDFS.subClassOf, a), "imported terms should be flattened in"); + // (b) RDFS inference materialized as a concrete triple: x a A + assertTrue(result.contains(x, RDF.type, a), "RDFS-inferred 'x a A' should be materialized in the cached graph"); + // (c) the import is also cached under its (fragment-stripped) document URI + assertTrue(repository.isCached(IMPORT_URI), "import should remain cached"); + // (d) REGRESSION GUARD: both owl:Class and rdfs:Class-only terms must be recognized as OntClasses by the returned + // model, so GET /ns?forClass= resolves the class and runs its SPIN constructor. + // OntologyFilter promotes all rdfs:Class subjects to owl:Class so OWL2 profiles (which do not recognize bare + // rdfs:Class) can find third-party vocab terms like sp:Describe. + OntModel ontology = OntModelFactory.createModel(repository.get(BASE_URI), OntSpecification.OWL2_FULL_MEM); + assertNotNull(ontology.getOntClass(NS + "A"), "owl:Class term must be recognized as an OntClass under OWL2_FULL_MEM"); + assertNotNull(ontology.getOntClass(NS + "B"), "rdfs:Class-only term must be recognized as an OntClass after promotion"); + } + +} diff --git a/src/test/java/com/atomgraph/linkeddatahub/server/filter/request/auth/ProxiedWebIDFilterTest.java b/src/test/java/com/atomgraph/linkeddatahub/server/filter/request/auth/ProxiedWebIDFilterTest.java new file mode 100644 index 0000000000..e23a32861d --- /dev/null +++ b/src/test/java/com/atomgraph/linkeddatahub/server/filter/request/auth/ProxiedWebIDFilterTest.java @@ -0,0 +1,115 @@ +/** + * Copyright 2025 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.filter.request.auth; + +import jakarta.ws.rs.container.ContainerRequestContext; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.when; + +/** + * Unit tests for {@link ProxiedWebIDFilter}, which extracts the client certificate from the + * {@code Client-Cert} request header (URL-encoded PEM) instead of the TLS connection. + * + * @author Martynas Jusevičius {@literal } + */ +@ExtendWith(MockitoExtension.class) +public class ProxiedWebIDFilterTest +{ + + private static final String CLIENT_CERT_HEADER_NAME = "Client-Cert"; + + /** A self-signed X.509 certificate (CN=Test WebID, O=LinkedDataHub) in PEM form. */ + private static final String CERT_PEM = + "-----BEGIN CERTIFICATE-----\n" + + "MIIC1jCCAb4CCQD35LqgLKP0ATANBgkqhkiG9w0BAQsFADAtMRMwEQYDVQQDDApU\n" + + "ZXN0IFdlYklEMRYwFAYDVQQKDA1MaW5rZWREYXRhSHViMB4XDTI2MDYxODA4Mzkz\n" + + "OFoXDTI2MDYxOTA4MzkzOFowLTETMBEGA1UEAwwKVGVzdCBXZWJJRDEWMBQGA1UE\n" + + "CgwNTGlua2VkRGF0YUh1YjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB\n" + + "AMRsMfh0IikmxDbZ0kca2FRI+WXelczdi+dU9ZC42cAZJM6pEu9icvCdTKBKipYE\n" + + "07PkfAgsmkS3qzly1iQzyXZFzopndg9FdFvWZyn8SdxNSKCcQt13NTp8sXflkdxK\n" + + "SfOseUx1cZ0T4ylGNwkqxcqZo5b06CJqiZqjgO4x7kYWWrli44AgzMkT3AgJqq5X\n" + + "iSo5j8gOjicR+ZywLAEvWH0ITja4sIgsQzZHxbquOuPEevnT+135M33wHxsY5MHJ\n" + + "Ykxid7C4ifVm4jXf81CmnoCifR9UeBnMZ0QBPBP/Exv+CpTgb3TBAfF1o1QsEuM3\n" + + "60fYmqLcwLiKgZqDJ7ZH80UCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAeYJGVnFq\n" + + "CARK15JQQk1YBAUPspkFWAeXH9UzYyxpqt0bLlYO9g4KExJVJvE9Qub2lHXBs36j\n" + + "/elRF+PR5Zt/6LD26OnSu+QWkFSqbO6Otul7g9ikMufuhNrZyyOOzidqFfcfkhWx\n" + + "FZh+yZhGoo2f+ddMuYbK3lKI+/DMswfdNN6VN++EOYskjWBB85GKUxEJTLEF2yE+\n" + + "yRtqnQfX3ucvO2Zd1XHsgknzoSfG8CXZF3GDcqzzEZ6Aa//xtwYRCmNmj9E9SdMY\n" + + "xuCHnQP3cV/vBBhxt1BWdIRtcU6xpasNMfWGgAxqrCTz+GnT7FExbe5qt6CgX7yl\n" + + "JLw8c9VNQzsM9g==\n" + + "-----END CERTIFICATE-----\n"; + + @Mock private ContainerRequestContext requestContext; + + private ProxiedWebIDFilter filter; + + @BeforeEach + public void setUp() + { + filter = new ProxiedWebIDFilter(); + } + + @Test + public void testCertificateFactoryIsX509() + { + assertNotNull(filter.getCertificateFactory()); + assertEquals("X.509", filter.getCertificateFactory().getType()); + } + + /** No {@code Client-Cert} header — no certificate. */ + @Test + public void testNoHeaderReturnsNull() throws Exception + { + when(requestContext.getHeaderString(CLIENT_CERT_HEADER_NAME)).thenReturn(null); + assertNull(filter.getWebIDCertificate(requestContext)); + } + + /** A URL-encoded PEM certificate in the header is decoded and parsed into an X509Certificate. */ + @Test + public void testValidHeaderParsesCertificate() throws Exception + { + String encoded = URLEncoder.encode(CERT_PEM, StandardCharsets.UTF_8); + when(requestContext.getHeaderString(CLIENT_CERT_HEADER_NAME)).thenReturn(encoded); + + X509Certificate cert = filter.getWebIDCertificate(requestContext); + + assertNotNull(cert); + assertTrue(cert.getSubjectX500Principal().getName().contains("Test WebID")); + } + + /** A header that is not a valid certificate must surface as a CertificateException. */ + @Test + public void testMalformedHeaderThrows() + { + when(requestContext.getHeaderString(CLIENT_CERT_HEADER_NAME)).thenReturn("garbage-not-a-cert"); + assertThrows(CertificateException.class, () -> filter.getWebIDCertificate(requestContext)); + } + +} diff --git a/src/test/java/com/atomgraph/linkeddatahub/server/model/impl/DocumentHierarchyGraphStoreImplTest.java b/src/test/java/com/atomgraph/linkeddatahub/server/model/impl/DocumentHierarchyGraphStoreImplTest.java new file mode 100644 index 0000000000..de95fa2891 --- /dev/null +++ b/src/test/java/com/atomgraph/linkeddatahub/server/model/impl/DocumentHierarchyGraphStoreImplTest.java @@ -0,0 +1,274 @@ +/** + * Copyright 2025 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.model.impl; + +import com.atomgraph.linkeddatahub.vocabulary.SIOC; +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.InputStream; +import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Calendar; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.HashMap; +import java.util.Set; +import org.apache.jena.datatypes.xsd.XSDDateTime; +import org.apache.jena.rdf.model.Model; +import org.apache.jena.rdf.model.ModelFactory; +import org.apache.jena.rdf.model.Resource; +import org.apache.jena.vocabulary.DCTerms; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.CALLS_REAL_METHODS; +import static org.mockito.Mockito.mock; + +/** + * Unit tests for the stateless logic of {@link DocumentHierarchyGraphStoreImpl}. + *

+ * The class has a single heavyweight {@code @Inject} constructor that wires the JAX-RS request + * context and an HTTP-backed {@code GraphStoreClient}, so it cannot be instantiated directly in a + * unit test. The methods exercised here ({@code getChangedResources}, {@code getLastModified}, and + * the {@code writeFile}/{@code writeFiles} guards and file I/O) read no instance state, so we obtain + * an instance via Mockito's {@code CALLS_REAL_METHODS} (which skips the constructor) and invoke the + * real method bodies. + * + * @author Martynas Jusevičius {@literal } + */ +public class DocumentHierarchyGraphStoreImplTest +{ + + private final DocumentHierarchyGraphStoreImpl gs = mock(DocumentHierarchyGraphStoreImpl.class, CALLS_REAL_METHODS); + + // getChangedResources() + + @Test + public void testChangedResourcesNullBefore() + { + assertThrows(IllegalArgumentException.class, () -> gs.getChangedResources(null, ModelFactory.createDefaultModel())); + } + + @Test + public void testChangedResourcesNullAfter() + { + assertThrows(IllegalArgumentException.class, () -> gs.getChangedResources(ModelFactory.createDefaultModel(), null)); + } + + @Test + public void testChangedResourcesDetectsAddedSubject() + { + Model before = ModelFactory.createDefaultModel(); + Model after = ModelFactory.createDefaultModel(); + Resource added = after.createResource("http://localhost/added"); + added.addProperty(SIOC.HAS_PARENT, after.createResource("http://localhost/")); + + Set changed = gs.getChangedResources(before, after); + assertTrue(changed.contains(added)); + } + + @Test + public void testChangedResourcesDetectsRemovedSubject() + { + Model before = ModelFactory.createDefaultModel(); + Resource removed = before.createResource("http://localhost/removed"); + removed.addProperty(SIOC.HAS_PARENT, before.createResource("http://localhost/")); + Model after = ModelFactory.createDefaultModel(); + + Set changed = gs.getChangedResources(before, after); + assertTrue(changed.contains(removed)); + } + + @Test + public void testChangedResourcesIdenticalModelsAreEmpty() + { + Model before = ModelFactory.createDefaultModel(); + before.createResource("http://localhost/doc").addProperty(DCTerms.creator, before.createResource("http://localhost/agent")); + Model after = ModelFactory.createDefaultModel().add(before); + + assertTrue(gs.getChangedResources(before, after).isEmpty()); + } + + // getLastModified(Resource) + + @Test + public void testLastModifiedNullResource() + { + assertThrows(IllegalArgumentException.class, () -> gs.getLastModified((Resource)null)); + } + + @Test + public void testLastModifiedReturnsNullWhenNoDates() + { + Model model = ModelFactory.createDefaultModel(); + Resource resource = model.createResource("http://localhost/doc"); + assertNull(gs.getLastModified(resource)); + } + + @Test + public void testLastModifiedReturnsMaxOfCreatedAndModified() + { + Model model = ModelFactory.createDefaultModel(); + Calendar createdCal = new GregorianCalendar(2020, Calendar.JANUARY, 1, 0, 0, 0); + Calendar modifiedCal = new GregorianCalendar(2021, Calendar.JANUARY, 1, 0, 0, 0); + Resource resource = model.createResource("http://localhost/doc"). + addProperty(DCTerms.created, model.createTypedLiteral(createdCal)). + addProperty(DCTerms.modified, model.createTypedLiteral(modifiedCal)); + + // expected value is the later (modified) date, round-tripped through the same XSDDateTime path the method uses + Date expected = ((XSDDateTime)model.createTypedLiteral(modifiedCal).getValue()).asCalendar().getTime(); + assertEquals(expected, gs.getLastModified(resource)); + } + + @Test + public void testLastModifiedIgnoresNonDateTimeLiterals() + { + Model model = ModelFactory.createDefaultModel(); + Resource resource = model.createResource("http://localhost/doc"). + addProperty(DCTerms.modified, "not a date"); // plain string literal, not xsd:dateTime + assertNull(gs.getLastModified(resource)); + } + + // getLastModified(Model, URI) + + @Test + public void testLastModifiedByGraphURIReturnsNullForNullURI() + { + assertNull(gs.getLastModified(ModelFactory.createDefaultModel(), null)); + } + + @Test + public void testLastModifiedByGraphURI() + { + Model model = ModelFactory.createDefaultModel(); + Calendar cal = new GregorianCalendar(2022, Calendar.MARCH, 15, 12, 0, 0); + URI graphURI = URI.create("http://localhost/doc"); + model.createResource(graphURI.toString()).addProperty(DCTerms.modified, model.createTypedLiteral(cal)); + + Date expected = ((XSDDateTime)model.createTypedLiteral(cal).getValue()).asCalendar().getTime(); + assertEquals(expected, gs.getLastModified(model, graphURI)); + } + + // writeFile(File, InputStream) + + @Test + public void testWriteFileNullFile() + { + assertThrows(IllegalArgumentException.class, () -> gs.writeFile((File)null, new ByteArrayInputStream(new byte[0]))); + } + + @Test + public void testWriteFileNullInputStream(@TempDir Path tempDir) + { + File file = tempDir.resolve("out.bin").toFile(); + assertThrows(IllegalArgumentException.class, () -> gs.writeFile(file, (InputStream)null)); + } + + @Test + public void testWriteFileWritesContent(@TempDir Path tempDir) throws Exception + { + File file = tempDir.resolve("out.bin").toFile(); + byte[] data = "hello world".getBytes(StandardCharsets.UTF_8); + + gs.writeFile(file, new ByteArrayInputStream(data)); + + assertTrue(file.exists()); + assertArrayEquals(data, Files.readAllBytes(file.toPath())); + } + + // writeFile(URI, URI, URI, InputStream) + + @Test + public void testWriteFileByURINullURI(@TempDir Path tempDir) + { + assertThrows(IllegalArgumentException.class, () -> gs.writeFile(null, URI.create("http://localhost/"), tempDir.toUri(), new ByteArrayInputStream(new byte[0]))); + } + + @Test + public void testWriteFileByURIRelativeURIRejected(@TempDir Path tempDir) + { + assertThrows(IllegalArgumentException.class, () -> gs.writeFile(URI.create("relative/path"), URI.create("http://localhost/"), tempDir.toUri(), new ByteArrayInputStream(new byte[0]))); + } + + @Test + public void testWriteFileByURINullBase(@TempDir Path tempDir) + { + assertThrows(IllegalArgumentException.class, () -> gs.writeFile(URI.create("http://localhost/myfile"), null, tempDir.toUri(), new ByteArrayInputStream(new byte[0]))); + } + + @Test + public void testWriteFileByURINullUploadRoot() + { + assertThrows(IllegalArgumentException.class, () -> gs.writeFile(URI.create("http://localhost/myfile"), URI.create("http://localhost/"), null, new ByteArrayInputStream(new byte[0]))); + } + + @Test + public void testWriteFileByURIResolvesRelativePath(@TempDir Path tempDir) throws Exception + { + URI base = URI.create("http://localhost/"); + URI uri = URI.create("http://localhost/myfile"); + URI uploadRoot = tempDir.toUri(); // ends with '/' + byte[] data = "content-addressed".getBytes(StandardCharsets.UTF_8); + + File written = gs.writeFile(uri, base, uploadRoot, new ByteArrayInputStream(data)); + + assertEquals(new File(uploadRoot.resolve("myfile")), written); + assertArrayEquals(data, Files.readAllBytes(written.toPath())); + } + + // writeFiles(Model, Map) + + @Test + public void testWriteFilesNullModel() + { + assertThrows(IllegalArgumentException.class, () -> gs.writeFiles(null, new HashMap<>())); + } + + @Test + public void testWriteFilesNullMap() + { + assertThrows(IllegalArgumentException.class, () -> gs.writeFiles(ModelFactory.createDefaultModel(), null)); + } + + @Test + public void testWriteFilesNoFileResourcesWritesNothing() + { + Model model = ModelFactory.createDefaultModel(); + model.createResource("http://localhost/doc").addProperty(DCTerms.creator, model.createResource("http://localhost/agent")); + + assertEquals(0, gs.writeFiles(model, new HashMap<>())); + } + + @Test + public void testChangedResourcesUnchangedDoesNotContainSubject() + { + Model before = ModelFactory.createDefaultModel(); + Resource subject = before.createResource("http://localhost/doc"); + subject.addProperty(DCTerms.creator, before.createResource("http://localhost/agent")); + Model after = ModelFactory.createDefaultModel().add(before); + + assertFalse(gs.getChangedResources(before, after).contains(subject)); + } + +} diff --git a/src/test/java/com/atomgraph/linkeddatahub/server/util/OntologyRepositoryTest.java b/src/test/java/com/atomgraph/linkeddatahub/server/util/OntologyRepositoryTest.java new file mode 100644 index 0000000000..33b3183f25 --- /dev/null +++ b/src/test/java/com/atomgraph/linkeddatahub/server/util/OntologyRepositoryTest.java @@ -0,0 +1,111 @@ +/** + * 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 com.atomgraph.core.client.GraphStoreClient; +import com.atomgraph.linkeddatahub.apps.model.AdminApplication; +import com.atomgraph.linkeddatahub.apps.model.EndUserApplication; +import com.atomgraph.linkeddatahub.model.Service; +import com.atomgraph.linkeddatahub.model.ServiceContext; +import jakarta.ws.rs.core.MultivaluedMap; +import jakarta.ws.rs.core.Response; +import org.apache.jena.graph.Graph; +import org.apache.jena.graph.NodeFactory; +import org.apache.jena.query.Query; +import org.apache.jena.query.QueryFactory; +import org.apache.jena.rdf.model.Model; +import org.apache.jena.rdf.model.ModelFactory; +import org.apache.jena.vocabulary.OWL; +import org.apache.jena.vocabulary.RDF; +import org.apache.jena.vocabulary.RDFS; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * Characterization tests for {@link OntologyRepository}, which resolves an ontology graph + * SPARQL-first (admin endpoint CONSTRUCT) and falls back to the superclass (bundled mapping / HTTP) + * only when the SPARQL result is empty. + * + * @author Martynas Jusevičius {@literal } + */ +@ExtendWith(MockitoExtension.class) +public class OntologyRepositoryTest +{ + + private static final String ONTOLOGY_URI = "http://example.org/ontology#"; + private static final Query ONTOLOGY_QUERY = QueryFactory.create("CONSTRUCT { ?ontology ?p ?o } WHERE { ?ontology ?p ?o }"); + + @Mock com.atomgraph.linkeddatahub.Application system; + @Mock EndUserApplication app; + @Mock AdminApplication adminApp; + @Mock Service service; + @Mock ServiceContext serviceContext; + @Mock com.atomgraph.core.client.SPARQLClient sparqlClient; + @Mock Response response; + @Mock GraphStoreClient gsc; + + private void stubSPARQLChain(Model sparqlResult) + { + when(app.getAdminApplication()).thenReturn(adminApp); + when(adminApp.getService()).thenReturn(service); + when(system.getServiceContext(service)).thenReturn(serviceContext); + when(serviceContext.getSPARQLClient()).thenReturn(sparqlClient); + when(sparqlClient.query(any(Query.class), eq(Model.class), any(MultivaluedMap.class), any(MultivaluedMap.class))).thenReturn(response); + when(response.readEntity(Model.class)).thenReturn(sparqlResult); + } + + /** A non-empty admin SPARQL CONSTRUCT result is returned directly; the HTTP fallback is not used. */ + @Test + public void testSPARQLResultUsedWhenNonEmpty() + { + Model sparqlResult = ModelFactory.createDefaultModel(); + sparqlResult.createResource(ONTOLOGY_URI).addProperty(RDF.type, OWL.Ontology); + stubSPARQLChain(sparqlResult); + + OntologyRepository repository = new OntologyRepository(app, system, gsc, ONTOLOGY_QUERY); + Graph result = repository.get(ONTOLOGY_URI); + + assertTrue(result.contains(NodeFactory.createURI(ONTOLOGY_URI), RDF.type.asNode(), OWL.Ontology.asNode())); + verify(gsc, never()).getModel(any()); + } + + /** An empty SPARQL result falls back to the Graph Store client (HTTP) load. */ + @Test + public void testFallsBackToHttpWhenSPARQLEmpty() + { + Model empty = ModelFactory.createDefaultModel(); + stubSPARQLChain(empty); + + Model fallback = ModelFactory.createDefaultModel(); + fallback.createResource(ONTOLOGY_URI).addProperty(RDFS.label, "fallback"); + when(gsc.getModel(ONTOLOGY_URI)).thenReturn(fallback); + + OntologyRepository repository = new OntologyRepository(app, system, gsc, ONTOLOGY_QUERY); + Graph result = repository.get(ONTOLOGY_URI); + + assertTrue(result.contains(NodeFactory.createURI(ONTOLOGY_URI), RDFS.label.asNode(), NodeFactory.createLiteralString("fallback"))); + } + +} diff --git a/src/test/java/com/atomgraph/linkeddatahub/server/util/SPINConstraintValidationTest.java b/src/test/java/com/atomgraph/linkeddatahub/server/util/SPINConstraintValidationTest.java new file mode 100644 index 0000000000..6d2062f9c1 --- /dev/null +++ b/src/test/java/com/atomgraph/linkeddatahub/server/util/SPINConstraintValidationTest.java @@ -0,0 +1,98 @@ +/** + * 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 com.atomgraph.server.util.Validator; +import com.atomgraph.spinrdf.constraints.ConstraintViolation; +import com.atomgraph.spinrdf.constraints.SPINConstraints; +import java.util.List; +import org.apache.jena.ontapi.OntModelFactory; +import org.apache.jena.ontapi.OntSpecification; +import org.apache.jena.ontapi.model.OntModel; +import org.apache.jena.rdf.model.Model; +import org.apache.jena.rdf.model.ModelFactory; +import org.apache.jena.riot.RDFDataMgr; +import org.apache.jena.vocabulary.RDF; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; + +/** + * Regression guard for SPIN constraint validation under the ontapi migration. + * + * {@code SPINConstraints} resolves a constraint through Jena enhanced-node polymorphism + * ({@code canAs(Query)}/{@code canAs(TemplateCall)}), which an ontapi {@link OntModel}'s profile-aware personality + * does not carry — so before the fix, validation silently fired nothing. twirl now re-bases the constraint model + * onto its SPIN personality internally (twirl {@code SPINConstraints.check}), so {@link Validator} can hand it the + * ontapi model directly. This pins that the real {@code dh:Item spin:constraint :MissingTitle} template constraint + * flags a {@code dh:Item} that lacks {@code dct:title}. + * + * @author Martynas Jusevičius {@literal } + */ +public class SPINConstraintValidationTest +{ + + static + { + org.apache.jena.sys.JenaSystem.init(); + } + + private static final String DH = "https://www.w3.org/ns/ldt/document-hierarchy#"; + + /** The full owl:imports closure the ontology pipeline assembles for the document-hierarchy ontology. */ + private OntModel loadOntology() + { + Model closure = ModelFactory.createDefaultModel(); + for (String classpath : new String[] { + "etc/sp.ttl", "etc/spl.spin.ttl", "etc/spin.ttl", + "com/atomgraph/linkeddatahub/dh.ttl", + "com/atomgraph/linkeddatahub/def.ttl", + "com/atomgraph/linkeddatahub/ldh.ttl" }) + RDFDataMgr.read(closure, classpath); + + // mirror OntologyFilter.loadOntology: RDFS-infer then materialize into a plain OWL2_FULL_MEM graph + OntModel inferred = OntModelFactory.createModel(closure.getGraph(), OntSpecification.OWL2_FULL_MEM_RDFS_INF); + OntModel materialized = OntModelFactory.createModel(OntSpecification.OWL2_FULL_MEM); + materialized.add(inferred); + return materialized; + } + + /** A dh:Item with NO dct:title — violates the MissingTitle constraint. */ + private Model violatingItem() + { + Model data = ModelFactory.createDefaultModel(); + data.createResource("http://example.org/doc").addProperty(RDF.type, data.createResource(DH + "Item")); + return data; + } + + @Test + public void testMissingTitleConstraintFiresThroughValidator() + { + List violations = new Validator(loadOntology()).validate(violatingItem()); + assertFalse(violations.isEmpty(), "MissingTitle SPIN constraint must fire on a dh:Item without dct:title"); + } + + @Test + public void testRawOntApiModelFiresViaTwirlRebase() + { + // The ontapi OntModel's profile-aware personality lacks the SPIN views, so canAs(...) is false on it. + // twirl's SPINConstraints.check re-bases the constraint model onto its own SPIN personality, so passing the + // ontapi model straight in still fires. Guards against twirl regressing that re-base. + List direct = SPINConstraints.check(violatingItem(), loadOntology()); + assertFalse(direct.isEmpty(), "SPINConstraints.check must fire on a raw ontapi OntModel (twirl re-bases internally)"); + } + +} diff --git a/src/test/java/com/atomgraph/linkeddatahub/vocabulary/VocabularyHolderTest.java b/src/test/java/com/atomgraph/linkeddatahub/vocabulary/VocabularyHolderTest.java new file mode 100644 index 0000000000..95f10ddc6d --- /dev/null +++ b/src/test/java/com/atomgraph/linkeddatahub/vocabulary/VocabularyHolderTest.java @@ -0,0 +1,63 @@ +/** + * 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.vocabulary; + +import org.apache.jena.rdf.model.Resource; +import org.apache.jena.vocabulary.OWL; +import org.apache.jena.vocabulary.RDF; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; + +/** + * Characterization snapshot for the vocabulary holders (representative: {@link DH}). + * + * All ~19 vocabulary classes follow the identical pattern — a static OntModel built with + * {@code ModelFactory.createOntologyModel(OntModelSpec.OWL_MEM, null)} on which + * {@code createClass} / {@code createDatatypeProperty} mint typed terms. Phase A swaps the + * factory to {@code OntModelFactory.createModel(OntSpecification.OWL2_FULL_MEM)} and renames + * {@code createClass}→{@code createOntClass}, {@code createDatatypeProperty}→{@code createDataProperty}. + * + * Asserted via the migration-stable {@link Resource} interface: the term URIs and the + * {@code rdf:type owl:Class} / {@code rdf:type owl:DatatypeProperty} triples they produce + * must stay identical after the rename. + * + * @author Martynas Jusevičius {@literal } + */ +public class VocabularyHolderTest +{ + + @Test + public void testClassTermsHaveStableUriAndType() + { + Resource document = DH.Document; + assertEquals(DH.NS + "Document", document.getURI()); + assertTrue(document.hasProperty(RDF.type, OWL.Class), "DH.Document must be typed owl:Class"); + + Resource container = DH.Container; + assertEquals(DH.NS + "Container", container.getURI()); + assertTrue(container.hasProperty(RDF.type, OWL.Class), "DH.Container must be typed owl:Class"); + } + + @Test + public void testDatatypePropertyTermHasStableUriAndType() + { + Resource slug = DH.slug; + assertEquals(DH.NS + "slug", slug.getURI()); + assertTrue(slug.hasProperty(RDF.type, OWL.DatatypeProperty), "DH.slug must be typed owl:DatatypeProperty"); + } + +}