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
61 changes: 61 additions & 0 deletions src/main/java/com/knowledgepixels/query/SpacesExtractor.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.eclipse.rdf4j.model.IRI;
Expand Down Expand Up @@ -252,6 +254,65 @@ private static void emitSpaceEntry(Nanopub np, Context ctx, IRI spaceIri, IRI ro
out.add(vf.createStatement(riIri, SpacesVocab.VIA_NANOPUB, np.getUri(), GRAPH));
addProvenance(riIri, ctx, out);
}

// Inline non-hasAdmin role triples: a gen:Space nanopub may also assert
// <space> <pred> <agent> (INVERSE) or <agent> <pred> <space> (REGULAR)
// for any of the back-compat role predicates (has-event-facilitator,
// participatedAsParticipantIn, …). Without an extraction path here those
// are silently dropped because gen:Space nanopubs are not auto-typed
// with back-compat predicates (only single-triple-assertion nanopubs are).
// Emit one RoleInstantiation per distinct predicate found, grouping
// multi-agent like the admin case. The subject is disambiguated by a
// hash of the predicate IRI so multiple predicates in one nanopub don't
// collide on the same npari:<artifactCode> subject as the admin RI.
emitInlineRoleInstantiations(np, ctx, spaceIri, out);
}

/**
* Scans the assertion of a {@code gen:Space} nanopub for inline role triples
* (excluding {@code gen:hasAdmin}, which is handled separately as the trust
* seed), grouping by predicate and emitting one {@link GEN#ROLE_INSTANTIATION}
* per (predicate, direction) pair with multi-valued {@code npa:forAgent}.
*/
private static void emitInlineRoleInstantiations(Nanopub np, Context ctx, IRI spaceIri,
List<Statement> out) {
Map<IRI, BackcompatRolePredicates.Direction> directionByPred = new LinkedHashMap<>();
Map<IRI, Set<IRI>> agentsByPred = new LinkedHashMap<>();
for (Statement st : np.getAssertion()) {
IRI predicate = st.getPredicate();
if (GEN.HAS_ADMIN.equals(predicate)) continue; // already emitted above
BackcompatRolePredicates.Direction direction = BackcompatRolePredicates.DIRECTIONS.get(predicate);
if (direction == null) continue;
if (!(st.getSubject() instanceof IRI subjIri)) continue;
if (!(st.getObject() instanceof IRI objIri)) continue;
IRI agent;
if (direction == BackcompatRolePredicates.Direction.INVERSE) {
if (!spaceIri.equals(subjIri)) continue;
agent = objIri;
} else {
if (!spaceIri.equals(objIri)) continue;
agent = subjIri;
}
directionByPred.put(predicate, direction);
agentsByPred.computeIfAbsent(predicate, k -> new LinkedHashSet<>()).add(agent);
}
for (Map.Entry<IRI, Set<IRI>> entry : agentsByPred.entrySet()) {
IRI predicate = entry.getKey();
BackcompatRolePredicates.Direction direction = directionByPred.get(predicate);
String predHash = Utils.createHash(predicate.stringValue());
IRI riIri = SpacesVocab.forRoleInstantiation(ctx.artifactCode(), predHash);
out.add(vf.createStatement(riIri, RDF.TYPE, GEN.ROLE_INSTANTIATION, GRAPH));
out.add(vf.createStatement(riIri, SpacesVocab.FOR_SPACE, spaceIri, GRAPH));
IRI directionPredicate = (direction == BackcompatRolePredicates.Direction.REGULAR)
? SpacesVocab.REGULAR_PROPERTY
: SpacesVocab.INVERSE_PROPERTY;
out.add(vf.createStatement(riIri, directionPredicate, predicate, GRAPH));
for (IRI agent : entry.getValue()) {
out.add(vf.createStatement(riIri, SpacesVocab.FOR_AGENT, agent, GRAPH));
}
out.add(vf.createStatement(riIri, SpacesVocab.VIA_NANOPUB, np.getUri(), GRAPH));
addProvenance(riIri, ctx, out);
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ private static IRI iri(String s) {
// FAIR 3pff
Map.entry(iri("https://w3id.org/fair/3pff/has-event-assistant"), Direction.INVERSE),
Map.entry(iri("https://w3id.org/fair/3pff/has-event-facilitator"), Direction.INVERSE),
Map.entry(iri("https://w3id.org/fair/3pff/has-event-organizer"), Direction.INVERSE),
Map.entry(iri("https://w3id.org/fair/3pff/participatedAsFacilitatorAssistantIn"), Direction.REGULAR),
Map.entry(iri("https://w3id.org/fair/3pff/participatedAsFacilitatorIn"), Direction.REGULAR),
Map.entry(iri("https://w3id.org/fair/3pff/participatedAsImplementerAspirantIn"), Direction.REGULAR),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,17 @@ public static IRI forRoleInstantiation(String artifactCode) {
return vf.createIRI(NPARI_NAMESPACE, artifactCode);
}

/**
* Mints {@code npari:<artifactCode>_<discriminatorHash>} for a role-instantiation
* entry where a single nanopub emits multiple RIs (e.g. a {@code gen:Space}
* nanopub with inline role-predicate triples for several distinct roles).
* The hash is on the predicate IRI so RIs for different roles in the same
* nanopub get distinct subjects.
*/
public static IRI forRoleInstantiation(String artifactCode, String discriminatorHash) {
return vf.createIRI(NPARI_NAMESPACE, artifactCode + "_" + discriminatorHash);
}

/** Mints {@code npara:<artifactCode>} for a role-attachment entry. */
public static IRI forRoleAssignment(String artifactCode) {
return vf.createIRI(NPARA_NAMESPACE, artifactCode);
Expand Down
47 changes: 47 additions & 0 deletions src/test/java/com/knowledgepixels/query/SpacesExtractorTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,53 @@ void extract_spaceSelfRooted_emitsSpaceRefSpaceDefinitionAndAdminRoleInstantiati
assertContains(out, riIri, VIA_NANOPUB, NP_URI);
}

@Test
void extract_spaceWithInlineRoleTriples_emitsAdditionalRoleInstantiations() throws Exception {
// A gen:Space nanopub may also declare other role-predicate assignments
// inline alongside hasAdmin (event facilitators, participants, etc.).
// Each distinct predicate should mint its own RoleInstantiation entry
// — gen:hasAdmin keeps the unhashed subject npari:<artifactCode> (existing
// behaviour); others use npari:<artifactCode>_<predicateHash> to avoid
// collision.
IRI facilitatorPred = vf.createIRI("https://w3id.org/fair/3pff/has-event-facilitator");
IRI participantPred = vf.createIRI("https://w3id.org/fair/3pff/participatedAsParticipantIn");
IRI participantAgent = vf.createIRI("https://orcid.org/0000-0000-0000-0004");

Nanopub np = creator()
.type(GEN.SPACE)
.assertion(SPACE_IRI_1, RDF.TYPE, GEN.SPACE)
.assertion(SPACE_IRI_1, GEN.HAS_ROOT_DEFINITION, NP_URI)
.assertion(SPACE_IRI_1, GEN.HAS_ADMIN, ADMIN_AGENT_1)
.assertion(SPACE_IRI_1, facilitatorPred, ADMIN_AGENT_1)
.assertion(SPACE_IRI_1, facilitatorPred, ADMIN_AGENT_2)
.assertion(participantAgent, participantPred, SPACE_IRI_1)
.finalizeNanopub();

List<Statement> out = SpacesExtractor.extract(np, defaultContext());
IRI adminRi = forRoleInstantiation(ARTIFACT_CODE);
IRI facilitatorRi = forRoleInstantiation(ARTIFACT_CODE, Utils.createHash(facilitatorPred.stringValue()));
IRI participantRi = forRoleInstantiation(ARTIFACT_CODE, Utils.createHash(participantPred.stringValue()));

// Admin RI unchanged (same subject as before).
assertContains(out, adminRi, RDF.TYPE, GEN.ROLE_INSTANTIATION);
assertContains(out, adminRi, INVERSE_PROPERTY, GEN.HAS_ADMIN);
assertContains(out, adminRi, FOR_AGENT, ADMIN_AGENT_1);

// Facilitator RI: INVERSE (space-centric), multi-valued forAgent.
assertContains(out, facilitatorRi, RDF.TYPE, GEN.ROLE_INSTANTIATION);
assertContains(out, facilitatorRi, FOR_SPACE, SPACE_IRI_1);
assertContains(out, facilitatorRi, INVERSE_PROPERTY, facilitatorPred);
assertContains(out, facilitatorRi, FOR_AGENT, ADMIN_AGENT_1);
assertContains(out, facilitatorRi, FOR_AGENT, ADMIN_AGENT_2);
assertContains(out, facilitatorRi, VIA_NANOPUB, NP_URI);

// Participant RI: REGULAR (agent-centric).
assertContains(out, participantRi, RDF.TYPE, GEN.ROLE_INSTANTIATION);
assertContains(out, participantRi, FOR_SPACE, SPACE_IRI_1);
assertContains(out, participantRi, REGULAR_PROPERTY, participantPred);
assertContains(out, participantRi, FOR_AGENT, participantAgent);
}

@Test
void extract_spaceUpdate_emitsSpaceEntryButNoRootAdminSeed() throws Exception {
// Update: gen:hasRootDefinition points at a different (original root) nanopub.
Expand Down