From 38a1ac99b318ba608bc9e0a1cf6f805df2333c3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martynas=20Jusevi=C4=8Dius?= Date: Wed, 7 Jan 2026 16:01:34 +0100 Subject: [PATCH 01/13] System config dataset uses named graphs --- config/system.trig | 72 ++++++++------- docker-compose.yml | 2 +- platform/select-root-services.rq | 52 ++++++----- .../atomgraph/linkeddatahub/Application.java | 88 +++++++++++++++---- .../server/util/SystemConfigFileManager.java | 59 +++++++++++++ 5 files changed, 200 insertions(+), 73 deletions(-) create mode 100644 src/main/java/com/atomgraph/linkeddatahub/server/util/SystemConfigFileManager.java diff --git a/config/system.trig b/config/system.trig index 21cd69615..2a32e2c90 100644 --- a/config/system.trig +++ b/config/system.trig @@ -15,41 +15,47 @@ # root admin - a lapp:Application, lapp:AdminApplication ; - dct:title "LinkedDataHub admin" ; - # ldt:base ; - lapp:origin ; - ldt:ontology ; - ldt:service ; - ac:stylesheet ; - lapp:endUserApplication ; - lapp:frontendProxy . + +{ + a lapp:Application, lapp:AdminApplication ; + dct:title "LinkedDataHub admin" ; + # ldt:base ; + lapp:origin ; + ldt:ontology ; + ldt:service ; + ac:stylesheet ; + lapp:endUserApplication ; + lapp:frontendProxy . - a sd:Service ; - dct:title "LinkedDataHub admin service" ; - sd:supportedLanguage sd:SPARQL11Query, sd:SPARQL11Update ; - sd:endpoint ; - a:graphStore ; - a:quadStore ; - lapp:backendProxy . + a sd:Service ; + dct:title "LinkedDataHub admin service" ; + sd:supportedLanguage sd:SPARQL11Query, sd:SPARQL11Update ; + sd:endpoint ; + a:graphStore ; + a:quadStore ; + lapp:backendProxy . +} # root end-user - a lapp:Application, lapp:EndUserApplication ; - dct:title "LinkedDataHub" ; - # ldt:base ; - lapp:origin ; - ldt:ontology ; - ldt:service ; - ac:stylesheet ; - lapp:adminApplication ; - lapp:frontendProxy ; - lapp:public true . + +{ + a lapp:Application, lapp:EndUserApplication ; + dct:title "LinkedDataHub" ; + # ldt:base ; + lapp:origin ; + ldt:ontology ; + ldt:service ; + ac:stylesheet ; + lapp:adminApplication ; + lapp:frontendProxy ; + lapp:public true . - a sd:Service ; - dct:title "LinkedDataHub service" ; - sd:supportedLanguage sd:SPARQL11Query, sd:SPARQL11Update ; - sd:endpoint ; - a:graphStore ; - a:quadStore ; - lapp:backendProxy . + a sd:Service ; + dct:title "LinkedDataHub service" ; + sd:supportedLanguage sd:SPARQL11Query, sd:SPARQL11Update ; + sd:endpoint ; + a:graphStore ; + a:quadStore ; + lapp:backendProxy . +} diff --git a/docker-compose.yml b/docker-compose.yml index 7b8fffd36..8f16be46b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -93,7 +93,7 @@ services: - ./datasets/secretary:/var/linkeddatahub/datasets/secretary - ./uploads:/var/www/linkeddatahub/uploads - ./config/dev.log4j.properties:/usr/local/tomcat/webapps/ROOT/WEB-INF/classes/log4j.properties:ro - - ./config/system.trig:/var/linkeddatahub/datasets/system.trig:ro + - ./config/system.trig:/var/linkeddatahub/datasets/system.trig fuseki-admin: image: atomgraph/fuseki:4.7.0 user: root # otherwise fuseki user does not have permissions to the mounted folder which is owner by root diff --git a/platform/select-root-services.rq b/platform/select-root-services.rq index 4c6d67546..10cf84b85 100644 --- a/platform/select-root-services.rq +++ b/platform/select-root-services.rq @@ -7,31 +7,37 @@ PREFIX foaf: SELECT ?endUserApp ?endUserOrigin ?endUserQuadStore ?endUserEndpoint ?endUserAuthUser ?endUserAuthPwd ?endUserMaker ?adminApp ?adminOrigin ?adminQuadStore ?adminEndpoint ?adminAuthUser ?adminAuthPwd ?adminMaker { - ?endUserApp lapp:origin ?endUserOrigin ; - ldt:service ?endUserService ; - lapp:adminApplication ?adminApp . - ?adminApp ldt:service ?adminService ; - lapp:origin ?adminOrigin . - ?endUserService a:quadStore ?endUserQuadStore ; - sd:endpoint ?endUserEndpoint . - ?adminService a:quadStore ?adminQuadStore ; - sd:endpoint ?adminEndpoint . - OPTIONAL - { - ?endUserService a:authUser ?endUserAuthUser ; - a:authPwd ?endUserAuthPwd . - } - OPTIONAL + GRAPH ?endUserAppGraph { - ?adminService a:authUser ?adminAuthUser ; - a:authPwd ?adminAuthPwd . + ?endUserApp lapp:origin ?endUserOrigin ; + ldt:service ?endUserService ; + lapp:adminApplication ?adminApp . + ?endUserService a:quadStore ?endUserQuadStore ; + sd:endpoint ?endUserEndpoint . + OPTIONAL + { + ?endUserService a:authUser ?endUserAuthUser ; + a:authPwd ?endUserAuthPwd . + } + OPTIONAL + { + ?endUserService foaf:maker ?endUserMaker + } } - OPTIONAL + GRAPH ?adminAppGraph { - ?endUserService foaf:maker ?endUserMaker - } - OPTIONAL - { - ?adminService foaf:maker ?adminMaker + ?adminApp ldt:service ?adminService ; + lapp:origin ?adminOrigin . + ?adminService a:quadStore ?adminQuadStore ; + sd:endpoint ?adminEndpoint . + OPTIONAL + { + ?adminService a:authUser ?adminAuthUser ; + a:authPwd ?adminAuthPwd . + } + OPTIONAL + { + ?adminService foaf:maker ?adminMaker + } } } \ No newline at end of file diff --git a/src/main/java/com/atomgraph/linkeddatahub/Application.java b/src/main/java/com/atomgraph/linkeddatahub/Application.java index e28066e34..c0e478fbd 100644 --- a/src/main/java/com/atomgraph/linkeddatahub/Application.java +++ b/src/main/java/com/atomgraph/linkeddatahub/Application.java @@ -176,6 +176,8 @@ import java.io.FileOutputStream; import java.io.UnsupportedEncodingException; import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.nio.file.Paths; import java.security.MessageDigest; import java.util.Arrays; import java.util.List; @@ -294,9 +296,9 @@ public class Application extends ResourceConfig private final boolean enableWebIDSignUp; private final String oidcRefreshTokensPropertiesPath; private final Properties oidcRefreshTokens; + private final URI contextDatasetURI; + private final Dataset contextDataset; - private Dataset contextDataset; - /** * Constructs system application and configures it using sevlet config. * @@ -313,6 +315,7 @@ public Application(@Context ServletConfig servletConfig) throws URISyntaxExcepti 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(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, servletConfig.getServletContext().getInitParameter(AC.resolvingUncached.getURI()) != null ? Boolean.parseBoolean(servletConfig.getServletContext().getInitParameter(AC.resolvingUncached.getURI())) : true, @@ -355,14 +358,6 @@ public Application(@Context ServletConfig servletConfig) throws URISyntaxExcepti servletConfig.getServletContext().getInitParameter(ORCID.clientID.getURI()) != null ? servletConfig.getServletContext().getInitParameter(ORCID.clientID.getURI()) : null, servletConfig.getServletContext().getInitParameter(ORCID.clientSecret.getURI()) != null ? servletConfig.getServletContext().getInitParameter(ORCID.clientSecret.getURI()) : null ); - - URI contextDatasetURI = servletConfig.getServletContext().getInitParameter(LDHC.contextDataset.getURI()) != null ? new URI(servletConfig.getServletContext().getInitParameter(LDHC.contextDataset.getURI())) : null; - if (contextDatasetURI == null) - { - if (log.isErrorEnabled()) log.error("Context dataset URI '{}' not configured", LDHC.contextDataset.getURI()); - throw new ConfigurationException(LDHC.contextDataset); - } - this.contextDataset = getDataset(servletConfig.getServletContext(), contextDatasetURI); } /** @@ -374,6 +369,7 @@ public Application(@Context ServletConfig servletConfig) throws URISyntaxExcepti * @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 contextDatasetURIString location of the context dataset * @param stylesheet stylesheet URI * @param cacheStylesheet true if stylesheet should be cached * @param resolvingUncached true if XLST processor should dereference URLs that are not cached @@ -418,7 +414,8 @@ public Application(@Context ServletConfig servletConfig) throws URISyntaxExcepti */ public Application(final ServletConfig servletConfig, final MediaTypes mediaTypes, final Integer maxGetRequestSize, final boolean cacheModelLoads, final boolean preemptiveAuth, - final LocationMapper locationMapper, final Source stylesheet, final boolean cacheStylesheet, final boolean resolvingUncached, + final LocationMapper locationMapper, final String contextDatasetURIString, + final Source stylesheet, final boolean cacheStylesheet, final boolean resolvingUncached, final String clientKeyStoreURIString, final String clientKeyStorePassword, final String secretaryCertAlias, final String clientTrustStoreURIString, final String clientTrustStorePassword, @@ -433,6 +430,13 @@ public Application(final ServletConfig servletConfig, final MediaTypes mediaType final String googleClientID, final String googleClientSecret, final String orcidClientID, final String orcidClientSecret) { + if (contextDatasetURIString == null) + { + if (log.isErrorEnabled()) log.error("Context dataset URI '{}' not configured", LDHC.contextDataset.getURI()); + throw new ConfigurationException(LDHC.contextDataset); + } + this.contextDatasetURI = URI.create(contextDatasetURIString); + if (clientKeyStoreURIString == null) { if (log.isErrorEnabled()) log.error("Client key store ({}) not configured", LDHC.clientKeyStore.getURI()); @@ -664,6 +668,8 @@ public Application(final ServletConfig servletConfig, final MediaTypes mediaType try { + this.contextDataset = getDataset(servletConfig.getServletContext(), contextDatasetURI); + keyStore = KeyStore.getInstance("PKCS12"); try (FileInputStream keyStoreInputStream = new FileInputStream(new java.io.File(new URI(clientKeyStoreURIString)))) { @@ -1974,7 +1980,7 @@ public URI getUploadRoot() /** * Returns RDF dataset with LinkedDataHub application descriptions. - * + * * @return RDF dataset */ protected Dataset getContextDataset() @@ -1982,19 +1988,69 @@ protected Dataset getContextDataset() return contextDataset; } + /** + * Returns the URI of the context dataset file. + * + * @return context dataset URI + */ + protected URI getContextDatasetURI() + { + return contextDatasetURI; + } + /** * Returns RDF model with LinkedDataHub application descriptions. - * - * @return RDF model + * This method returns a union of all named graphs from the context dataset. + * + * @return RDF model (read-only union of all named graphs) */ public Model getContextModel() { - return ModelFactory.createModelForGraph(new GraphReadOnly(getContextDataset().getDefaultModel().getGraph())); + return ModelFactory.createModelForGraph(new GraphReadOnly(getContextDataset().getUnionModel().getGraph())); + } + + /** + * Updates a dataspace by replacing its named graph with a new Model. + * This is a template method that can be overridden by subclasses to provide alternative implementations + * (e.g., HTTP-based updates using GraphStoreClient to a remote triplestore). + * + * Default implementation uses file-based operations via SystemConfigFileManager. + * + * @param application the dataspace application to update + * @param newModel the new RDF model to replace the existing named graph + * @throws IOException if an I/O error occurs + */ + public void updateDataspace(com.atomgraph.linkeddatahub.apps.model.Application application, Model newModel) throws IOException + { + if (application == null) throw new IllegalArgumentException("Application cannot be null"); + if (newModel == null) throw new IllegalArgumentException("Model cannot be null"); + + synchronized (getContextDataset()) + { + String dataspaceURI = application.getURI(); + + // Only support file-based URIs for the default implementation + if (!getContextDatasetURI().isAbsolute() || !"file".equals(getContextDatasetURI().getScheme())) + { + throw new UnsupportedOperationException("Only file-based context dataset URIs are supported for updates in default implementation"); + } + + Path configFilePath = Paths.get(getContextDatasetURI()); + + // Update the named graph in the dataset + getContextDataset().removeNamedModel(dataspaceURI). + addNamedModel(dataspaceURI, newModel); + + // Write the updated dataset back to file + com.atomgraph.linkeddatahub.server.util.SystemConfigFileManager.writeDataset(getContextDataset(), configFilePath); + + if (log.isInfoEnabled()) log.info("Updated dataspace <{}> in file: {}", dataspaceURI, configFilePath); + } } /** * Returns true if configured to invalidate HTTP proxy cache of triplestore results. - * + * * @return true if invalidated */ public boolean isInvalidateCache() diff --git a/src/main/java/com/atomgraph/linkeddatahub/server/util/SystemConfigFileManager.java b/src/main/java/com/atomgraph/linkeddatahub/server/util/SystemConfigFileManager.java new file mode 100644 index 000000000..27956f666 --- /dev/null +++ b/src/main/java/com/atomgraph/linkeddatahub/server/util/SystemConfigFileManager.java @@ -0,0 +1,59 @@ +/** + * 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.util; + +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.file.Path; +import org.apache.jena.query.Dataset; +import org.apache.jena.rdf.model.Model; +import org.apache.jena.riot.RDFDataMgr; +import org.apache.jena.riot.RDFFormat; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Utility class for managing system configuration files in TriG format. + * Provides methods for updating and writing configuration datasets. + * + * @author Martynas Jusevičius {@literal } + */ +public class SystemConfigFileManager +{ + + private static final Logger log = LoggerFactory.getLogger(SystemConfigFileManager.class); + + /** + * Writes a dataset to a file in TriG format. + * + * @param dataset the dataset to write + * @param filePath the path to the output file + * @throws IOException if an I/O error occurs during writing + */ + public static void writeDataset(Dataset dataset, Path filePath) throws IOException + { + if (dataset == null) throw new IllegalArgumentException("Dataset cannot be null"); + if (filePath == null) throw new IllegalArgumentException("File path cannot be null"); + + try (FileOutputStream out = new FileOutputStream(filePath.toFile())) + { + RDFDataMgr.write(out, dataset, RDFFormat.TRIG_PRETTY); + if (log.isDebugEnabled()) log.debug("Wrote dataset to file: {}", filePath); + } + } + +} From 8139601dcf1b1e73e6071fe6e80594a7aa2fd289 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martynas=20Jusevi=C4=8Dius?= Date: Wed, 7 Jan 2026 17:00:49 +0100 Subject: [PATCH 02/13] Settings endpoint --- platform/entrypoint.sh | 4 +- .../resource/admin/Settings.java | 95 +++++++++++++++++++ .../server/model/impl/Dispatcher.java | 12 +++ 3 files changed, 109 insertions(+), 2 deletions(-) create mode 100644 src/main/java/com/atomgraph/linkeddatahub/resource/admin/Settings.java diff --git a/platform/entrypoint.sh b/platform/entrypoint.sh index 6a4bb2e6d..1fbe571cf 100755 --- a/platform/entrypoint.sh +++ b/platform/entrypoint.sh @@ -625,10 +625,10 @@ for app in "${apps[@]}"; do # append ownership metadata to apps if it's not present (apps have to be URI resources!) if [ -z "$end_user_owner" ]; then - echo "<${end_user_app}> <${OWNER_URI}> ." >> "$based_context_dataset" + echo "<${end_user_app}> <${OWNER_URI}> <${end_user_app}> ." >> "$based_context_dataset" fi if [ -z "$admin_owner" ]; then - echo "<${admin_app}> <${OWNER_URI}> ." >> "$based_context_dataset" + echo "<${admin_app}> <${OWNER_URI}> <${admin_app}> ." >> "$based_context_dataset" fi printf "\n### Quad store URL of the root end-user service: %s\n" "$end_user_quad_store_url" diff --git a/src/main/java/com/atomgraph/linkeddatahub/resource/admin/Settings.java b/src/main/java/com/atomgraph/linkeddatahub/resource/admin/Settings.java new file mode 100644 index 000000000..acdc7d273 --- /dev/null +++ b/src/main/java/com/atomgraph/linkeddatahub/resource/admin/Settings.java @@ -0,0 +1,95 @@ +/** + * 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.resource.admin; + +import com.atomgraph.linkeddatahub.apps.model.Application; +import jakarta.inject.Inject; +import jakarta.ws.rs.BadRequestException; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.core.Response; +import java.io.IOException; +import org.apache.jena.rdf.model.Model; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * JAX-RS resource for updating dataspace settings. + * Handles POST requests with RDF data representing the updated dataspace configuration. + * + * @author Martynas Jusevičius {@literal } + */ +public class Settings +{ + private static final Logger log = LoggerFactory.getLogger(Settings.class); + + private final Application application; + private final com.atomgraph.linkeddatahub.Application system; + + /** + * Constructs the Settings endpoint. + * + * @param application the current dataspace application + * @param system the system application + */ + @Inject + public Settings(Application application, com.atomgraph.linkeddatahub.Application system) + { + this.application = application; + this.system = system; + } + + /** + * Updates the dataspace settings by accepting RDF data representing the new configuration. + * + * @param model the RDF model containing the updated dataspace configuration + * @return response indicating success or failure + * @throws java.io.IOException + */ + @POST + public Response post(Model model) throws IOException + { + if (model == null || model.isEmpty()) throw new BadRequestException("Model cannot be empty"); + + // Update the dataspace configuration + getSystem().updateDataspace(getApplication(), model); + + if (log.isInfoEnabled()) log.info("Updated settings for dataspace <{}>", getApplication().getURI()); + + return Response.ok().build(); + } + + /** + * Returns the current dataspace application. + * + * @return the application + */ + public Application getApplication() + { + return application; + } + + /** + * Returns the system application. + * + * @return the system application + */ + public com.atomgraph.linkeddatahub.Application getSystem() + { + return system; + } + +} diff --git a/src/main/java/com/atomgraph/linkeddatahub/server/model/impl/Dispatcher.java b/src/main/java/com/atomgraph/linkeddatahub/server/model/impl/Dispatcher.java index 430427a60..9f3d316a1 100644 --- a/src/main/java/com/atomgraph/linkeddatahub/server/model/impl/Dispatcher.java +++ b/src/main/java/com/atomgraph/linkeddatahub/server/model/impl/Dispatcher.java @@ -23,6 +23,7 @@ import com.atomgraph.linkeddatahub.resource.Namespace; import com.atomgraph.linkeddatahub.resource.Transform; import com.atomgraph.linkeddatahub.resource.admin.ClearOntology; +import com.atomgraph.linkeddatahub.resource.admin.Settings; import com.atomgraph.linkeddatahub.resource.admin.pkg.InstallPackage; import com.atomgraph.linkeddatahub.resource.admin.pkg.UninstallPackage; import com.atomgraph.linkeddatahub.resource.admin.SignUp; @@ -227,6 +228,17 @@ public Class getClearEndpoint() return getProxyClass().orElse(ClearOntology.class); } + /** + * Returns the endpoint for updating dataspace settings. + * + * @return endpoint resource + */ + @Path("settings") + public Class getSettingsEndpoint() + { + return getProxyClass().orElse(Settings.class); + } + /** * Returns the endpoint for installing LinkedDataHub packages. * From 76d1845ba4a0f38ff32967bef9e3c6ac44ddb72e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martynas=20Jusevi=C4=8Dius?= Date: Thu, 8 Jan 2026 00:55:10 +0100 Subject: [PATCH 03/13] Settings endpoint As well as ACL authorizations for it --- platform/datasets/end-user.trig | 8 ++ platform/namespace-ontology.trig.template | 2 +- .../atomgraph/linkeddatahub/Application.java | 12 ++ .../linkeddatahub/resource/Settings.java | 134 ++++++++++++++++++ .../resource/admin/Settings.java | 95 ------------- .../server/model/impl/Dispatcher.java | 24 ++-- 6 files changed, 167 insertions(+), 108 deletions(-) create mode 100644 src/main/java/com/atomgraph/linkeddatahub/resource/Settings.java delete mode 100644 src/main/java/com/atomgraph/linkeddatahub/resource/admin/Settings.java diff --git a/platform/datasets/end-user.trig b/platform/datasets/end-user.trig index 4c3574b08..2608b6a39 100644 --- a/platform/datasets/end-user.trig +++ b/platform/datasets/end-user.trig @@ -389,6 +389,14 @@ WHERE } + +{ + + a foaf:Document ; + dct:title "Settings endpoint" . + +} + { diff --git a/platform/namespace-ontology.trig.template b/platform/namespace-ontology.trig.template index c62472479..a3531ccb8 100644 --- a/platform/namespace-ontology.trig.template +++ b/platform/namespace-ontology.trig.template @@ -107,7 +107,7 @@ rdfs:label "Full control" ; rdfs:comment "Allows full read/write access to all application resources" ; acl:accessToClass dh:Item, dh:Container, def:Root ; - acl:accessTo <${end_user_origin}/sparql>, <${end_user_origin}/importer>, <${end_user_origin}/add>, <${end_user_origin}/generate>, <${end_user_origin}/ns> ; + acl:accessTo <${end_user_origin}/sparql>, <${end_user_origin}/importer>, <${end_user_origin}/add>, <${end_user_origin}/generate>, <${end_user_origin}/ns>, <${end_user_origin}/settings> ; acl:mode acl:Read, acl:Append, acl:Write, acl:Control ; acl:agentGroup <${admin_origin}/acl/groups/owners/#this> . diff --git a/src/main/java/com/atomgraph/linkeddatahub/Application.java b/src/main/java/com/atomgraph/linkeddatahub/Application.java index c0e478fbd..a3cdf78ce 100644 --- a/src/main/java/com/atomgraph/linkeddatahub/Application.java +++ b/src/main/java/com/atomgraph/linkeddatahub/Application.java @@ -2009,6 +2009,18 @@ public Model getContextModel() return ModelFactory.createModelForGraph(new GraphReadOnly(getContextDataset().getUnionModel().getGraph())); } + /** + * Retrieves a dataspace model by application from the context dataset. + * + * @param application the dataspace application + * @return the model for the specified dataspace, or null if not found + */ + public Model getDataspaceModel(com.atomgraph.linkeddatahub.apps.model.Application application) + { + if (application == null) throw new IllegalArgumentException("Application cannot be null"); + return getContextDataset().getNamedModel(application.getURI()); + } + /** * Updates a dataspace by replacing its named graph with a new Model. * This is a template method that can be overridden by subclasses to provide alternative implementations diff --git a/src/main/java/com/atomgraph/linkeddatahub/resource/Settings.java b/src/main/java/com/atomgraph/linkeddatahub/resource/Settings.java new file mode 100644 index 000000000..a3ea45896 --- /dev/null +++ b/src/main/java/com/atomgraph/linkeddatahub/resource/Settings.java @@ -0,0 +1,134 @@ +/** + * 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.resource; + +import com.atomgraph.linkeddatahub.apps.model.Application; +import jakarta.inject.Inject; +import jakarta.ws.rs.BadRequestException; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.NotFoundException; +import jakarta.ws.rs.PATCH; +import jakarta.ws.rs.core.Response; +import java.io.IOException; +import org.apache.jena.query.Dataset; +import org.apache.jena.query.DatasetFactory; +import org.apache.jena.rdf.model.Model; +import org.apache.jena.update.UpdateAction; +import org.apache.jena.update.UpdateRequest; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * JAX-RS resource for updating dataspace settings. + * Handles POST requests with RDF data representing the updated dataspace configuration. + * + * @author Martynas Jusevičius {@literal } + */ +public class Settings +{ + private static final Logger log = LoggerFactory.getLogger(Settings.class); + + private final Application application; + private final com.atomgraph.linkeddatahub.Application system; + + /** + * Constructs the Settings endpoint. + * + * @param application the current dataspace application + * @param system the system application + */ + @Inject + public Settings(Application application, com.atomgraph.linkeddatahub.Application system) + { + this.application = application; + this.system = system; + } + + /** + * Retrieves the dataspace settings from the context dataset. + * + * @return the dataspace resource as RDF + */ + @GET + public Response get() + { + Model dataspaceModel = getSystem().getDataspaceModel(getApplication()); + + if (dataspaceModel == null || dataspaceModel.isEmpty()) + { + if (log.isWarnEnabled()) log.warn("No settings found for dataspace <{}> in context dataset", getApplication().getURI()); + return Response.status(Response.Status.NOT_FOUND).build(); + } + + if (log.isDebugEnabled()) log.debug("Retrieved settings for dataspace <{}>", getApplication().getURI()); + + return Response.ok(dataspaceModel).build(); + } + + /** + * Updates the dataspace settings by executing a SPARQL UPDATE request. + * Accepts SPARQL update as the request body which is executed in the context of the dataspace named graph. + * + * @param updateRequest SPARQL update + * @return response indicating success or failure + * @throws java.io.IOException + */ + @PATCH + public Response patch(UpdateRequest updateRequest) throws IOException + { + if (updateRequest == null) throw new BadRequestException("SPARQL update not specified"); + + if (log.isDebugEnabled()) log.debug("PATCH request for dataspace <{}>", getApplication().getURI()); + if (log.isDebugEnabled()) log.debug("PATCH update string: {}", updateRequest.toString()); + + Model dataspaceModel = getSystem().getDataspaceModel(getApplication()); + if (dataspaceModel == null || dataspaceModel.isEmpty()) + throw new NotFoundException("No settings found for dataspace <" + getApplication().getURI() + "> in context dataset"); + + // Execute the SPARQL UPDATE on the dataspace model in memory + Dataset dataset = DatasetFactory.wrap(dataspaceModel); + UpdateAction.execute(updateRequest, dataset); + + // Write the updated model back to the context dataset file + getSystem().updateDataspace(getApplication(), dataspaceModel); + + if (log.isInfoEnabled()) log.info("Updated settings for dataspace <{}> via PATCH", getApplication().getURI()); + + return Response.noContent().build(); + } + + /** + * Returns the current dataspace application. + * + * @return the application + */ + public Application getApplication() + { + return application; + } + + /** + * Returns the system application. + * + * @return the system application + */ + public com.atomgraph.linkeddatahub.Application getSystem() + { + return system; + } + +} diff --git a/src/main/java/com/atomgraph/linkeddatahub/resource/admin/Settings.java b/src/main/java/com/atomgraph/linkeddatahub/resource/admin/Settings.java deleted file mode 100644 index acdc7d273..000000000 --- a/src/main/java/com/atomgraph/linkeddatahub/resource/admin/Settings.java +++ /dev/null @@ -1,95 +0,0 @@ -/** - * 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.resource.admin; - -import com.atomgraph.linkeddatahub.apps.model.Application; -import jakarta.inject.Inject; -import jakarta.ws.rs.BadRequestException; -import jakarta.ws.rs.POST; -import jakarta.ws.rs.core.Response; -import java.io.IOException; -import org.apache.jena.rdf.model.Model; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * JAX-RS resource for updating dataspace settings. - * Handles POST requests with RDF data representing the updated dataspace configuration. - * - * @author Martynas Jusevičius {@literal } - */ -public class Settings -{ - private static final Logger log = LoggerFactory.getLogger(Settings.class); - - private final Application application; - private final com.atomgraph.linkeddatahub.Application system; - - /** - * Constructs the Settings endpoint. - * - * @param application the current dataspace application - * @param system the system application - */ - @Inject - public Settings(Application application, com.atomgraph.linkeddatahub.Application system) - { - this.application = application; - this.system = system; - } - - /** - * Updates the dataspace settings by accepting RDF data representing the new configuration. - * - * @param model the RDF model containing the updated dataspace configuration - * @return response indicating success or failure - * @throws java.io.IOException - */ - @POST - public Response post(Model model) throws IOException - { - if (model == null || model.isEmpty()) throw new BadRequestException("Model cannot be empty"); - - // Update the dataspace configuration - getSystem().updateDataspace(getApplication(), model); - - if (log.isInfoEnabled()) log.info("Updated settings for dataspace <{}>", getApplication().getURI()); - - return Response.ok().build(); - } - - /** - * Returns the current dataspace application. - * - * @return the application - */ - public Application getApplication() - { - return application; - } - - /** - * Returns the system application. - * - * @return the system application - */ - public com.atomgraph.linkeddatahub.Application getSystem() - { - return system; - } - -} diff --git a/src/main/java/com/atomgraph/linkeddatahub/server/model/impl/Dispatcher.java b/src/main/java/com/atomgraph/linkeddatahub/server/model/impl/Dispatcher.java index 9f3d316a1..451dc874f 100644 --- a/src/main/java/com/atomgraph/linkeddatahub/server/model/impl/Dispatcher.java +++ b/src/main/java/com/atomgraph/linkeddatahub/server/model/impl/Dispatcher.java @@ -23,9 +23,9 @@ import com.atomgraph.linkeddatahub.resource.Namespace; import com.atomgraph.linkeddatahub.resource.Transform; import com.atomgraph.linkeddatahub.resource.admin.ClearOntology; -import com.atomgraph.linkeddatahub.resource.admin.Settings; import com.atomgraph.linkeddatahub.resource.admin.pkg.InstallPackage; import com.atomgraph.linkeddatahub.resource.admin.pkg.UninstallPackage; +import com.atomgraph.linkeddatahub.resource.Settings; import com.atomgraph.linkeddatahub.resource.admin.SignUp; import com.atomgraph.linkeddatahub.resource.Graph; import com.atomgraph.linkeddatahub.resource.acl.Access; @@ -228,17 +228,6 @@ public Class getClearEndpoint() return getProxyClass().orElse(ClearOntology.class); } - /** - * Returns the endpoint for updating dataspace settings. - * - * @return endpoint resource - */ - @Path("settings") - public Class getSettingsEndpoint() - { - return getProxyClass().orElse(Settings.class); - } - /** * Returns the endpoint for installing LinkedDataHub packages. * @@ -261,6 +250,17 @@ public Class getUninstallPackageEndpoint() return getProxyClass().orElse(UninstallPackage.class); } + /** + * Returns the endpoint for updating dataspace settings. + * + * @return endpoint resource + */ + @Path("settings") + public Class getSettingsEndpoint() + { + return getProxyClass().orElse(Settings.class); + } + /** * Returns the default JAX-RS resource class. * From cae3b4fc97bff8b3cca09fe6843e826fa09be295 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martynas=20Jusevi=C4=8Dius?= Date: Thu, 8 Jan 2026 01:03:39 +0100 Subject: [PATCH 04/13] Test config fix --- http-tests/config/system.trig | 148 ++++++++++++++++++++-------------- 1 file changed, 88 insertions(+), 60 deletions(-) diff --git a/http-tests/config/system.trig b/http-tests/config/system.trig index 277499e5d..428f205bf 100644 --- a/http-tests/config/system.trig +++ b/http-tests/config/system.trig @@ -15,76 +15,104 @@ # root admin - a lapp:Application, lapp:AdminApplication ; - dct:title "LinkedDataHub admin" ; - # ldt:base ; - lapp:origin ; - ldt:ontology ; - ldt:service ; - lapp:endUserApplication ; - lapp:frontendProxy . + +{ + a lapp:Application, lapp:AdminApplication ; + dct:title "LinkedDataHub admin" ; + # ldt:base ; + lapp:origin ; + ldt:ontology ; + ldt:service ; + ac:stylesheet ; + lapp:endUserApplication ; + lapp:frontendProxy . +} - a sd:Service ; - dct:title "LinkedDataHub admin service" ; - sd:supportedLanguage sd:SPARQL11Query, sd:SPARQL11Update ; - sd:endpoint ; - a:graphStore ; - a:quadStore ; - lapp:backendProxy . + +{ + a sd:Service ; + dct:title "LinkedDataHub admin service" ; + sd:supportedLanguage sd:SPARQL11Query, sd:SPARQL11Update ; + sd:endpoint ; + a:graphStore ; + a:quadStore ; + lapp:backendProxy . +} # root end-user - a lapp:Application, lapp:EndUserApplication ; - dct:title "LinkedDataHub" ; - # ldt:base ; - lapp:origin ; - ldt:ontology ; - ldt:service ; - lapp:adminApplication ; - lapp:frontendProxy ; - lapp:public true . + +{ + a lapp:Application, lapp:EndUserApplication ; + dct:title "LinkedDataHub" ; + # ldt:base ; + lapp:origin ; + ldt:ontology ; + ldt:service ; + ac:stylesheet ; + lapp:adminApplication ; + lapp:frontendProxy ; + lapp:public true . +} - a sd:Service ; - dct:title "LinkedDataHub service" ; - sd:supportedLanguage sd:SPARQL11Query, sd:SPARQL11Update ; - sd:endpoint ; - a:graphStore ; - a:quadStore ; - lapp:backendProxy . + +{ + a sd:Service ; + dct:title "LinkedDataHub service" ; + sd:supportedLanguage sd:SPARQL11Query, sd:SPARQL11Update ; + sd:endpoint ; + a:graphStore ; + a:quadStore ; + lapp:backendProxy . +} # test admin - a lapp:Application, lapp:AdminApplication ; - dct:title "Test admin" ; - lapp:origin ; - ldt:ontology ; - ldt:service ; - lapp:endUserApplication ; - lapp:frontendProxy . + +{ + a lapp:Application, lapp:AdminApplication ; + dct:title "Test admin" ; + lapp:origin ; + ldt:ontology ; + ldt:service ; + ac:stylesheet ; + lapp:endUserApplication ; + lapp:frontendProxy . +} - a sd:Service ; - dct:title "Test admin service" ; - sd:supportedLanguage sd:SPARQL11Query, sd:SPARQL11Update ; - sd:endpoint ; - a:graphStore ; - a:quadStore ; - lapp:backendProxy . + +{ + a sd:Service ; + dct:title "Test admin service" ; + sd:supportedLanguage sd:SPARQL11Query, sd:SPARQL11Update ; + sd:endpoint ; + a:graphStore ; + a:quadStore ; + lapp:backendProxy . +} # test end-user - a lapp:Application, lapp:EndUserApplication ; - dct:title "Test" ; - lapp:origin ; - ldt:ontology ; - ldt:service ; - lapp:adminApplication ; - lapp:frontendProxy ; - lapp:public true . + +{ + a lapp:Application, lapp:EndUserApplication ; + dct:title "Test" ; + lapp:origin ; + ldt:ontology ; + ldt:service ; + ac:stylesheet ; + lapp:adminApplication ; + lapp:frontendProxy ; + lapp:public true . +} - a sd:Service ; - dct:title "Test service" ; - sd:supportedLanguage sd:SPARQL11Query, sd:SPARQL11Update ; - sd:endpoint ; - a:graphStore ; - a:quadStore ; - lapp:backendProxy . + +{ + a sd:Service ; + dct:title "Test service" ; + sd:supportedLanguage sd:SPARQL11Query, sd:SPARQL11Update ; + sd:endpoint ; + a:graphStore ; + a:quadStore ; + lapp:backendProxy . +} From e6bb97f3e088dd18dc5e35d40acaf7cdf47236f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martynas=20Jusevi=C4=8Dius?= Date: Thu, 8 Jan 2026 12:28:53 +0100 Subject: [PATCH 05/13] Frontend --- .../xsl/bootstrap/2.3.2/client/modal.xsl | 53 +++++++++++++++++-- .../xsl/bootstrap/2.3.2/layout.xsl | 7 +++ 2 files changed, 56 insertions(+), 4 deletions(-) diff --git a/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/bootstrap/2.3.2/client/modal.xsl b/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/bootstrap/2.3.2/client/modal.xsl index 4f63577a7..bdcc15a9c 100644 --- a/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/bootstrap/2.3.2/client/modal.xsl +++ b/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/bootstrap/2.3.2/client/modal.xsl @@ -673,7 +673,7 @@ LIMIT 10 - + @@ -712,7 +712,52 @@ LIMIT 10 - + + + + + + + + + + + + + + + + + + + + + + @@ -874,7 +919,7 @@ LIMIT 10 - + @@ -941,7 +986,7 @@ LIMIT 10 - + diff --git a/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/bootstrap/2.3.2/layout.xsl b/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/bootstrap/2.3.2/layout.xsl index 14cdf1a60..3073dfe28 100644 --- a/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/bootstrap/2.3.2/layout.xsl +++ b/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/bootstrap/2.3.2/layout.xsl @@ -1245,6 +1245,13 @@ LIMIT 100