Skip to content

Commit b34f704

Browse files
committed
Release version 5.0.23
2 parents 59dd0ac + ce84385 commit b34f704

27 files changed

Lines changed: 736 additions & 480 deletions

File tree

CHANGELOG.md

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,46 @@
1+
## [5.0.23] - 2025-09-11
2+
### Added
3+
- Drag handles for content blocks - blocks can now only be dragged by their dedicated drag handles
4+
- Client-side XPath `ac:mode` function for layout mode detection
5+
- New `ldh:request-uri` XPath function for URI handling
6+
- New `acl:mode` XPath function for client-side ACL mode detection
7+
- New HTTP tests for ACL `Link` headers to verify authorization modes in response
8+
9+
### Changed
10+
- Service input on the container generation form is now optional
11+
- IXSL promise cleanup and refactoring for better client-side performance
12+
- Document context handling improvements
13+
14+
### Fixed
15+
- `AuthorizationFilter` to always load authorizations from the admin dataset
16+
- Modal form validation
17+
- Layout mode is now retained after RDF file upload
18+
19+
## [5.0.22] - 2025-08-29
20+
### Added
21+
- SPARQL query support for `ProxyResourceBase` via `POST` requests
22+
- YouTube object block support with GRDDL transformation
23+
- New HTTP tests for proxy SPARQL query functionality
24+
- `JSONGRDDLFilter` feature for processing JSON-LD from HTML script elements
25+
- New CLI command for `PATCH` requests
26+
- Self-referencing object detection to prevent infinite loops
27+
28+
### Changed
29+
- Web-Client dependency version bump
30+
- Increased nginx rate limits for better performance
31+
- Uniform `ldh:href` function calls across codebase
32+
- Improved `Link` header parsing and usage fixes
33+
- Adjusted document controls size for better UI
34+
- Enhanced view titles for better user experience
35+
- Improved tests for document property cardinalities
36+
- Removed DBPedia's prefix mapping
37+
38+
### Fixed
39+
- Fixed template match issues
40+
- Improved `dct:modified` handling in Graph `POST` operations
41+
- Fixed error handling for "Document loaded successfully but resource was not found" cases
42+
- HTTP test fixes for better reliability
43+
144
## [5.0.19] - 2025-07-01
245
### Fixed
346
- Form callback invocation

README.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,36 @@ The environment variable `JENA_HOME` is used by all the command line tools to co
218218

219219
These demo applications can be installed into a LinkedDataHub instance using `make install`. You will need to provide the path to your WebID certificate as well as its password.
220220

221+
## AI-Powered Automation
222+
223+
### [Web-Algebra](https://github.com/AtomGraph/Web-Algebra)
224+
225+
Web-Algebra enables AI agents to consume Linked Data and SPARQL as well as control and automate LinkedDataHub operations through natural language instructions.
226+
This innovative system translates human language into JSON-formatted RDF operations that can be executed against your LinkedDataHub instance.
227+
228+
**Key capabilities:**
229+
* **Natural Language to RDF Operations**: Translate complex instructions into executable semantic workflows
230+
* **LLM Agent Integration**: AI agents can compose and execute complex multi-step operations automatically
231+
* **Atomic Execution**: Complex workflows are compiled into optimized JSON "bytecode" that executes as a single unit
232+
* **Model Context Protocol (MCP)**: Interactive tools for AI assistants to manage LinkedDataHub content
233+
234+
**Example use cases:**
235+
236+
*Business Analytics:*
237+
> Analyze quarterly sales performance from our Northwind dataset, identify the top 5 customers by revenue, and create an interactive dashboard showing regional sales trends with automated alerts for territories underperforming by more than 15%
238+
239+
*FAIR Life Sciences Integration:*
240+
> Query federated endpoints for protein interaction data from UniProt, gene expression profiles from EBI, and clinical trial outcomes from ClinicalTrials.gov, then integrate these datasets through SPARQL CONSTRUCT queries, create cross-references using shared identifiers, and embed the unified knowledge graph into an interactive research article with live data visualizations
241+
242+
**Perfect for:**
243+
* Business intelligence automation and reporting
244+
* Federated biomedical data integration and analysis
245+
* AI-assisted research data discovery and linking
246+
* Natural language interfaces to knowledge graphs
247+
* Intelligent data processing and monitoring pipelines
248+
249+
See the [Web-Algebra repository](https://github.com/AtomGraph/Web-Algebra) for setup instructions and examples of AI agents managing LinkedDataHub instances.
250+
221251
## How to get involved
222252

223253
* contribute a new LDH application or modify [one of ours](https://github.com/AtomGraph/LinkedDataHub-Apps)
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
initialize_dataset "$END_USER_BASE_URL" "$TMP_END_USER_DATASET" "$END_USER_ENDPOINT_URL"
5+
initialize_dataset "$ADMIN_BASE_URL" "$TMP_ADMIN_DATASET" "$ADMIN_ENDPOINT_URL"
6+
purge_cache "$END_USER_VARNISH_SERVICE"
7+
purge_cache "$ADMIN_VARNISH_SERVICE"
8+
purge_cache "$FRONTEND_VARNISH_SERVICE"
9+
10+
# add agent to the writers group
11+
12+
add-agent-to-group.sh \
13+
-f "$OWNER_CERT_FILE" \
14+
-p "$OWNER_CERT_PWD" \
15+
--agent "$AGENT_URI" \
16+
"${ADMIN_BASE_URL}acl/groups/writers/"
17+
18+
# create a new document to test ACL modes against
19+
20+
doc_url=$(create-item.sh \
21+
-b "$END_USER_BASE_URL" \
22+
-f "$AGENT_CERT_FILE" \
23+
-p "$AGENT_CERT_PWD" \
24+
--container "$END_USER_BASE_URL" \
25+
--title "ACL Test Document Agent" \
26+
--slug "acl-test-document-agent"
27+
)
28+
29+
# test that the signed up agent accessing the document returns correct Link header with ACL modes (no Control)
30+
31+
response_headers=$(mktemp)
32+
33+
curl -k -f -v \
34+
-E "$AGENT_CERT_FILE":"$AGENT_CERT_PWD" \
35+
-H "Accept: application/n-triples" \
36+
-D "$response_headers" \
37+
-o /dev/null \
38+
"$doc_url"
39+
40+
cat "$response_headers"
41+
42+
# check that each expected ACL mode is present in Link header (order independent)
43+
# signed up agents should have Read, Write, Append but NOT Control
44+
grep -q "Link:.*<http://www.w3.org/ns/auth/acl#Read>; rel=http://www.w3.org/ns/auth/acl#mode" "$response_headers"
45+
grep -q "Link:.*<http://www.w3.org/ns/auth/acl#Write>; rel=http://www.w3.org/ns/auth/acl#mode" "$response_headers"
46+
grep -q "Link:.*<http://www.w3.org/ns/auth/acl#Append>; rel=http://www.w3.org/ns/auth/acl#mode" "$response_headers"
47+
48+
# verify Control mode is NOT present for signed up agent
49+
! grep -q "Link:.*<http://www.w3.org/ns/auth/acl#Control>; rel=http://www.w3.org/ns/auth/acl#mode" "$response_headers"
50+
51+
rm "$response_headers"
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
initialize_dataset "$END_USER_BASE_URL" "$TMP_END_USER_DATASET" "$END_USER_ENDPOINT_URL"
5+
initialize_dataset "$ADMIN_BASE_URL" "$TMP_ADMIN_DATASET" "$ADMIN_ENDPOINT_URL"
6+
purge_cache "$END_USER_VARNISH_SERVICE"
7+
purge_cache "$ADMIN_VARNISH_SERVICE"
8+
purge_cache "$FRONTEND_VARNISH_SERVICE"
9+
10+
# create a new document to test ACL modes against
11+
12+
doc_url=$(create-item.sh \
13+
-b "$END_USER_BASE_URL" \
14+
-f "$OWNER_CERT_FILE" \
15+
-p "$OWNER_CERT_PWD" \
16+
--container "$END_USER_BASE_URL" \
17+
--title "ACL Test Document" \
18+
--slug "acl-test-document"
19+
)
20+
21+
# test that the owner agent accessing the document returns correct Link header with all ACL modes
22+
23+
response_headers=$(mktemp)
24+
25+
curl -k -f -v \
26+
-E "$OWNER_CERT_FILE":"$OWNER_CERT_PWD" \
27+
-H "Accept: application/n-triples" \
28+
-D "$response_headers" \
29+
-o /dev/null \
30+
"$doc_url"
31+
32+
cat "$response_headers"
33+
34+
# check that each ACL mode is present in Link header (order independent)
35+
grep -q "Link:.*<http://www.w3.org/ns/auth/acl#Read>; rel=http://www.w3.org/ns/auth/acl#mode" "$response_headers"
36+
grep -q "Link:.*<http://www.w3.org/ns/auth/acl#Write>; rel=http://www.w3.org/ns/auth/acl#mode" "$response_headers"
37+
grep -q "Link:.*<http://www.w3.org/ns/auth/acl#Append>; rel=http://www.w3.org/ns/auth/acl#mode" "$response_headers"
38+
grep -q "Link:.*<http://www.w3.org/ns/auth/acl#Control>; rel=http://www.w3.org/ns/auth/acl#mode" "$response_headers"
39+
40+
rm "$response_headers"

pom.xml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
<groupId>com.atomgraph</groupId>
55
<artifactId>linkeddatahub</artifactId>
6-
<version>5.0.22</version>
6+
<version>5.0.23</version>
77
<packaging>${packaging.type}</packaging>
88

99
<name>AtomGraph LinkedDataHub</name>
@@ -46,7 +46,7 @@
4646
<url>https://github.com/AtomGraph/LinkedDataHub</url>
4747
<connection>scm:git:git://github.com/AtomGraph/LinkedDataHub.git</connection>
4848
<developerConnection>scm:git:git@github.com:AtomGraph/LinkedDataHub.git</developerConnection>
49-
<tag>linkeddatahub-5.0.22</tag>
49+
<tag>linkeddatahub-5.0.23</tag>
5050
</scm>
5151

5252
<repositories>

src/main/java/com/atomgraph/linkeddatahub/resource/Generate.java

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -102,15 +102,13 @@ public Generate(@Context Request request, @Context UriInfo uriInfo, MediaTypes m
102102
@Override
103103
public Response post(Model model, @QueryParam("default") @DefaultValue("false") Boolean defaultGraph, @QueryParam("graph") URI graphUri)
104104
{
105-
ResIterator it = model.listSubjectsWithProperty(LDH.service);
105+
ResIterator it = model.listSubjectsWithProperty(SIOC.HAS_PARENT);
106106
try
107107
{
108108
if (!it.hasNext()) throw new BadRequestException("Argument resource not provided");
109109

110110
Resource arg = it.next();
111111
Resource service = arg.getPropertyResourceValue(LDH.service);
112-
if (service == null) throw new BadRequestException("Service URI (ldh:service) not provided");
113-
114112
Resource parent = arg.getPropertyResourceValue(SIOC.HAS_PARENT);
115113
if (parent == null) throw new BadRequestException("Parent container (sioc:has_parent) not provided");
116114

@@ -177,16 +175,19 @@ public Response post(Model model, @QueryParam("default") @DefaultValue("false")
177175
* @param model RDF model
178176
* @param title query title
179177
* @param query query object
180-
* @param service SPARQL service resource
178+
* @param service optional SPARQL service resource
181179
* @return query resource
182180
*/
183181
public Resource createContainerSelect(Model model, String title, Query query, Resource service)
184182
{
185-
return model.createResource().
183+
Resource resource = model.createResource().
186184
addProperty(RDF.type, SP.Select).
187185
addLiteral(DCTerms.title, title).
188-
addProperty(SP.text, query.toString()).
189-
addProperty(LDH.service, service);
186+
addProperty(SP.text, query.toString());
187+
188+
if (service != null) resource.addProperty(LDH.service, service);
189+
190+
return resource;
190191
}
191192

192193
/**

src/main/java/com/atomgraph/linkeddatahub/server/filter/request/AuthorizationFilter.java

Lines changed: 28 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -136,14 +136,14 @@ public void filter(ContainerRequestContext request) throws IOException
136136
if (request.getSecurityContext().getUserPrincipal() instanceof Agent) agent = ((Agent)(request.getSecurityContext().getUserPrincipal()));
137137
else agent = null; // public access
138138

139-
Resource authorization = authorize(request, agent, accessMode);
140-
if (authorization == null)
139+
Model authorizations = authorize(request, agent, accessMode);
140+
if (authorizations == null)
141141
{
142142
if (log.isTraceEnabled()) log.trace("Access not authorized for request URI: {} and access mode: {}", request.getUriInfo().getAbsolutePath(), accessMode);
143143
throw new AuthorizationException("Access not authorized for request URI", request.getUriInfo().getAbsolutePath(), accessMode);
144144
}
145145
else // authorization successful
146-
request.setProperty(AuthorizationContext.class.getCanonicalName(), new AuthorizationContext(authorization.getModel()));
146+
request.setProperty(AuthorizationContext.class.getCanonicalName(), new AuthorizationContext(authorizations));
147147
}
148148

149149
/**
@@ -152,21 +152,22 @@ public void filter(ContainerRequestContext request) throws IOException
152152
* @param request current request
153153
* @param agent agent resource or null
154154
* @param accessMode ACL access mode
155-
* @return authorization resource or null
155+
* @return authorizations model or null if access denied
156156
*/
157-
public Resource authorize(ContainerRequestContext request, Resource agent, Resource accessMode)
157+
public Model authorize(ContainerRequestContext request, Resource agent, Resource accessMode)
158158
{
159159
Resource accessTo = ResourceFactory.createResource(request.getUriInfo().getAbsolutePath().toString());
160-
161-
// special case where the agent is the owner of the requested document - automatically grant acl:Read/acl:Append/acl:Write access
160+
QuerySolutionMap thisQsm = new QuerySolutionMap();
161+
thisQsm.add(SPIN.THIS_VAR_NAME, accessTo);
162+
Model authorizations = ModelFactory.createDefaultModel();
163+
164+
// the agent is the owner of the requested document - automatically grant acl:Read/acl:Append/acl:Write access.
165+
// Note: the document does not even need to have a type at this point.
162166
if (agent != null && isOwner(accessTo, agent))
163167
{
164168
log.debug("Agent <{}> is the owner of <{}>, granting acl:Read/acl:Append/acl:Write access", agent, accessTo);
165-
return createOwnerAuthorization(accessTo, agent);
169+
createOwnerAuthorization(authorizations, accessTo, agent);
166170
}
167-
168-
QuerySolutionMap thisQsm = new QuerySolutionMap();
169-
thisQsm.add(SPIN.THIS_VAR_NAME, accessTo);
170171

171172
ResultSetRewindable docTypesResult = loadResultSet(getApplication().getService(), getDocumentTypeQuery(), thisQsm);
172173
try
@@ -192,24 +193,30 @@ public Resource authorize(ContainerRequestContext request, Resource agent, Resou
192193
// only root and containers allow child documents. This needs to be checked before checking ownership
193194
if (Collections.disjoint(parentTypes, Set.of(Default.Root, DH.Container))) return null;
194195
docTypesResult.reset(); // rewind result set to the beginning - it's used again later on
195-
196-
// special case where the agent is the owner of the requested document - automatically grant acl:Read/acl:Append/acl:Write access
196+
197+
// the agent is the owner of the requested document - automatically grant acl:Read/acl:Append/acl:Write access
197198
if (agent != null && isOwner(accessTo, agent))
198199
{
199200
log.debug("Agent <{}> is the owner of <{}>, granting acl:Read/acl:Append/acl:Write access", agent, accessTo);
200-
return createOwnerAuthorization(accessTo, agent);
201+
createOwnerAuthorization(authorizations, accessTo, agent);
201202
}
202203
}
204+
// access to non-existing documents is denied if the request method is not PUT *and* the agent has no Write access
203205
else return null;
204206
}
205-
207+
206208
ParameterizedSparqlString pss = getApplication().canAs(EndUserApplication.class) ? getACLQuery() : getOwnerACLQuery();
207209
Query query = new SetResultSetValues().apply(pss.asQuery(), docTypesResult);
208210
pss = new ParameterizedSparqlString(query.toString()); // make sure VALUES are now part of the query string
209211
assert pss.toString().contains("VALUES");
210212

211-
Model authModel = loadModel(getAdminService(), pss, new AuthorizationParams(getApplication().getBase(), accessTo, agent).get());
212-
return getAuthorizationByMode(authModel, accessMode);
213+
// note we're not setting the $mode value on the ACL queries as we want to provide the AuthorizationContext with all of the agent's authorizations
214+
authorizations.add(loadModel(getAdminService(), pss, new AuthorizationParams(getApplication().getBase(), accessTo, agent).get()));
215+
216+
// access denied if the agent has no authorization to the requested document with the requested ACL mode
217+
if (getAuthorizationByMode(authorizations, accessMode) == null) return null;
218+
219+
return authorizations;
213220
}
214221
finally
215222
{
@@ -326,17 +333,18 @@ protected ResultSetRewindable loadResultSet(com.atomgraph.linkeddatahub.model.Se
326333

327334
/**
328335
* Creates a special <code>acl:Authorization</code> resource for an owner.
336+
* @param model RDF model
329337
* @param accessTo requested URI
330338
* @param agent authenticated agent
331339
* @return authorization resource
332340
*/
333-
public Resource createOwnerAuthorization(Resource accessTo, Resource agent)
341+
public Resource createOwnerAuthorization(Model model, Resource accessTo, Resource agent)
334342
{
343+
if (model == null) throw new IllegalArgumentException("Model cannot be null");
335344
if (accessTo == null) throw new IllegalArgumentException("Document resource cannot be null");
336345
if (agent == null) throw new IllegalArgumentException("Agent resource cannot be null");
337346

338-
return ModelFactory.createDefaultModel().
339-
createResource().
347+
return model.createResource().
340348
addProperty(RDF.type, ACL.Authorization).
341349
addProperty(RDF.type, LACL.OwnerAuthorization).
342350
addProperty(ACL.accessTo, accessTo).

src/main/java/com/atomgraph/linkeddatahub/server/filter/response/ResponseHeadersFilter.java

Lines changed: 10 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import com.atomgraph.linkeddatahub.apps.model.Application;
2424
import com.atomgraph.linkeddatahub.apps.model.Dataset;
2525
import com.atomgraph.linkeddatahub.model.auth.Agent;
26+
import com.atomgraph.linkeddatahub.server.model.impl.Dispatcher;
2627
import com.atomgraph.linkeddatahub.server.security.AuthorizationContext;
2728
import com.atomgraph.linkeddatahub.vocabulary.ACL;
2829
import java.io.IOException;
@@ -77,27 +78,17 @@ public void filter(ContainerRequestContext request, ContainerResponseContext res
7778
List<Object> linkValues = response.getHeaders().get(HttpHeaders.LINK);
7879
List<Link> links = parseLinkHeaderValues(linkValues);
7980

80-
// check whether Link rel=ldt:base is not already set. Link headers might be forwarded by ProxyResourceBase
81-
if (getLinksByRel(links, LDT.base.getURI()).isEmpty())
82-
{
83-
// add Link rel=ldt:base
84-
response.getHeaders().add(HttpHeaders.LINK, new Link(getApplication().getBaseURI(), LDT.base.getURI(), null));
81+
if (getLinksByRel(links, SD.endpoint.getURI()).isEmpty())
8582
// add Link rel=sd:endpoint.
8683
// TO-DO: The external SPARQL endpoint URL is different from the internal one currently specified as sd:endpoint in the context dataset
87-
response.getHeaders().add(HttpHeaders.LINK, new Link(request.getUriInfo().getBaseUriBuilder().path("sparql").build(), SD.endpoint.getURI(), null));
88-
// add Link rel=ldt:ontology, if the ontology URI is specified
89-
if (getApplication().getOntology() != null)
90-
response.getHeaders().add(HttpHeaders.LINK, new Link(URI.create(getApplication().getOntology().getURI()), LDT.ontology.getURI(), null));
91-
// add Link rel=ac:stylesheet, if the stylesheet URI is specified
92-
if (getApplication().getStylesheet() != null)
93-
response.getHeaders().add(HttpHeaders.LINK, new Link(URI.create(getApplication().getStylesheet().getURI()), AC.stylesheet.getURI(), null));
94-
}
95-
else
96-
{
97-
// add Link rel=sd:endpoint.
98-
if (getLinksByRel(links, SD.endpoint.getURI()).isEmpty() && getDataset().isPresent() && getDataset().get().getService() != null)
99-
response.getHeaders().add(HttpHeaders.LINK, new Link(URI.create(getDataset().get().getService().getSPARQLEndpoint().getURI()), SD.endpoint.getURI(), null));
100-
}
84+
response.getHeaders().add(HttpHeaders.LINK, new Link(request.getUriInfo().getBaseUriBuilder().path(Dispatcher.class, "getSPARQLEndpoint").build(), SD.endpoint.getURI(), null));
85+
86+
// add Link rel=ldt:ontology, if the ontology URI is specified
87+
if (getApplication().getOntology() != null)
88+
response.getHeaders().add(HttpHeaders.LINK, new Link(URI.create(getApplication().getOntology().getURI()), LDT.ontology.getURI(), null));
89+
// add Link rel=ac:stylesheet, if the stylesheet URI is specified
90+
if (getApplication().getStylesheet() != null)
91+
response.getHeaders().add(HttpHeaders.LINK, new Link(URI.create(getApplication().getStylesheet().getURI()), AC.stylesheet.getURI(), null));
10192

10293
if (response.getHeaders().get(HttpHeaders.LINK) != null)
10394
{

0 commit comments

Comments
 (0)