Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,10 @@ Key implementation files:

The SPARQL endpoint forwarding chain ensures ContentMode blocks (charts, maps) query the **remote** app's SPARQL endpoint, not the local one. `LinkedDataHub.endpoint` is reset to the local endpoint by `ldh:HTMLDocumentLoaded` on every HTML page navigation, so there is no stale state when navigating back to local documents.

### UI Conventions

**Form placement follows document membership.** A creation/edit form for a resource that lives (or will live) in its **own document** renders as a modal (`div.modal.modal-constructor` — Container/Item creation, document edit, inline creation in views); a form for a resource being added **to the current document** renders inline in the content body (`ldh:render-add-row-form`, content-block forms). The main content body renders only the current document's content.

### Key Extension Points
- **Vocabulary definitions** in `com.atomgraph.linkeddatahub.vocabulary`
- **Custom resource handlers** in `com.atomgraph.linkeddatahub.resource`
Expand Down
22 changes: 20 additions & 2 deletions src/main/resources/com/atomgraph/linkeddatahub/ldh.ttl
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
owl:imports dh:, ac:, <http://spinrdf.org/spin>, foaf:, sd:, nfo:, owl: ;
rdfs:label "LinkedDataHub ontology" ;
rdfs:comment "Should be imported and reused by all extending applications" ;
owl:versionInfo "1.1.6" .
owl:versionInfo "1.1.7" .

# PROPERTIES

Expand Down Expand Up @@ -105,6 +105,20 @@
rdfs:comment "Attaches a view to a property for inverse relationships (?value property $about)" ;
rdfs:isDefinedBy : .

:container a owl:ObjectProperty ;
rdfs:domain :View ;
rdfs:range dh:Container ;
rdfs:label "Item container" ;
rdfs:comment "Container under which instances created inline from the view are stored as child documents" ;
rdfs:isDefinedBy : .

:showWhenEmpty a owl:DatatypeProperty ;
rdfs:domain :View ;
rdfs:range xsd:boolean ;
rdfs:label "Show when empty" ;
rdfs:comment "Whether the view block is shown when its query returns no results. Defaults to true" ;
rdfs:isDefinedBy : .

# CLASSES

# constructor
Expand Down Expand Up @@ -324,11 +338,15 @@
PREFIX spin: <http://spinrdf.org/spin#>
PREFIX sp: <http://spinrdf.org/sp#>
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
PREFIX xsd: <http://www.w3.org/2001/XMLSchema#>
PREFIX ac: <https://w3id.org/atomgraph/client#>
PREFIX ldh: <https://w3id.org/atomgraph/linkeddatahub#>

CONSTRUCT {
$this spin:query [ a sp:Select ] ;
ac:mode [ a rdfs:Resource ] .
ac:mode [ a rdfs:Resource ] ;
ldh:container [ a rdfs:Resource ] ;
ldh:showWhenEmpty [ a xsd:boolean ] .
}
WHERE {}""" ;
rdfs:label "View constructor" ;
Expand Down
8 changes: 4 additions & 4 deletions src/main/resources/com/linkeddatahub/packages/skos/ns.ttl
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ WHERE
GRAPH ?narrowerGraph
{
?narrower skos:prefLabel ?prefLabel .
FILTER (langMatches(lang(?prefLabel), "en"))
FILTER (langMatches(lang(?prefLabel), "en") || lang(?prefLabel) = "")
}
}
}
Expand Down Expand Up @@ -84,7 +84,7 @@ WHERE
GRAPH ?broaderGraph
{
?broader skos:prefLabel ?prefLabel .
FILTER (langMatches(lang(?prefLabel), "en"))
FILTER (langMatches(lang(?prefLabel), "en") || lang(?prefLabel) = "")
}
}
}
Expand Down Expand Up @@ -130,7 +130,7 @@ WHERE
GRAPH ?memberGraph
{
?member skos:prefLabel ?prefLabel .
FILTER (langMatches(lang(?prefLabel), "en"))
FILTER (langMatches(lang(?prefLabel), "en") || lang(?prefLabel) = "")
}
}
}
Expand Down Expand Up @@ -174,7 +174,7 @@ WHERE
{ GRAPH ?graph
{ ?concept skos:inScheme $about ;
skos:prefLabel ?prefLabel .
FILTER (langMatches(lang(?prefLabel), "en"))
FILTER (langMatches(lang(?prefLabel), "en") || lang(?prefLabel) = "")
}
}
ORDER BY ?prefLabel
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,27 +116,44 @@ exclude-result-prefixes="#all"
]]>
</xsl:variable>

<!-- combined forward + inverse view-block lookup; substitutes VALUES ?type { ... } from the resource's rdf:types -->
<!-- combined forward + inverse view-block lookup; substitutes VALUES ?type { ... } from the resource's rdf:types.
Besides ?block, binds the inline-creation metadata: attachment ?property, ?inverse direction flag, the ?forClass
to construct (property range for forward views, domain for inverse ones) and the view's ldh:container/ldh:showWhenEmpty.
Runs against the /ns ontology union, so ldh:container asserted on a package view from the app's own namespace is visible. -->
<xsl:variable name="ontology-view-query" as="xs:string">
<![CDATA[
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
PREFIX ldh: <https://w3id.org/atomgraph/linkeddatahub#>

SELECT DISTINCT ?block
SELECT DISTINCT ?block ?property ?inverse ?forClass ?container ?showWhenEmpty
WHERE {
{
?property ldh:view ?block .
{ ?property rdfs:domain ?type }
UNION
{ ?property rdfs:subPropertyOf+/rdfs:domain ?type }
BIND(false AS ?inverse)
OPTIONAL {
{ ?property rdfs:range ?forClass }
UNION
{ ?property rdfs:subPropertyOf+/rdfs:range ?forClass }
}
}
UNION
{
?property ldh:inverseView ?block .
{ ?property rdfs:range ?type }
UNION
{ ?property rdfs:subPropertyOf+/rdfs:range ?type }
BIND(true AS ?inverse)
OPTIONAL {
{ ?property rdfs:domain ?forClass }
UNION
{ ?property rdfs:subPropertyOf+/rdfs:domain ?forClass }
}
}
OPTIONAL { ?block ldh:container ?container }
OPTIONAL { ?block ldh:showWhenEmpty ?showWhenEmpty }
}
]]>
<!-- VALUES ?type goes here -->
Expand Down Expand Up @@ -673,13 +690,31 @@ exclude-result-prefixes="#all"

<xsl:if test="$response?status = 200 and $response?media-type = 'application/sparql-results+xml'">
<xsl:variable name="results" select="$response?body" as="document-node()"/>
<xsl:variable name="view-uris" select="distinct-values($results/srx:sparql/srx:results/srx:result/srx:binding[@name = 'block']/srx:uri/xs:anyURI(.))" as="xs:anyURI*"/>

<xsl:for-each select="$view-uris">
<xsl:variable name="view-uri" select="xs:anyURI(.)" as="xs:anyURI"/>
<!-- one fetch per view; the first row of each group supplies the inline-creation metadata (multiple ?type/?forClass rows can occur) -->
<xsl:for-each-group select="$results/srx:sparql/srx:results/srx:result[srx:binding[@name = 'block']/srx:uri]" group-by="srx:binding[@name = 'block']/srx:uri">
<xsl:variable name="view-uri" select="xs:anyURI(current-grouping-key())" as="xs:anyURI"/>
<xsl:variable name="view-request-uri" select="ldh:href(ac:document-uri($view-uri), map{})" as="xs:anyURI"/>
<xsl:variable name="view-request" select="map{ 'method': 'GET', 'href': $view-request-uri, 'headers': map{ 'Accept': 'application/rdf+xml' } }" as="map(*)"/>
<xsl:variable name="view-context" as="map(*)" select="map:merge(($context, map{ 'request': $view-request, 'view-uri': $view-uri }))"/>
<!-- ?forClass is read from srx:uri only: bnode ranges (e.g. owl:unionOf lists) cannot be constructed -->
<xsl:variable name="view-metadata" as="map(*)*">
<xsl:for-each select="srx:binding[@name = 'property']/srx:uri">
<xsl:sequence select="map{ 'view-property': xs:anyURI(.) }"/>
</xsl:for-each>
<xsl:if test="srx:binding[@name = 'inverse']/srx:literal = 'true'">
<xsl:sequence select="map{ 'view-inverse': true() }"/>
</xsl:if>
<xsl:for-each select="srx:binding[@name = 'forClass']/srx:uri">
<xsl:sequence select="map{ 'view-for-class': xs:anyURI(.) }"/>
</xsl:for-each>
<xsl:for-each select="srx:binding[@name = 'container']/srx:uri">
<xsl:sequence select="map{ 'view-container': xs:anyURI(.) }"/>
</xsl:for-each>
<xsl:for-each select="srx:binding[@name = 'showWhenEmpty']/srx:literal">
<xsl:sequence select="map{ 'view-show-when-empty': string(.) }"/>
</xsl:for-each>
</xsl:variable>
<xsl:variable name="view-context" as="map(*)" select="map:merge(($context, map{ 'request': $view-request, 'view-uri': $view-uri }, $view-metadata))"/>

<ixsl:promise select="
ixsl:http-request($view-request) =>
Expand All @@ -700,22 +735,60 @@ exclude-result-prefixes="#all"
ixsl:then(ldh:ontology-view-render-thunk#1)
"
on-failure="ldh:promise-failure#1"/>
</xsl:for-each>
</xsl:for-each-group>
</xsl:if>

<xsl:sequence select="$context"/>
</xsl:function>

<!-- render one view block from its loaded RDF, append into the wrapper's .span12 alongside the typed resource block, hydrate via existing ldh:RenderRow chain -->
<!-- render one view block from its loaded RDF. Creatable views (ldh:container + inferred forClass) first HEAD the container for its acl:mode Link headers, which gate the Create button; the rest insert directly -->
<xsl:function name="ldh:ontology-view-render-thunk" as="map(*)" ixsl:updating="yes">
<xsl:param name="context" as="map(*)"/>

<xsl:message>ldh:ontology-view-render-thunk</xsl:message>

<xsl:choose>
<xsl:when test="map:contains($context, 'view-container') and map:contains($context, 'view-for-class')">
<xsl:variable name="container-request" select="map{ 'method': 'HEAD', 'href': ldh:href($context('view-container'), map{}), 'headers': map{ 'Accept': 'application/rdf+xml' } }" as="map(*)"/>
<xsl:variable name="container-context" select="map:merge(($context, map{ 'container-request': $container-request }))" as="map(*)"/>

<ixsl:promise select="
ixsl:resolve($container-context) =>
ixsl:then(ldh:http-request-threaded(?, 'container-request', 'container-response')) =>
ixsl:then(ldh:handle-response(?, 'container-response')) =>
ixsl:then(ldh:set-container-acl-modes#1) =>
ixsl:then(ldh:ontology-view-insert#1)
"
on-failure="ldh:promise-failure#1"/>

<xsl:sequence select="$context"/>
</xsl:when>
<xsl:otherwise>
<xsl:sequence select="ldh:ontology-view-insert($context)"/>
</xsl:otherwise>
</xsl:choose>
</xsl:function>

<!-- extract acl:mode URIs from the container HEAD response's Link headers (emitted by ResponseHeadersFilter, forwarded for remote containers by ProxyRequestFilter); same parsing as ldh:rdf-document-response. A non-200 response yields no modes, which downstream reads as no Create button -->
<xsl:function name="ldh:set-container-acl-modes" as="map(*)" ixsl:updating="no">
<xsl:param name="context" as="map(*)"/>
<xsl:variable name="response" select="$context('container-response')" as="map(*)"/>
<xsl:variable name="acl-mode-links" select="tokenize($response?headers?link, ',')[contains(., '&acl;mode')]" as="xs:string*"/>
<xsl:variable name="acl-modes" select="for $mode-link in $acl-mode-links return xs:anyURI(substring-before(substring-after(substring-before($mode-link, ';'), '&lt;'), '&gt;'))" as="xs:anyURI*"/>

<xsl:sequence select="map:merge(($context, map{ 'container-acl-modes': $acl-modes }))"/>
</xsl:function>

<!-- append the rendered view block into the wrapper's .span12 alongside the typed resource block, stamp the inline-creation metadata as data-* attributes, hydrate via existing ldh:RenderRow chain -->
<xsl:function name="ldh:ontology-view-insert" as="map(*)" ixsl:updating="yes">
<xsl:param name="context" as="map(*)"/>
<xsl:variable name="response" select="$context('response')" as="map(*)"/>
<xsl:variable name="container" select="$context('container')" as="element()"/>
<xsl:variable name="span12" select="$container/div[contains-token(@class, 'span12')]" as="element()"/>
<xsl:variable name="view-uri" select="$context('view-uri')" as="xs:anyURI"/>
<xsl:variable name="base-uri" select="$context('base-uri')" as="xs:anyURI"/>

<xsl:message>ldh:ontology-view-render-thunk</xsl:message>
<xsl:message>ldh:ontology-view-insert</xsl:message>

<xsl:if test="$response?status = 200 and $response?media-type = 'application/rdf+xml'">
<xsl:variable name="view-rdf" select="$response?body" as="document-node()"/>
Expand All @@ -738,6 +811,27 @@ exclude-result-prefixes="#all"
<!-- hydrate the freshly-injected wrapper via the existing view.xsl:62 RenderRow handler -->
<xsl:variable name="injected" select="$span12/*[last()]" as="element()?"/>
<xsl:if test="$injected">
<!-- stamp inline-creation metadata before hydration so ldh:RenderViewResults can read it off the wrapper -->
<xsl:for-each select="$injected">
<xsl:if test="map:contains($context, 'view-property')">
<ixsl:set-attribute name="data-property" select="string($context('view-property'))" object="."/>
</xsl:if>
<xsl:if test="map:contains($context, 'view-inverse')">
<ixsl:set-attribute name="data-inverse" select="'true'" object="."/>
</xsl:if>
<xsl:if test="map:contains($context, 'view-for-class')">
<ixsl:set-attribute name="data-for-class" select="string($context('view-for-class'))" object="."/>
</xsl:if>
<xsl:if test="map:contains($context, 'view-container')">
<ixsl:set-attribute name="data-container" select="string($context('view-container'))" object="."/>
</xsl:if>
<xsl:if test="map:contains($context, 'view-show-when-empty')">
<ixsl:set-attribute name="data-show-when-empty" select="string($context('view-show-when-empty'))" object="."/>
</xsl:if>
<xsl:if test="map:contains($context, 'container-acl-modes')">
<ixsl:set-attribute name="data-acl-modes" select="string-join($context('container-acl-modes'), ' ')" object="."/>
</xsl:if>
</xsl:for-each>
<xsl:variable name="factories" as="(function(item()?) as item()*)*">
<xsl:apply-templates select="$injected" mode="ldh:RenderRow"/>
</xsl:variable>
Expand Down
Loading
Loading