• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

knowledgepixels / nanodash / 27145358627

08 Jun 2026 02:39PM UTC coverage: 20.682% (-0.3%) from 20.947%
27145358627

push

github

web-flow
Merge pull request #479 from knowledgepixels/feat/about-pages-478

Resource-page tabs, presets, and role-gated view actions (#478, #302)

1052 of 6429 branches covered (16.36%)

Branch coverage included in aggregate %.

2642 of 11432 relevant lines covered (23.11%)

3.31 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

69.77
src/main/java/com/knowledgepixels/nanodash/SpaceMemberRole.java
1
package com.knowledgepixels.nanodash;
2

3
import com.google.common.collect.Multimap;
4
import com.knowledgepixels.nanodash.domain.AbstractResourceWithProfile;
5
import com.knowledgepixels.nanodash.domain.IndividualAgent;
6
import com.knowledgepixels.nanodash.domain.Space;
7
import com.knowledgepixels.nanodash.template.Template;
8
import com.knowledgepixels.nanodash.template.TemplateData;
9
import com.knowledgepixels.nanodash.vocabulary.KPXL_TERMS;
10
import org.eclipse.rdf4j.model.IRI;
11
import org.nanopub.extra.services.ApiResponseEntry;
12

13
import java.io.Serializable;
14
import java.util.Set;
15
import java.util.stream.Stream;
16

17
/**
18
 * A role that a space member can have, with associated properties.
19
 */
20
public class SpaceMemberRole implements Serializable {
21

22
    private IRI id;
23
    private String label, name, title;
24
    private Template roleAssignmentTemplate = null;
18✔
25
    private IRI[] regularProperties, inverseProperties;
26
    private IRI tier;
27

28
    /**
29
     * Rank of the "everyone" floor (no role held). Below {@link #OBSERVER_RANK}.
30
     */
31
    public static final int EVERYONE_RANK = 0;
32
    private static final int OBSERVER_RANK = 1;
33
    private static final int MEMBER_RANK = 2;
34
    private static final int MAINTAINER_RANK = 3;
35
    private static final int ADMIN_RANK = 4;
36

37
    /**
38
     * Construct a SpaceMemberRole from an API response entry.
39
     *
40
     * @param e The API response entry.
41
     */
42
    public SpaceMemberRole(ApiResponseEntry e) {
6✔
43
        this.id = Utils.vf.createIRI(e.get("role"));
21✔
44
        this.label = e.get("roleLabel");
15✔
45
        this.name = e.get("roleName");
15✔
46
        this.title = e.get("roleTitle");
15✔
47
        if (e.get("roleAssignmentTemplate") != null && !e.get("roleAssignmentTemplate").isBlank()) {
12!
48
            this.roleAssignmentTemplate = TemplateData.get().getTemplate(e.get("roleAssignmentTemplate"));
×
49
        }
50
        regularProperties = stringToIriArray(e.get("regularProperties"));
18✔
51
        inverseProperties = stringToIriArray(e.get("inverseProperties"));
18✔
52
        this.tier = parseTier(e.get("roleType"));
18✔
53
    }
3✔
54

55
    private SpaceMemberRole(IRI id, String label, String name, String title, Template roleAssignmentTemplate, IRI[] regularProperties, IRI[] inverseProperties, IRI tier) {
6✔
56
        this.id = id;
9✔
57
        this.label = label;
9✔
58
        this.name = name;
9✔
59
        this.title = title;
9✔
60
        this.roleAssignmentTemplate = roleAssignmentTemplate;
9✔
61
        this.regularProperties = regularProperties;
9✔
62
        this.inverseProperties = inverseProperties;
9✔
63
        this.tier = tier;
9✔
64
    }
3✔
65

66
    /**
67
     * Parse the role tier from the {@code roleType} query column (the
68
     * server-materialized {@code npa:hasRoleType} value). Defaults to
69
     * {@link KPXL_TERMS#OBSERVER_ROLE} when absent, matching the server-side
70
     * default for roles that declare no tier subclass.
71
     *
72
     * @param roleType the role-type IRI string, or null/blank
73
     * @return the tier IRI (never null)
74
     */
75
    private static IRI parseTier(String roleType) {
76
        if (roleType == null || roleType.isBlank()) return KPXL_TERMS.OBSERVER_ROLE;
21!
77
        return Utils.vf.createIRI(roleType);
12✔
78
    }
79

80
    /**
81
     * Check if this role is the admin role.
82
     *
83
     * @return True if this role is the admin role, false otherwise.
84
     */
85
    public boolean isAdminRole() {
86
        return id.equals(ADMIN_ROLE_IRI);
15✔
87
    }
88

89
    /**
90
     * Get the IRI of this role.
91
     *
92
     * @return The IRI of this role.
93
     */
94
    public IRI getId() {
95
        return id;
9✔
96
    }
97

98
    /**
99
     * Get the label of this role.
100
     *
101
     * @return The label of this role.
102
     */
103
    public String getLabel() {
104
        return label;
9✔
105
    }
106

107
    /**
108
     * Get the name of this role.
109
     *
110
     * @return The name of this role.
111
     */
112
    public String getName() {
113
        return name;
9✔
114
    }
115

116
    /**
117
     * Get the title of this role.
118
     *
119
     * @return The title of this role.
120
     */
121
    public String getTitle() {
122
        return title;
9✔
123
    }
124

125
    /**
126
     * Get the template used for assigning this role.
127
     *
128
     * @return The template used for assigning this role.
129
     */
130
    public Template getRoleAssignmentTemplate() {
131
        return roleAssignmentTemplate;
×
132
    }
133

134
    /**
135
     * Get the regular properties associated with this role.
136
     *
137
     * @return The regular properties associated with this role.
138
     */
139
    public IRI[] getRegularProperties() {
140
        return regularProperties;
9✔
141
    }
142

143
    /**
144
     * Get the inverse properties associated with this role.
145
     *
146
     * @return The inverse properties associated with this role.
147
     */
148
    public IRI[] getInverseProperties() {
149
        return inverseProperties;
9✔
150
    }
151

152
    /**
153
     * Get the tier (role class) of this role — one of the role-tier IRIs in
154
     * {@link KPXL_TERMS} ({@code ADMIN_ROLE_TYPE} / {@code MAINTAINER_ROLE} /
155
     * {@code MEMBER_ROLE} / {@code OBSERVER_ROLE}).
156
     *
157
     * @return The tier IRI (never null; defaults to observer).
158
     */
159
    public IRI getTier() {
160
        return tier;
9✔
161
    }
162

163
    /**
164
     * Get the numeric rank of this role's tier, for threshold comparisons
165
     * (admin {@literal >} maintainer {@literal >} member {@literal >} observer).
166
     *
167
     * @return The tier rank (1..4).
168
     */
169
    public int getTierRank() {
170
        return tierRank(tier);
12✔
171
    }
172

173
    /**
174
     * Numeric rank of a role-tier IRI, for threshold comparisons. Unknown or
175
     * null tiers (the "everyone" floor) rank below observer.
176
     *
177
     * @param tier a role-tier IRI, or null
178
     * @return the rank: admin=4, maintainer=3, member=2, observer=1, else 0
179
     */
180
    public static int tierRank(IRI tier) {
181
        if (KPXL_TERMS.ADMIN_ROLE_TYPE.equals(tier)) return ADMIN_RANK;
18✔
182
        if (KPXL_TERMS.MAINTAINER_ROLE.equals(tier)) return MAINTAINER_RANK;
18✔
183
        if (KPXL_TERMS.MEMBER_ROLE.equals(tier)) return MEMBER_RANK;
18✔
184
        if (KPXL_TERMS.OBSERVER_ROLE.equals(tier)) return OBSERVER_RANK;
18✔
185
        return EVERYONE_RANK;
6✔
186
    }
187

188
    /**
189
     * Whether the given IRI is one of the known role-tier IRIs (as opposed to a
190
     * specific role IRI). Used to interpret {@code gen:isVisibleTo} objects.
191
     *
192
     * @param iri an IRI, or null
193
     * @return true if the IRI is a role tier
194
     */
195
    public static boolean isTier(IRI iri) {
196
        return KPXL_TERMS.EVERYONE_ROLE.equals(iri)
21✔
197
                || KPXL_TERMS.ADMIN_ROLE_TYPE.equals(iri)
12✔
198
                || KPXL_TERMS.MAINTAINER_ROLE.equals(iri)
12✔
199
                || KPXL_TERMS.MEMBER_ROLE.equals(iri)
12✔
200
                || KPXL_TERMS.OBSERVER_ROLE.equals(iri);
15✔
201
    }
202

203
    /**
204
     * Evaluates a {@code gen:isVisibleTo} restriction (a set of role-tier and/or
205
     * specific-role IRIs) against a viewer. Used to gate per-action visibility on
206
     * views (see docs/role-specific-views.md).
207
     *
208
     * <p>An empty restriction is visible to everyone. A role-tier IRI matches when
209
     * the viewer's highest tier in the governing space meets or exceeds it
210
     * (admin {@literal >} maintainer {@literal >} member {@literal >} observer); a
211
     * specific role IRI matches when the viewer holds exactly that role. Multiple
212
     * entries are OR-ed; there is no admin override for specific roles. When there
213
     * is no governing space (e.g. a user page), a non-empty restriction is
214
     * satisfied only for the resource owner.</p>
215
     *
216
     * @param requiredVisibility the set of {@code gen:isVisibleTo} IRIs (may be empty)
217
     * @param viewer             the viewer's agent IRI, or null if logged out
218
     * @param governingSpace     the space whose roles govern visibility, or null
219
     * @param viewerIsOwner      whether the viewer owns the resource (used only
220
     *                           when there is no governing space)
221
     * @return true if the viewer is entitled
222
     */
223
    public static boolean isViewerEntitled(Set<IRI> requiredVisibility, IRI viewer, Space governingSpace, boolean viewerIsOwner) {
224
        if (requiredVisibility == null || requiredVisibility.isEmpty()) return true;
21✔
225
        // gen:EveryoneRole is the explicit "no restriction" value (the default the
226
        // view-creation template emits since it cannot leave the statement optional);
227
        // it is visible to everyone, including anonymous viewers, so short-circuit
228
        // before the null-viewer / null-space guards below.
229
        if (requiredVisibility.contains(KPXL_TERMS.EVERYONE_ROLE)) return true;
18✔
230
        if (governingSpace == null) {
6✔
231
            // A user page is a degenerate space: the owner is its sole admin and no
232
            // other members or role assignments exist (observers may be added
233
            // later). So the owner holds the admin tier and everyone else the
234
            // everyone floor; only tier requirements can match here, and specific
235
            // role IRIs — unholdable without a space — never do.
236
            int tier = viewerIsOwner ? ADMIN_RANK : EVERYONE_RANK;
18✔
237
            for (IRI req : requiredVisibility) {
30✔
238
                if (isTier(req) && tier >= tierRank(req)) return true;
27✔
239
            }
3✔
240
            return false;
6✔
241
        }
242
        if (viewer == null) return false;
12✔
243
        for (IRI req : requiredVisibility) {
30✔
244
            if (isTier(req)) {
9✔
245
                if (governingSpace.userTier(viewer) >= tierRank(req)) return true;
24✔
246
            } else if (governingSpace.viewerHoldsRole(viewer, req)) {
15✔
247
                return true;
6✔
248
            }
249
        }
3✔
250
        return false;
6✔
251
    }
252

253
    /**
254
     * Convenience overload that resolves the current viewer, the governing space,
255
     * and ownership from a resource-with-profile, then evaluates the
256
     * {@code gen:isVisibleTo} restriction. The governing space is the resource
257
     * itself if it is a space, otherwise its owning space (null for a user page).
258
     *
259
     * @param requiredVisibility the set of {@code gen:isVisibleTo} IRIs (may be empty)
260
     * @param resource           the resource the action is being rendered for, or null
261
     * @return true if the current viewer is entitled
262
     */
263
    public static boolean isViewerEntitled(Set<IRI> requiredVisibility, AbstractResourceWithProfile resource) {
264
        if (requiredVisibility == null || requiredVisibility.isEmpty()) return true;
×
265
        Space governingSpace = (resource instanceof Space s) ? s : (resource != null ? resource.getSpace() : null);
×
266
        IRI viewer = NanodashSession.getCurrentUserIriOrNull();
×
267
        boolean viewerIsOwner = viewer != null && resource instanceof IndividualAgent ia && ia.isCurrentUser();
×
268
        return isViewerEntitled(requiredVisibility, viewer, governingSpace, viewerIsOwner);
×
269
    }
270

271
    /**
272
     * Add the role parameters to the given multimap.
273
     *
274
     * @param params The multimap to add the parameters to.
275
     */
276
    public void addRoleParams(Multimap<String, String> params) {
277
        for (IRI p : regularProperties) params.put("role", p.stringValue());
69✔
278
        for (IRI p : inverseProperties) params.put("invrole", p.stringValue());
69✔
279
    }
3✔
280

281

282
    private static final IRI ADMIN_ROLE_IRI = Utils.vf.createIRI("https://w3id.org/np/RA_eEJjQbxzSqYSwPzfjzOZi5sMPpUmHskFNsgJYSws8I/adminRole");
12✔
283
    private static final String ADMIN_ROLE_ASSIGNMENT_TEMPLATE_ID = "https://w3id.org/np/RAsOQ7k3GNnuUqZuLm57PWwWopQJR_4onnCpNR457CZg8";
284

285
    /**
286
     * The predefined admin role.
287
     */
288
    public static final SpaceMemberRole ADMIN_ROLE = new SpaceMemberRole(ADMIN_ROLE_IRI, "Admin role", "admin", "Admins", TemplateData.get().getTemplate(ADMIN_ROLE_ASSIGNMENT_TEMPLATE_ID), new IRI[]{}, new IRI[]{KPXL_TERMS.HAS_ADMIN_PREDICATE}, KPXL_TERMS.ADMIN_ROLE_TYPE);
63✔
289

290
    /**
291
     * Convert a space-separated string of IRIs to an array of IRI objects.
292
     *
293
     * @param string The space-separated string of IRIs.
294
     * @return An array of IRI objects.
295
     */
296
    private static IRI[] stringToIriArray(String string) {
297
        if (string == null || string.isBlank()) return new IRI[]{};
24!
298
        return Stream.of(string.split(" ")).map(Utils.vf::createIRI).toArray(IRI[]::new);
51✔
299
    }
300

301
    /**
302
     * Check if the current user is a member of the given space.
303
     *
304
     * @param space The space to check.
305
     * @return True if the current user is a member of the space, false otherwise.
306
     */
307
    public static boolean isCurrentUserMember(Space space) {
308
        if (space == null) return false;
×
309
        IRI userIri = NanodashSession.get().getUserIri();
×
310
        if (userIri == null) return false;
×
311
        return space.isMember(userIri);
×
312
    }
313

314
    /**
315
     * Check if the current user is an admin of the given space.
316
     *
317
     * @param space The space to check.
318
     * @return True if the current user is an admin of the space, false otherwise.
319
     */
320
    public static boolean isCurrentUserAdmin(Space space) {
321
        if (space == null) return false;
×
322
        IRI userIri = NanodashSession.get().getUserIri();
×
323
        if (userIri == null) return false;
×
324
        if (space.getMemberRoles(userIri) == null) return false;
×
325
        for (SpaceMemberRoleRef spaceMemberRoleRef : space.getMemberRoles(userIri)) {
×
326
            if (spaceMemberRoleRef.getRole().isAdminRole()) return true;
×
327
        }
×
328
        return false;
×
329
    }
330

331
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc