Skip to content

Commit 00f0d60

Browse files
namedgraphclaude
andauthored
Proxy URIs from namespace (#285)
* Serve namespace ontology terms from ProxyRequestFilter After the DataManager cache check, run a DESCRIBE query against the app's in-memory OntModel (full imports closure) before falling through to the external proxy or 405. Covers both slash-based term URIs (e.g. schema:category) and hash-based namespaces (e.g. sioc:ns → describes all sioc:ns#* terms via the WHERE clause). This allows facet headers and other term lookups to resolve from imported ontologies without requiring ENABLE_LINKED_DATA_PROXY=true. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Add proxy test for OntModel namespace term lookup Creates two OWL classes in a made-up hash-based namespace, clears the in-memory ontology, then retrieves the namespace document via ?uri= and verifies both class descriptions are returned. The namespace is not DataManager-mapped and not a registered app, so the request exercises the new OntModel DESCRIBE path in ProxyRequestFilter. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Fix ProxyRequestFilterTest after ontology injection added Set filter.ontology = Optional.empty() in setUp() so existing tests reach the proxy/NotAllowed paths they were designed to test. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Injection fixes --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent e20d1c1 commit 00f0d60

File tree

3 files changed

+104
-0
lines changed

3 files changed

+104
-0
lines changed
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
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 readers group to be able to read documents
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/readers/"
17+
18+
# use a made-up hash-based namespace: not mapped as a static file, not a registered app
19+
namespace_uri="http://made-up-test-ns.example/ns"
20+
class1="${namespace_uri}#ClassOne"
21+
class2="${namespace_uri}#ClassTwo"
22+
ontology_doc="${ADMIN_BASE_URL}ontologies/namespace/"
23+
namespace="${END_USER_BASE_URL}ns#"
24+
25+
# add two classes with URIs in the made-up namespace to the app's ontology
26+
27+
add-class.sh \
28+
-f "$OWNER_CERT_FILE" \
29+
-p "$OWNER_CERT_PWD" \
30+
-b "$ADMIN_BASE_URL" \
31+
--uri "$class1" \
32+
--label "Class One" \
33+
"$ontology_doc"
34+
35+
add-class.sh \
36+
-f "$OWNER_CERT_FILE" \
37+
-p "$OWNER_CERT_PWD" \
38+
-b "$ADMIN_BASE_URL" \
39+
--uri "$class2" \
40+
--label "Class Two" \
41+
"$ontology_doc"
42+
43+
# clear the in-memory ontology so the new classes are present on next request
44+
45+
clear-ontology.sh \
46+
-f "$OWNER_CERT_FILE" \
47+
-p "$OWNER_CERT_PWD" \
48+
-b "$ADMIN_BASE_URL" \
49+
--ontology "$namespace"
50+
51+
# request the namespace document URI (without fragment) via ?uri= proxy.
52+
# the namespace document is not DataManager-mapped and not a registered app,
53+
# so ProxyRequestFilter falls through to the OntModel DESCRIBE path, which
54+
# returns descriptions of all #-fragment terms in that namespace.
55+
56+
response=$(curl -k -f -s \
57+
-G \
58+
-E "$AGENT_CERT_FILE":"$AGENT_CERT_PWD" \
59+
-H "Accept: application/n-triples" \
60+
--data-urlencode "uri=${namespace_uri}" \
61+
"$END_USER_BASE_URL")
62+
63+
# verify both class descriptions are present in the response
64+
65+
echo "$response" | grep -q "$class1"
66+
echo "$response" | grep -q "$class2"

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

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import com.atomgraph.core.util.ModelUtils;
2424
import com.atomgraph.core.util.ResultSetUtils;
2525
import com.atomgraph.linkeddatahub.apps.model.Dataset;
26+
import org.apache.jena.ontology.Ontology;
2627
import com.atomgraph.linkeddatahub.client.GraphStoreClient;
2728
import com.atomgraph.linkeddatahub.client.filter.auth.IDTokenDelegationFilter;
2829
import com.atomgraph.linkeddatahub.client.filter.auth.WebIDDelegationFilter;
@@ -36,6 +37,8 @@
3637
import java.util.ArrayList;
3738
import java.util.List;
3839
import java.util.Optional;
40+
import org.apache.jena.query.QueryExecution;
41+
import org.apache.jena.query.QueryFactory;
3942
import jakarta.annotation.Priority;
4043
import jakarta.inject.Inject;
4144
import jakarta.ws.rs.NotAcceptableException;
@@ -91,6 +94,7 @@ public class ProxyRequestFilter implements ContainerRequestFilter
9194

9295
@Inject com.atomgraph.linkeddatahub.Application system;
9396
@Inject MediaTypes mediaTypes;
97+
@Inject jakarta.inject.Provider<Optional<Ontology>> ontology;
9498
@Context Request request;
9599

96100
@Override
@@ -123,6 +127,26 @@ public void filter(ContainerRequestContext requestContext) throws IOException
123127
return;
124128
}
125129

130+
// serve terms from the app's in-memory namespace ontology (full imports closure) via DESCRIBE.
131+
// covers both slash-based term URIs (e.g. schema:category) and hash-based namespaces
132+
// (e.g. sioc:UserAccount → ac:document-uri strips to sioc:ns, so we also describe all
133+
// ?term where STR(?term) starts with "<targetURI>#")
134+
if (getOntology().isPresent())
135+
{
136+
String describeQueryStr = "DESCRIBE <" + targetURI + "> ?term " +
137+
"WHERE { ?term ?p ?o FILTER(STRSTARTS(STR(?term), CONCAT(STR(<" + targetURI + ">), \"#\"))) }";
138+
try (QueryExecution qe = QueryExecution.create(QueryFactory.create(describeQueryStr), getOntology().get().getOntModel()))
139+
{
140+
Model description = qe.execDescribe();
141+
if (!description.isEmpty())
142+
{
143+
if (log.isDebugEnabled()) log.debug("Serving URI from namespace ontology: {}", targetURI);
144+
requestContext.abortWith(getResponse(description, Response.Status.OK));
145+
return;
146+
}
147+
}
148+
}
149+
126150
boolean isRegisteredApp = getSystem().matchApp(targetURI) != null;
127151
if (!isRegisteredApp && !getSystem().isEnableLinkedDataProxy())
128152
throw new NotAllowedException("Linked Data proxy not enabled");
@@ -312,6 +336,16 @@ public com.atomgraph.linkeddatahub.Application getSystem()
312336
return system;
313337
}
314338

339+
/**
340+
* Returns the current application's namespace ontology, if available.
341+
*
342+
* @return optional ontology
343+
*/
344+
public Optional<Ontology> getOntology()
345+
{
346+
return ontology.get();
347+
}
348+
315349
/**
316350
* Returns the media types registry.
317351
*

src/test/java/com/atomgraph/linkeddatahub/server/filter/request/ProxyRequestFilterTest.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import com.atomgraph.client.MediaTypes;
2020
import com.atomgraph.client.util.DataManager;
2121
import com.atomgraph.linkeddatahub.server.security.AgentContext;
22+
import org.apache.jena.ontology.Ontology;
2223
import com.atomgraph.linkeddatahub.server.util.URLValidator;
2324
import com.atomgraph.linkeddatahub.vocabulary.LAPP;
2425
import jakarta.ws.rs.NotAllowedException;
@@ -34,6 +35,7 @@
3435
import java.io.IOException;
3536
import java.net.URI;
3637
import java.util.List;
38+
import java.util.Optional;
3739
import org.apache.jena.query.ResultSet;
3840
import org.apache.jena.rdf.model.Model;
3941
import org.apache.jena.rdf.model.Resource;
@@ -60,6 +62,7 @@ public class ProxyRequestFilterTest
6062
@Mock com.atomgraph.linkeddatahub.Application system;
6163
@Mock MediaTypes mediaTypes;
6264
@Mock Request request;
65+
@Mock Ontology ontology;
6366

6467
@InjectMocks ProxyRequestFilter filter;
6568

@@ -85,6 +88,7 @@ public void setUp()
8588
when(system.getDataManager()).thenReturn(dataManager);
8689
when(dataManager.isMapped(anyString())).thenReturn(false);
8790
when(system.isEnableLinkedDataProxy()).thenReturn(false);
91+
filter.ontology = () -> Optional.empty();
8892
}
8993

9094
/**

0 commit comments

Comments
 (0)