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

knowledgepixels / nanodash / 39389438900

20 Oct 2026 07:52AM UTC coverage: 54.03% (-0.06%) from 54.087%
39389438900

push

github

web-flow
Merge pull request #803 from knowledgepixels/perf/slow-tests

build(test): run the tests in four JVMs

6185 of 12294 branches covered (50.31%)

Branch coverage included in aggregate %.

10910 of 19346 relevant lines covered (56.39%)

9.28 hits per line

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

36.18
src/main/java/com/knowledgepixels/nanodash/domain/UserData.java
1
package com.knowledgepixels.nanodash.domain;
2

3
import com.knowledgepixels.nanodash.*;
4
import org.eclipse.rdf4j.common.exception.RDF4JException;
5
import org.eclipse.rdf4j.model.IRI;
6
import org.eclipse.rdf4j.model.ValueFactory;
7
import org.eclipse.rdf4j.model.impl.SimpleValueFactory;
8
import org.eclipse.rdf4j.model.util.Values;
9
import org.nanopub.MalformedNanopubException;
10
import org.nanopub.Nanopub;
11
import org.nanopub.SimpleTimestampPattern;
12
import org.nanopub.extra.security.MalformedCryptoElementException;
13
import org.nanopub.extra.security.NanopubSignatureElement;
14
import org.nanopub.extra.security.SignatureUtils;
15
import org.nanopub.extra.server.GetNanopub;
16
import org.nanopub.extra.services.ApiResponse;
17
import org.nanopub.extra.services.ApiResponseEntry;
18
import org.nanopub.extra.services.QueryRef;
19
import org.nanopub.extra.setting.IntroNanopub;
20
import org.nanopub.extra.setting.NanopubSetting;
21
import org.nanopub.vocabulary.NPX;
22
import org.slf4j.Logger;
23
import org.slf4j.LoggerFactory;
24

25
import java.io.IOException;
26
import java.io.Serializable;
27
import java.util.*;
28

29
/**
30
 * UserData class manages user-related data.
31
 */
32
public class UserData implements Serializable {
33

34
    private static ValueFactory vf = SimpleValueFactory.getInstance();
6 ✔
35
    private static final Logger logger = LoggerFactory.getLogger(UserData.class);
12 ✔
36

37
    private HashMap<IRI, Set<String>> approvedIdPubkeyhashMap = new HashMap<>();
15 ✔
38
    private HashMap<String, Set<IRI>> approvedPubkeyhashIdMap = new HashMap<>();
15 ✔
39
    private HashMap<String, Set<IRI>> approvedPubkeyhashLocationMap = new HashMap<>();
15 ✔
40
    private HashMap<IRI, Set<String>> unapprovedIdPubkeyhashMap = new HashMap<>();
15 ✔
41
    private HashMap<String, Set<IRI>> unapprovedPubkeyhashIdMap = new HashMap<>();
15 ✔
42
    private HashMap<String, Set<IRI>> unapprovedPubkeyhashLocationMap = new HashMap<>();
15 ✔
43
    private HashMap<IRI, Boolean> softwareIdMap = new HashMap<>();
15 ✔
44
    private HashMap<String, Set<IRI>> pubkeyhashIntroMap = new HashMap<>();
15 ✔
45
    private HashMap<IRI, IntroNanopub> introMap = new HashMap<>();
15 ✔
46
    private Set<IRI> approvedIntros = new HashSet<>();
15 ✔
47
    private HashMap<IRI, String> idNameMap = new HashMap<>();
15 ✔
48
    private HashMap<IRI, List<IntroNanopub>> introNanopubLists = new HashMap<>();
15 ✔
49
    private final HashMap<IRI, ProfilePicture> profilePictures = new HashMap<>();
15 ✔
50
    private final HashMap<IRI, IRI> defaultLicense = new HashMap<>();
15 ✔
51

52
    private boolean complete = true;
9 ✔
53

54
    /**
55
     * Constructor for UserData.
56
     * Initializes the user data by fetching nanopublications settings.
57
     *
58
     * @param forced whether the user-detail queries must be re-fetched from the API even
59
     *               when a cached response exists. The periodic refresh passes true so each
60
     *               cycle actually brings the data current; the initial load passes false so
61
     *               it can build from cached responses right away — in particular from the
62
     *               persisted snapshot after a restart (issue #570), where forced fetches
63
     *               would stall the first request on the network.
64
     */
65
    UserData(boolean forced) {
6 ✔
66
        final NanodashPreferences pref = NanodashPreferences.get();
6 ✔
67

68
        // TODO Make nanopublication setting configurable:
69
        NanopubSetting setting = null;
6 ✔
70
        if (pref.getSettingUri() != null) {
9 !
71
            try {
72
                setting = new NanopubSetting(GetNanopub.get(pref.getSettingUri(), Utils.getRegistryHttpClient()));
×
73
            } catch (Exception ex) {
×
74
                // Retrieving it goes to the registry, which a cold instance may not be able to
75
                // reach (issue #684); the local setting stands in until it answers.
76
                logger.warn("Could not retrieve the configured nanopublication setting {}: {}", pref.getSettingUri(), ex.toString());
×
77
                complete = false;
×
78
            }
×
79
        }
80
        if (setting == null) {
6 !
81
            // No fallback beyond this one, and its failure is a misconfiguration rather than a
82
            // service that cannot answer, so it stays fatal.
83
            try {
84
                setting = NanopubSetting.getLocalSetting();
6 ✔
85
            } catch (RDF4JException | MalformedNanopubException | IOException ex) {
×
86
                throw new RuntimeException(ex);
×
87
            }
3 ✔
88
        }
89
        String settingId = setting.getNanopub().getUri().stringValue();
15 ✔
90
        try {
91
            if (setting.getUpdateStrategy().equals(NPX.UPDATES_BY_CREATOR)) {
15 ✔
92
                settingId = QueryApiAccess.getLatestVersionId(settingId);
9 ✔
93
                setting = new NanopubSetting(GetNanopub.get(settingId, Utils.getRegistryHttpClient()));
21 ✔
94
            }
95
            logger.info("Using nanopublication setting: {}", settingId);
12 ✔
96
        } catch (Exception ex) {
×
97
            // Resolving the latest version goes to the query service and the registry, so it
98
            // fails on a cold instance whose services are unavailable. The setting itself is
99
            // not read below, so the local one carries us until the services answer.
100
            logger.warn("Could not resolve the latest version of the nanopublication setting {}: {}", settingId, ex.toString());
×
101
            complete = false;
×
102
        }
3 ✔
103

104
//                // Get users that are listed directly in the authority index, and consider them approved:
105
//                ByteArrayOutputStream out = new ByteArrayOutputStream(); // TODO use piped out-in stream here
106
//                new FetchIndex(setting.getAgentIntroCollection().stringValue(), out, RDFFormat.TRIG, false, true, null).run();
107
//                InputStream in = new ByteArrayInputStream(out.toByteArray());
108
//                try {
109
//                        MultiNanopubRdfHandler.process(RDFFormat.TRIG, in, new MultiNanopubRdfHandler.NanopubHandler() {
110
//                                @Override
111
//                                public void handleNanopub(Nanopub np) {
112
//                                        // TODO: Check that latest version talks about same user
113
//                                        register(QueryApiAccess.getLatestVersionId(np.getUri().stringValue()), true);
114
//                                }
115
//                        });
116
//                } catch (RDFParseException | RDFHandlerException | IOException | MalformedNanopubException ex) {
117
//                        logger.error();
118
//                }
119
///
120
//                if (setting.getTrustRangeAlgorithm().equals(NPX.TRANSITIVE_TRUST)) {
121
//                        ApiResponse resp = QueryApiAccess.forcedGet("get-approved-nanopubs");
122
//                        List<ApiResponseEntry> results = new ArrayList<>(resp.getData());
123
//                        while (true) {
124
//                                boolean keepLooping = false;
125
//                                for (ApiResponseEntry entry : new ArrayList<>(results)) {
126
//                                        if (hasValue(approvedPubkeyIdMap, entry.get("pubkey"), Utils.vf.createIRI(entry.get("approver")))) {
127
//                                                register(entry.get("approved_np"), true);
128
//                                                results.remove(entry);
129
//                                                keepLooping = true;
130
//                                        }
131
//                                }
132
//                                if (!keepLooping) break;
133
//                        }
134
//                }
135

136
        logger.info("Loading approved users...");
9 ✔
137
        try {
138
            for (RegistryAccountInfo rai : RegistryAccountInfo.fromUrl(Utils.getMainRegistryUrl() + "list.json")) {
36 ✔
139
                registerApproved(rai);
9 ✔
140
            }
3 ✔
141
        } catch (Exception ex) {
3 ✔
142
            logger.error("Could not load the approved users from the registry: {}", ex.toString());
15 ✔
143
            complete = false;
9 ✔
144
        }
3 ✔
145

146
        logger.info("Loading user details...");
9 ✔
147
        // Get latest introductions for all users, including unapproved ones:
148
        for (ApiResponseEntry entry : entriesOf(QueryApiAccess.GET_ALL_USER_INTROS, forced)) {
27 !
149
            register(entry);
×
150
        }
×
151

152
        for (ApiResponseEntry entry : entriesOf(QueryApiAccess.GET_ALL_USER_PROFILE_PICS, forced)) {
39 ✔
153
            // The value can be a link or SVG markup (issue #634), and nothing stops a user
154
            // from declaring something unusable as either — hence the null check rather
155
            // than a bare parse, which would abort the whole user-data load.
156
            ProfilePicture picture = ProfilePicture.of(entry.get("imageUrl"));
15 ✔
157
            if (picture != null) profilePictures.put(Values.iri(entry.get("user")), picture);
33 !
158
        }
3 ✔
159

160
        for (ApiResponseEntry entry : entriesOf(QueryApiAccess.GET_ALL_USER_DEFAULT_LICENSE, forced)) {
39 ✔
161
            defaultLicense.put(Values.iri(entry.get("user")), Values.iri(entry.get("license")));
36 ✔
162
        }
3 ✔
163
    }
3 ✔
164

165
    /**
166
     * The entries of one of the user-detail queries, or none when the query service cannot
167
     * answer, in which case the data is marked as {@link #isComplete() not complete}.
168
     * <p>
169
     * Nothing here is worth failing the whole load for. A cold instance whose query service is
170
     * unavailable could not build user data at all, and since session construction needs it,
171
     * that left the instance unable to serve any page, its own error page included (issue
172
     * #684). What it can build is partial data — whoever the registry says is approved, and
173
     * no introductions — which is a serviceable state to render from and is replaced as soon
174
     * as the service answers.
175
     */
176
    private List<ApiResponseEntry> entriesOf(String queryId, boolean forced) {
177
        try {
178
            ApiResponse response = ApiCache.retrieveResponseSync(new QueryRef(queryId), forced);
21 ✔
179
            if (response != null) return response.getData();
15 ✔
180
            logger.warn("No response yet for {}; user data stays incomplete", queryId);
12 ✔
181
        } catch (Exception ex) {
3 ✔
182
            logger.error("Could not load {}: {}", queryId, ex.toString());
18 ✔
183
        }
3 ✔
184
        complete = false;
9 ✔
185
        return Collections.emptyList();
6 ✔
186
    }
187

188
    /**
189
     * Whether this user data could be built in full. It cannot when a service it is built from
190
     * is unavailable (issue #684): what is here is then only as much as the services that did
191
     * answer could tell, and the data is meant to be replaced by a complete load rather than
192
     * kept for the usual refresh interval.
193
     *
194
     * @return true if every source answered
195
     */
196
    public boolean isComplete() {
197
        return complete;
9 ✔
198
    }
199

200
    private IntroNanopub toIntroNanopub(IRI iri) {
201
        if (iri == null) return null;
×
202
        if (introMap.containsKey(iri)) return introMap.get(iri);
×
203
        Nanopub np = Utils.getNanopub(iri.stringValue());
×
204
        if (np == null) return null;
×
205
        IntroNanopub introNp = new IntroNanopub(np);
×
206
        introMap.put(np.getUri(), introNp);
×
207
        return introNp;
×
208
    }
209

210
    private void registerApproved(RegistryAccountInfo rai) {
211
        if (rai.getAgent().equals("$")) return;
18 ✔
212
        addValue(approvedIdPubkeyhashMap, rai.getAgentIri(), rai.getPubkey());
24 ✔
213
        addValue(approvedPubkeyhashIdMap, rai.getPubkey(), rai.getAgentIri());
24 ✔
214
    }
3 ✔
215

216
    private void register(ApiResponseEntry entry) {
217
        IRI userIri;
218
        try {
219
            userIri = vf.createIRI(entry.get("user"));
×
220
        } catch (IllegalArgumentException ex) {
×
221
            logger.error("Error creating IRI from user string: {}", entry.get("user"), ex);
×
222
            return;
×
223
        }
×
224
        String pubkeyhash = entry.get("pubkeyHash");
×
225
        boolean approved = approvedIdPubkeyhashMap.containsKey(userIri) && approvedIdPubkeyhashMap.get(userIri).contains(pubkeyhash);
×
226
        boolean authoritative = "true".equals(entry.get("authoritative"));
×
227
        IRI introNpIri = null;
×
228
        try {
229
            introNpIri = vf.createIRI(entry.get("intronp"));
×
230
        } catch (IllegalArgumentException ex) {
×
231
            logger.error("Error creating IRI from intronp string: {}", entry.get("intronp"), ex);
×
232
        }
×
233
        IRI keyLocation = null;
×
234
        try {
235
            if (!entry.get("keyLocation").isEmpty()) {
×
236
                keyLocation = vf.createIRI(entry.get("keyLocation"));
×
237
            }
238
        } catch (IllegalArgumentException ex) {
×
239
            logger.error("Error creating IRI from keyLocation string: {}", entry.get("keyLocation"), ex);
×
240
        }
×
241
        if (approved) {
×
242
            if (authoritative) {
×
243
                if (introNpIri != null) approvedIntros.add(introNpIri);
×
244
                if (keyLocation != null) addValue(approvedPubkeyhashLocationMap, pubkeyhash, keyLocation);
×
245
            }
246
        } else {
247
            addValue(unapprovedIdPubkeyhashMap, userIri, entry.get("pubkeyHash"));
×
248
            addValue(unapprovedPubkeyhashIdMap, entry.get("pubkeyHash"), userIri);
×
249
            if (keyLocation != null) addValue(unapprovedPubkeyhashLocationMap, pubkeyhash, keyLocation);
×
250
        }
251
        if (introNpIri != null) {
×
252
            addValue(pubkeyhashIntroMap, entry.get("pubkeyHash"), introNpIri);
×
253
        }
254
        String name = entry.get("name");
×
255
        if (!name.isEmpty() && !idNameMap.containsKey(userIri)) {
×
256
            idNameMap.put(userIri, name);
×
257
        }
258
        if ("true".equals(entry.get("isSoftware"))) {
×
259
            softwareIdMap.put(userIri, true);
×
260
        }
261

262

263
    }
×
264

265
/*
266
    private void register(String npId, boolean approved) {
267
        if (!TrustyUriUtils.isPotentialTrustyUri(npId)) return;
268
        IntroNanopub introNp = toIntroNanopub(npId);
269
        if (introNp == null) {
270
            //logger.error("No latest version of introduction found");
271
            return;
272
        }
273
        if (introNp.getUser() == null) {
274
            //logger.error("No identifier found in introduction");
275
            return;
276
        }
277
        if (introNp.getKeyDeclarations().isEmpty()) {
278
            //logger.error("No key declarations found in introduction");
279
            return;
280
        }
281
        if (approved) {
282
            approvedIntroMap.put(introNp.getNanopub().getUri(), introNp);
283
        }
284
        String userId = introNp.getUser().stringValue();
285
        IRI userIri = Utils.vf.createIRI(userId);
286
        if (userId.startsWith("https://orcid.org/")) {
287
            // Some simple ORCID ID wellformedness check:
288
            if (!userId.matches("https://orcid.org/[0-9]{4}-[0-9]{4}-[0-9]{4}-[0-9]{3}[0-9X]")) return;
289
        }
290
        for (KeyDeclaration kd : introNp.getKeyDeclarations()) {
291
            String pubkey = kd.getPublicKeyString();
292
            IRI keyLocation = kd.getKeyLocation();
293
            if (approved) {
294
                addValue(approvedIdPubkeyMap, userIri, pubkey);
295
                addValue(approvedPubkeyIdMap, pubkey, userIri);
296
                if (keyLocation != null) addValue(approvedPubkeyLocationMap, pubkey, keyLocation);
297
            } else {
298
                if (!hasValue(approvedIdPubkeyMap, userIri, pubkey)) {
299
                    addValue(unapprovedIdPubkeyMap, userIri, pubkey);
300
                    addValue(unapprovedPubkeyIdMap, pubkey, userIri);
301
                    if (keyLocation != null) addValue(unapprovedPubkeyLocationMap, pubkey, keyLocation);
302
                }
303
                addValue(pubkeyIntroMap, pubkey, introNp.getNanopub().getUri());
304
            }
305
        }
306
        if (!idNameMap.containsKey(userIri)) {
307
            idNameMap.put(userIri, introNp.getName());
308
        }
309
    }
310
    */
311

312
    private void addValue(Map<IRI, Set<String>> map, IRI key, String value) {
313
        Set<String> values = map.get(key);
15 ✔
314
        if (values == null) {
6 ✔
315
            values = new HashSet<>();
12 ✔
316
            map.put(key, values);
15 ✔
317
        }
318
        values.add(value);
12 ✔
319
    }
3 ✔
320

321
    private void addValue(Map<String, Set<IRI>> map, String key, IRI value) {
322
        Set<IRI> values = map.get(key);
15 ✔
323
        if (values == null) {
6 ✔
324
            values = new HashSet<>();
12 ✔
325
            map.put(key, values);
15 ✔
326
        }
327
        values.add(value);
12 ✔
328
    }
3 ✔
329

330
    private boolean hasValue(Map<IRI, Set<String>> map, IRI key, String value) {
331
        Set<String> values = map.get(key);
×
332
        if (values == null) return false;
×
333
        return values.contains(value);
×
334
    }
335

336
//        private static boolean hasValue(Map<String,Set<IRI>> map, String key, IRI value) {
337
//                Set<IRI> values = map.get(key);
338
//                if (values == null) return false;
339
//                return values.contains(value);
340
//        }
341

342
    /**
343
     * Checks if the given IRI is a user identifier.
344
     *
345
     * @param userIri the IRI to check
346
     * @return true if the IRI is a user identifier, false otherwise
347
     */
348
    public boolean isUser(IRI userIri) {
349
        return approvedIdPubkeyhashMap.containsKey(userIri) || unapprovedIdPubkeyhashMap.containsKey(userIri);
42 !
350
    }
351

352
    /**
353
     * Checks if the given IRI is a software identifier.
354
     *
355
     * @param userIri the IRI to check
356
     * @return true if the IRI is a software identifier, false otherwise
357
     */
358
    public boolean isSoftware(IRI userIri) {
359
        return softwareIdMap.containsKey(userIri) && softwareIdMap.get(userIri);
×
360
    }
361

362
    /**
363
     * Checks if the given userId is a valid user identifier.
364
     *
365
     * @param userId the user identifier to check, must start with "https://" or "http://"
366
     * @return true if the userId is a valid user identifier, false otherwise
367
     */
368
    public boolean isUser(String userId) {
369
        if (!userId.startsWith("https://") && !userId.startsWith("http://")) {
24 ✔
370
            return false;
6 ✔
371
        }
372
        try {
373
            IRI userIri = Utils.vf.createIRI(userId);
12 ✔
374
            return isUser(userIri);
12 ✔
375
        } catch (IllegalArgumentException ex) {
×
376
            return false;
×
377
        }
378
    }
379

380
    /**
381
     * Checks if the given public key is approved for the specified user.
382
     *
383
     * @param pubkeyHash the public key to check
384
     * @param user       the IRI of the user to check against
385
     * @return true if the key is approved for the user, false otherwise
386
     */
387
    public boolean isApprovedPubkeyHashForUser(String pubkeyHash, IRI user) {
388
        return hasValue(approvedIdPubkeyhashMap, user, pubkeyHash);
×
389
    }
390

391
    private String getShortName(IRI userIri) {
392
        if (userIri == null) return "(unknown)";
6 !
393
        String n = userIri.stringValue();
9 ✔
394
        n = n.replaceFirst("^https://orcid.org/", "");
15 ✔
395
        if (n.length() > 40) return n.substring(0, 30) + "...";
12 !
396
        return n;
6 ✔
397
    }
398

399
    /**
400
     * Retrieves the IRI of a user based on their public key.
401
     *
402
     * @param pubkeyHash   the public key of the user
403
     * @param approvedOnly if true, only approved users are considered; if false, unapproved users are also considered
404
     * @return the IRI of the user if found, or null if not found
405
     */
406
    public IRI getUserIriForPubkeyHash(String pubkeyHash, boolean approvedOnly) {
407
        Set<IRI> userIris = approvedPubkeyhashIdMap.get(pubkeyHash);
×
408
        if (userIris != null && userIris.size() == 1) return userIris.iterator().next();
×
409
        if (!approvedOnly) {
×
410
            userIris = unapprovedPubkeyhashIdMap.get(pubkeyHash);
×
411
            if (userIris != null && userIris.size() == 1) return userIris.iterator().next();
×
412
        }
413
        return null;
×
414
    }
415

416
    /**
417
     * Retrieves the IRI of the owner of a nanopublication's signature.
418
     *
419
     * @param np the nanopublication
420
     * @return the IRI of the signature owner if found, or null if not found or if an error occurs
421
     */
422
    public IRI getSignatureOwnerIri(Nanopub np) {
423
        try {
424
            if (np != null) {
6 !
425
                NanopubSignatureElement se = SignatureUtils.getSignatureElement(np);
9 ✔
426
                if (se != null) {
6 !
427
                    String pubkeyHash = Utils.createSha256HexHash(se.getPublicKeyString());
×
428
                    return getUserIriForPubkeyHash(pubkeyHash, true);
×
429
                }
430
            }
431
        } catch (MalformedCryptoElementException ex) {
×
432
            logger.error("Error getting signature element", ex);
×
433
        }
3 ✔
434
        return null;
6 ✔
435
    }
436

437
    /**
438
     * Retrieves the name of a user based on their IRI.
439
     *
440
     * @param userIri the IRI of the user
441
     * @return the name of the user if found, or null if not found
442
     */
443
    public String getName(IRI userIri) {
444
        return idNameMap.get(userIri);
18 ✔
445
    }
446

447
    /**
448
     * Retrieves the display name of a user based on their IRI.
449
     *
450
     * @param userIri the IRI of the user
451
     * @return the display name of the user, which includes their name and short name
452
     */
453
    public String getDisplayName(IRI userIri) {
454
        String name = getName(userIri);
×
455
        if (name != null && !name.isEmpty()) {
×
456
            return name + " (" + getShortName(userIri) + ")";
×
457
        }
458
        return getShortName(userIri);
×
459
    }
460

461
    /**
462
     * Retrieves a short display name for a user based on their IRI.
463
     *
464
     * @param userIri the IRI of the user
465
     * @return the short display name of the user, which is either their name or their short name
466
     */
467
    public String getShortDisplayName(IRI userIri) {
468
        String name = getName(userIri);
12 ✔
469
        if (name != null && !name.isEmpty()) {
6 !
470
            return name;
×
471
        }
472
        return getShortName(userIri);
12 ✔
473
    }
474

475
    /**
476
     * Retrieves a short display name for a user based on their IRI and public key.
477
     *
478
     * @param userIri    the IRI of the user
479
     * @param pubkeyHash the public key of the user
480
     * @return the short display name of the user, which may include a contested identity note if multiple identities are associated with the public key
481
     */
482
    public String getShortDisplayNameForPubkeyHash(IRI userIri, String pubkeyHash) {
483
        Set<IRI> ids = approvedPubkeyhashIdMap.get(pubkeyHash);
18 ✔
484
        if (ids == null || ids.isEmpty()) {
6 !
485
            ids = unapprovedPubkeyhashIdMap.get(pubkeyHash);
18 ✔
486
            if (ids == null || ids.isEmpty()) {
6 !
487
                return getShortDisplayName(userIri);
12 ✔
488
            } else if (ids.size() == 1) {
×
489
                return getShortDisplayName(ids.iterator().next());
×
490
            } else {
491
                // Not showing "contested identity" for now.
492
                return getShortDisplayName(userIri);  // + " (contested identity)";
×
493
            }
494
        } else if (ids.size() == 1) {
×
495
            return getShortDisplayName(ids.iterator().next());
×
496
        } else {
497
            // Not showing "contested identity" for now.
498
            //return "(contested identity)";
499
            return getShortDisplayName(ids.iterator().next());
×
500
        }
501
    }
502

503
    /**
504
     * Finds a single user ID for a given public key.
505
     *
506
     * @param pubkeyHash the public key to search for
507
     * @return the IRI of the user if exactly one ID is found for the public key, or null if no ID or multiple IDs are found
508
     */
509
    public IRI findSingleIdForPubkeyHash(String pubkeyHash) {
510
        if (approvedPubkeyhashIdMap.containsKey(pubkeyHash) && !approvedPubkeyhashIdMap.get(pubkeyHash).isEmpty()) {
15 !
511
            if (approvedPubkeyhashIdMap.get(pubkeyHash).size() == 1) {
×
512
                return approvedPubkeyhashIdMap.get(pubkeyHash).iterator().next();
×
513
            } else {
514
                return null;
×
515
            }
516
        }
517
        if (unapprovedPubkeyhashIdMap.containsKey(pubkeyHash) && !unapprovedPubkeyhashIdMap.get(pubkeyHash).isEmpty()) {
15 !
518
            if (unapprovedPubkeyhashIdMap.get(pubkeyHash).size() == 1) {
×
519
                return unapprovedPubkeyhashIdMap.get(pubkeyHash).iterator().next();
×
520
            } else {
521
                return null;
×
522
            }
523
        }
524
        return null;
6 ✔
525
    }
526

527
    /**
528
     * Comparator for sorting users by their display names in a case-insensitive manner.
529
     */
530
    public final transient Comparator<IRI> userComparator = (iri1, iri2) -> {
12 ✔
531
        String name1 = getName(iri1);
×
532
        String name2 = getName(iri2);
×
533
        boolean hasName1 = name1 != null && !name1.isEmpty();
×
534
        boolean hasName2 = name2 != null && !name2.isEmpty();
×
535
        if (hasName1 != hasName2) {
×
536
            return hasName1 ? -1 : 1;
×
537
        }
538
        return getDisplayName(iri1).toLowerCase().compareTo(getDisplayName(iri2).toLowerCase());
×
539
    };
540

541
    /**
542
     * Retrieves a list of users, either approved or unapproved.
543
     *
544
     * @param approved if true, retrieves approved users; if false, retrieves unapproved users
545
     * @return a sorted list of user IRIs
546
     */
547
    public List<IRI> getUsers(boolean approved) {
548
        List<IRI> list;
549
        if (approved) {
×
550
            list = new ArrayList<IRI>(approvedIdPubkeyhashMap.keySet());
×
551
        } else {
552
            list = new ArrayList<IRI>();
×
553
            for (IRI u : unapprovedIdPubkeyhashMap.keySet()) {
×
554
                if (!approvedIdPubkeyhashMap.containsKey(u)) list.add(u);
×
555
            }
×
556
        }
557
        // TODO Cache the sorted list to not sort from scratch each time:
558
        list.sort(userComparator);
×
559
        return list;
×
560
    }
561

562
    /**
563
     * Retrieves a list of public keys for a given user.
564
     *
565
     * @param user     the IRI of the user whose public keys are to be retrieved
566
     * @param approved if true, retrieves approved public keys; if false, retrieves unapproved public keys
567
     * @return a list of public keys associated with the user, either approved or unapproved
568
     */
569
    public List<String> getPubkeyHashes(IRI user, Boolean approved) {
570
        List<String> pubkeys = new ArrayList<>();
×
571
        if (user != null) {
×
572
            if (approved == null || approved) {
×
573
                if (approvedIdPubkeyhashMap.containsKey(user)) pubkeys.addAll(approvedIdPubkeyhashMap.get(user));
×
574
            }
575
            if (approved == null || !approved) {
×
576
                if (unapprovedIdPubkeyhashMap.containsKey(user)) pubkeys.addAll(unapprovedIdPubkeyhashMap.get(user));
×
577
            }
578
        }
579
        return pubkeys;
×
580
    }
581

582
    /**
583
     * Retrieves a list of introduction nanopublications for a given user.
584
     *
585
     * @param user the IRI of the user whose introduction nanopublications are to be retrieved
586
     * @return a list of introduction nanopublications associated with the user, sorted by creation time
587
     */
588
    public List<IntroNanopub> getIntroNanopubs(IRI user) {
589
        if (introNanopubLists.containsKey(user)) return introNanopubLists.get(user);
33 ✔
590

591
        Map<IRI, IntroNanopub> introNps = new HashMap<>();
12 ✔
592
        if (approvedIdPubkeyhashMap.containsKey(user)) {
15 !
593
            for (String pk : approvedIdPubkeyhashMap.get(user)) {
×
594
                getIntroNanopubs(pk, introNps);
×
595
            }
×
596
        }
597
        if (unapprovedIdPubkeyhashMap.containsKey(user)) {
15 !
598
            for (String pk : unapprovedIdPubkeyhashMap.get(user)) {
×
599
                getIntroNanopubs(pk, introNps);
×
600
            }
×
601
        }
602
        List<IntroNanopub> list = new ArrayList<>(introNps.values());
18 ✔
603
        list.sort((i0, i1) -> {
9 ✔
604
            Calendar c0 = SimpleTimestampPattern.getCreationTime(i0.getNanopub());
×
605
            Calendar c1 = SimpleTimestampPattern.getCreationTime(i1.getNanopub());
×
606
            if (c0 == null && c1 == null) return 0;
×
607
            if (c0 == null) return 1;
×
608
            if (c1 == null) return -1;
×
609
            return -c0.compareTo(c1);
×
610
        });
611
        introNanopubLists.put(user, list);
18 ✔
612
        return list;
6 ✔
613
    }
614

615
    /**
616
     * Retrieves a map of introduction nanopublications for a given public key.
617
     *
618
     * @param pubkey the public key for which introduction nanopublications are to be retrieved
619
     * @return a map where the keys are IRI identifiers of introduction nanopublications and the values are the corresponding IntroNanopub objects
620
     */
621
    public Map<IRI, IntroNanopub> getIntroNanopubs(String pubkey) {
622
        Map<IRI, IntroNanopub> introNps = new HashMap<>();
12 ✔
623
        getIntroNanopubs(pubkey, introNps);
12 ✔
624
        return introNps;
6 ✔
625
    }
626

627
    private void getIntroNanopubs(String pubkeyhash, Map<IRI, IntroNanopub> introNps) {
628
        if (pubkeyhashIntroMap.containsKey(pubkeyhash)) {
15 !
629
            for (IRI iri : pubkeyhashIntroMap.get(pubkeyhash)) {
×
630
                IntroNanopub introNp = toIntroNanopub(iri);
×
631
                if (introNp != null) {
×
632
                    introNps.put(iri, introNp);
×
633
                }
634
            }
×
635
        }
636
    }
3 ✔
637

638
    /**
639
     * Checks if the given introduction nanopublication is approved.
640
     *
641
     * @param in the introduction nanopublication to check
642
     * @return true if the introduction nanopublication is approved, false otherwise
643
     */
644
    public boolean isApproved(IntroNanopub in) {
645
        return approvedIntros.contains(in.getNanopub().getUri());
×
646
    }
647

648
    /**
649
     * Retrieves the location of a public key.
650
     *
651
     * @param pubkeyHash the public key for which the location is to be retrieved
652
     * @return the IRI of the key location if found, or null if not found or if multiple locations are associated with the key
653
     */
654
    public IRI getKeyLocationForPubkeyHash(String pubkeyHash) {
655
        if (approvedPubkeyhashLocationMap.containsKey(pubkeyHash) && !approvedPubkeyhashLocationMap.get(pubkeyHash).isEmpty()) {
×
656
            if (approvedPubkeyhashLocationMap.get(pubkeyHash).size() == 1)
×
657
                return approvedPubkeyhashLocationMap.get(pubkeyHash).iterator().next();
×
658
            return null;
×
659
        }
660
        if (unapprovedPubkeyhashLocationMap.containsKey(pubkeyHash) && unapprovedPubkeyhashLocationMap.get(pubkeyHash).size() == 1) {
×
661
            return unapprovedPubkeyhashLocationMap.get(pubkeyHash).iterator().next();
×
662
        }
663
        return null;
×
664
    }
665

666
    /**
667
     * Retrieves the profile picture IRI for a user based on their IRI. Users declare their
668
     * own picture, and the underlying query accepts any signer, so this is for user IRIs
669
     * only: spaces and maintained resources go through the admin-gated per-resource query
670
     * instead (issue #632, see
671
     * {@link com.knowledgepixels.nanodash.domain.AbstractResourceWithProfile#getProfilePicture()}).
672
     *
673
     * @param userIri the IRI of the user for whom to retrieve the profile picture
674
     * @return the IRI of the user's profile picture if found, or null if not found
675
     */
676
    public ProfilePicture getProfilePicture(IRI userIri) {
677
        return profilePictures.get(userIri);
18 ✔
678
    }
679

680
    /**
681
     * Retrieves the default license IRI for a user based on their IRI.
682
     *
683
     * @param userIri the IRI of the user for whom to retrieve the default license
684
     * @return the IRI of the user's default license if found, or null if not found
685
     */
686
    public IRI getDefaultLicense(IRI userIri) {
687
        return defaultLicense.get(userIri);
18 ✔
688
    }
689

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

© 2026 Coveralls, Inc