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

knowledgepixels / nanodash / 21766377938

06 Feb 2026 09:20PM UTC coverage: 14.086% (+0.2%) from 13.838%
21766377938

push

github

tkuhn
refactor: use nanopub URI as preview page id instead of random UUID

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

577 of 5266 branches covered (10.96%)

Branch coverage included in aggregate %.

1519 of 9614 relevant lines covered (15.8%)

2.07 hits per line

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

14.49
src/main/java/com/knowledgepixels/nanodash/NanodashSession.java
1
package com.knowledgepixels.nanodash;
2

3
import com.knowledgepixels.nanodash.component.NanopubResults;
4
import com.knowledgepixels.nanodash.component.PublishForm;
5
import com.knowledgepixels.nanodash.page.OrcidLoginPage;
6
import com.knowledgepixels.nanodash.page.ProfilePage;
7
import jakarta.servlet.http.HttpServletRequest;
8
import jakarta.servlet.http.HttpSession;
9
import jakarta.xml.bind.DatatypeConverter;
10
import org.apache.commons.io.FileUtils;
11
import org.apache.wicket.Session;
12
import org.apache.wicket.protocol.http.WebSession;
13
import org.apache.wicket.request.Request;
14
import org.apache.wicket.request.flow.RedirectToUrlException;
15
import org.apache.wicket.request.mapper.parameter.PageParameters;
16
import org.eclipse.rdf4j.model.IRI;
17
import org.eclipse.rdf4j.model.ValueFactory;
18
import org.eclipse.rdf4j.model.impl.SimpleValueFactory;
19
import org.nanopub.extra.security.*;
20
import org.nanopub.extra.setting.IntroNanopub;
21
import org.slf4j.Logger;
22
import org.slf4j.LoggerFactory;
23

24
import java.io.File;
25
import java.io.IOException;
26
import java.io.Serializable;
27
import java.nio.charset.StandardCharsets;
28
import java.security.KeyPair;
29
import java.util.Date;
30
import java.util.List;
31
import java.util.Map;
32
import java.util.concurrent.ConcurrentHashMap;
33
import java.util.concurrent.ConcurrentMap;
34

35
import org.apache.wicket.markup.html.WebPage;
36
import org.nanopub.Nanopub;
37

38
/**
39
 * Represents a session in the Nanodash application.
40
 */
41
public class NanodashSession extends WebSession {
42

43
    private transient HttpSession httpSession;
44
    private static final Logger logger = LoggerFactory.getLogger(NanodashSession.class);
9✔
45

46
    /**
47
     * Retrieves the current Nanodash session.
48
     *
49
     * @return The current NanodashSession instance.
50
     */
51
    public static NanodashSession get() {
52
        return (NanodashSession) Session.get();
×
53
    }
54

55
    /**
56
     * Constructs a new NanodashSession for the given request.
57
     * Initializes the HTTP session and loads profile information.
58
     *
59
     * @param request The HTTP request.
60
     */
61
    public NanodashSession(Request request) {
62
        super(request);
9✔
63
        httpSession = ((HttpServletRequest) request.getContainerRequest()).getSession();
18✔
64
        bind();
6✔
65
        loadProfileInfo();
6✔
66
    }
3✔
67

68
    private static ValueFactory vf = SimpleValueFactory.getInstance();
9✔
69

70
//        private IntroExtractor introExtractor;
71

72
    private String userDir = System.getProperty("user.home") + "/.nanopub/";
15✔
73
    private NanopubResults.ViewMode nanopubResultsViewMode = NanopubResults.ViewMode.LIST;
9✔
74

75
    private KeyPair keyPair;
76
    private IRI userIri;
77
    private ConcurrentMap<IRI, IntroNanopub> introNps;
78
//        private Boolean isOrcidLinked;
79
//        private String orcidLinkError;
80

81
    private Integer localIntroCount = null;
9✔
82
    private IntroNanopub localIntro = null;
9✔
83

84
    private Date lastTimeIntroPublished = null;
9✔
85

86
    // We should store here some sort of form model and not the forms themselves, but I couldn't figure
87
    // how to do it, so doing it like this for the moment...
88
    private ConcurrentMap<String, PublishForm> formMap = new ConcurrentHashMap<>();
15✔
89

90
    /**
91
     * Associates a form object with a specific ID.
92
     *
93
     * @param formObjId The ID of the form object.
94
     * @param formObj   The form object to associate.
95
     */
96
    public void setForm(String formObjId, PublishForm formObj) {
97
        formMap.put(formObjId, formObj);
×
98
    }
×
99

100
    /**
101
     * Checks if a form object with the given ID exists.
102
     *
103
     * @param formObjId The ID of the form object.
104
     * @return True if the form object exists, false otherwise.
105
     */
106
    public boolean hasForm(String formObjId) {
107
        return formMap.containsKey(formObjId);
×
108
    }
109

110
    /**
111
     * Retrieves the form object associated with the given ID.
112
     *
113
     * @param formObjId The ID of the form object.
114
     * @return The associated form object, or null if not found.
115
     */
116
    public PublishForm getForm(String formObjId) {
117
        return formMap.get(formObjId);
×
118
    }
119

120
    /**
121
     * Loads profile information for the user.
122
     * Initializes user-related data such as keys and introductions.
123
     */
124
    public void loadProfileInfo() {
125
        localIntroCount = null;
9✔
126
        localIntro = null;
9✔
127
        NanodashPreferences prefs = NanodashPreferences.get();
6✔
128
        if (prefs.isOrcidLoginMode()) {
9!
129
            File usersDir = new File(System.getProperty("user.home") + "/.nanopub/nanodash-users/");
×
130
            if (!usersDir.exists()) usersDir.mkdir();
×
131
        }
132
        if (userIri == null && !prefs.isReadOnlyMode() && !prefs.isOrcidLoginMode()) {
27!
133
            if (getOrcidFile().exists()) {
12!
134
                try {
135
                    String orcid = FileUtils.readFileToString(getOrcidFile(), StandardCharsets.UTF_8).trim();
×
136
                    //String orcid = Files.readString(orcidFile.toPath(), StandardCharsets.UTF_8).trim();
137
                    if (orcid.matches(ProfilePage.ORCID_PATTERN)) {
×
138
                        userIri = vf.createIRI("https://orcid.org/" + orcid);
×
139
                        if (httpSession != null) httpSession.setMaxInactiveInterval(24 * 60 * 60);  // 24h
×
140
                    }
141
                } catch (IOException ex) {
×
142
                    logger.error("Couldn't read ORCID file", ex);
×
143
                }
×
144
            }
145
        }
146
        if (userIri != null && keyPair == null) {
9!
147
            File keyFile = getKeyFile();
×
148
            if (keyFile.exists()) {
×
149
                try {
150
                    keyPair = SignNanopub.loadKey(keyFile.getPath(), SignatureAlgorithm.RSA);
×
151
                } catch (Exception ex) {
×
152
                    logger.error("Couldn't load key pair", ex);
×
153
                }
×
154
            } else {
155
                // Automatically generate new keys
156
                makeKeys();
×
157
            }
158
        }
159
        if (userIri != null && keyPair != null && introNps == null) {
9!
160
            introNps = new ConcurrentHashMap<>(User.getIntroNanopubs(getPubkeyString()));
×
161
        }
162
//                checkOrcidLink();
163
    }
3✔
164

165
    /**
166
     * Checks if the user's profile is complete.
167
     *
168
     * @return True if the profile is complete, false otherwise.
169
     */
170
    public boolean isProfileComplete() {
171
        return userIri != null && keyPair != null && introNps != null;
×
172
    }
173

174
    /**
175
     * Redirects the user to the login page if their profile is incomplete.
176
     *
177
     * @param path       The path to redirect to after login.
178
     * @param parameters The page parameters for the redirect.
179
     */
180
    public void redirectToLoginIfNeeded(String path, PageParameters parameters) {
181
        String loginUrl = getLoginUrl(path, parameters);
×
182
        if (loginUrl == null) return;
×
183
        throw new RedirectToUrlException(loginUrl);
×
184
    }
185

186
    /**
187
     * Retrieves the login URL for the user.
188
     *
189
     * @param path       The path to redirect to after login.
190
     * @param parameters The page parameters for the redirect.
191
     * @return The login URL, or null if the user is already logged in.
192
     */
193
    public String getLoginUrl(String path, PageParameters parameters) {
194
        if (isProfileComplete()) return null;
×
195
        if (NanodashPreferences.get().isOrcidLoginMode()) {
×
196
            return OrcidLoginPage.getOrcidLoginUrl(path, parameters);
×
197
        } else {
198
            return ProfilePage.MOUNT_PATH;
×
199
        }
200
    }
201

202
    /**
203
     * Retrieves the public key as a Base64-encoded string.
204
     *
205
     * @return The public key string, or null if the key pair is not set.
206
     */
207
    public String getPubkeyString() {
208
        if (keyPair == null) return null;
×
209
        return DatatypeConverter.printBase64Binary(keyPair.getPublic().getEncoded()).replaceAll("\\s", "");
×
210
    }
211

212
    /**
213
     * Retrieves the public key hash for the user.
214
     *
215
     * @return The SHA-256 hash of the public key, or null if the public key is not set.
216
     */
217
    public String getPubkeyhash() {
218
        String pubkey = getPubkeyString();
×
219
        if (pubkey == null) return null;
×
220
        return Utils.createSha256HexHash(pubkey);
×
221
    }
222

223
    /**
224
     * Checks if the user's public key is approved.
225
     *
226
     * @return True if the public key is approved, false otherwise.
227
     */
228
    public boolean isPubkeyApproved() {
229
        if (keyPair == null || userIri == null) return false;
×
230
        return User.isApprovedPubkeyhashForUser(getPubkeyhash(), userIri);
×
231
    }
232

233
    /**
234
     * Retrieves the user's key pair.
235
     *
236
     * @return The key pair.
237
     */
238
    public KeyPair getKeyPair() {
239
        return keyPair;
×
240
    }
241

242
    /**
243
     * Generates a new key pair for the user.
244
     */
245
    public void makeKeys() {
246
        try {
247
            MakeKeys.make(getKeyFile().getAbsolutePath().replaceFirst("_rsa$", ""), SignatureAlgorithm.RSA);
×
248
            keyPair = SignNanopub.loadKey(getKeyFile().getPath(), SignatureAlgorithm.RSA);
×
249
        } catch (Exception ex) {
×
250
            logger.error("Couldn't create key pair", ex);
×
251
        }
×
252
    }
×
253

254
    /**
255
     * Retrieves the user's IRI.
256
     *
257
     * @return The user's IRI, or null if not set.
258
     */
259
    public IRI getUserIri() {
260
        return userIri;
×
261
    }
262

263
    /**
264
     * Retrieves the user's introduction nanopublications.
265
     *
266
     * @return A list of user's introduction nanopublications.
267
     */
268
    public List<IntroNanopub> getUserIntroNanopubs() {
269
        return User.getIntroNanopubs(userIri);
×
270
    }
271

272
    /**
273
     * Counts the number of local introduction nanopublications.
274
     *
275
     * @return The count of local introduction nanopublications.
276
     */
277
    public int getLocalIntroCount() {
278
        if (localIntroCount == null) {
×
279
            localIntroCount = 0;
×
280
            for (IntroNanopub inp : getUserIntroNanopubs()) {
×
281
                if (isIntroWithLocalKey(inp)) {
×
282
                    localIntroCount++;
×
283
                    localIntro = inp;
×
284
                }
285
            }
×
286
            if (localIntroCount > 1) localIntro = null;
×
287
        }
288
        return localIntroCount;
×
289
    }
290

291
    /**
292
     * Retrieves the local introduction nanopublication.
293
     *
294
     * @return The local introduction nanopublication, or null if not found.
295
     */
296
    public IntroNanopub getLocalIntro() {
297
        getLocalIntroCount();
×
298
        return localIntro;
×
299
    }
300

301
    /**
302
     * Checks if the given introduction nanopublication is associated with the local key.
303
     *
304
     * @param inp The introduction nanopublication.
305
     * @return True if associated with the local key, false otherwise.
306
     */
307
    public boolean isIntroWithLocalKey(IntroNanopub inp) {
308
        IRI location = Utils.getLocation(inp);
×
309
        NanopubSignatureElement el = Utils.getNanopubSignatureElement(inp);
×
310
        String siteUrl = NanodashPreferences.get().getWebsiteUrl();
×
311
        if (location != null && siteUrl != null) {
×
312
            String l = location.stringValue();
×
313
            // TODO: Solve the name change recognition in a better way:
314
            if (!l.equals(siteUrl) && !l.replace("nanobench", "nanodash").equals(siteUrl)) return false;
×
315
        }
316
        if (!getPubkeyString().equals(el.getPublicKeyString())) return false;
×
317
        for (KeyDeclaration kd : inp.getKeyDeclarations()) {
×
318
            if (getPubkeyString().equals(kd.getPublicKeyString())) return true;
×
319
        }
×
320
        return false;
×
321
    }
322

323
    /**
324
     * Sets the user's ORCID identifier.
325
     *
326
     * @param orcid The ORCID identifier.
327
     */
328
    public void setOrcid(String orcid) {
329
        if (!orcid.matches(ProfilePage.ORCID_PATTERN)) {
×
330
            throw new RuntimeException("Illegal ORCID identifier: " + orcid);
×
331
        }
332
        if (NanodashPreferences.get().isOrcidLoginMode()) {
×
333
            userDir = System.getProperty("user.home") + "/.nanopub/nanodash-users/" + orcid + "/";
×
334
            File f = new File(userDir);
×
335
            if (!f.exists()) f.mkdir();
×
336
        } else {
×
337
            try {
338
                FileUtils.writeStringToFile(getOrcidFile(), orcid + "\n", StandardCharsets.UTF_8);
×
339
                //                        Files.writeString(orcidFile.toPath(), orcid + "\n");
340
            } catch (IOException ex) {
×
341
                logger.error("Couldn't write ORCID file", ex);
×
342
            }
×
343
        }
344
        userIri = vf.createIRI("https://orcid.org/" + orcid);
×
345
        loadProfileInfo();
×
346
        if (httpSession != null) httpSession.setMaxInactiveInterval(24 * 60 * 60);  // 24h
×
347
    }
×
348

349
    /**
350
     * Logs out the user and invalidates the session.
351
     */
352
    public void logout() {
353
        userIri = null;
×
354
        invalidateNow();
×
355
    }
×
356

357
    /**
358
     * Retrieves the user's introduction nanopublications as a map.
359
     *
360
     * @return A map of introduction nanopublications.
361
     */
362
    public Map<IRI, IntroNanopub> getIntroNanopubs() {
363
        return introNps;
×
364
    }
365

366
//        public void checkOrcidLink() {
367
//                if (isOrcidLinked == null && userIri != null) {
368
//                        orcidLinkError = "";
369
//                        introExtractor = null;
370
//                        try {
371
//                                introExtractor = IntroNanopub.extract(userIri.stringValue(), null);
372
//                                if (introExtractor.getIntroNanopub() == null) {
373
//                                        orcidLinkError = "ORCID account is not linked.";
374
//                                        isOrcidLinked = false;
375
//                                } else {
376
//                                        IntroNanopub inp = IntroNanopub.get(userIri.stringValue(), introExtractor);
377
//                                        if (introNps != null && introNps.containsKey(inp.getNanopub().getUri())) {
378
//                                                // TODO: also check whether introduction contains local key
379
//                                                isOrcidLinked = true;
380
//                                        } else {
381
//                                                isOrcidLinked = false;
382
//                                                orcidLinkError = "Error: ORCID is linked to another introduction nanopublication.";
383
//                                        }
384
//                                }
385
//                        } catch (Exception ex) {
386
//                                logger.error("ORCID check failed");
387
//                                orcidLinkError = "ORCID check failed.";
388
//                        }
389
//                }
390
//        }
391
//
392
//        public void resetOrcidLinked() {
393
//                isOrcidLinked = null;
394
//        }
395
//
396
//        public boolean isOrcidLinked() {
397
//                checkOrcidLink();
398
//                return isOrcidLinked != null && isOrcidLinked == true;
399
//        }
400
//
401
//        public String getOrcidLinkError() {
402
//                return orcidLinkError;
403
//        }
404
//
405
//        public String getOrcidName() {
406
//                if (introExtractor == null || introExtractor.getName() == null) return null;
407
//                if (introExtractor.getName().trim().isEmpty()) return null;
408
//                return introExtractor.getName();
409
//        }
410

411
    /**
412
     * Retrieves the file for storing the user's ORCID identifier.
413
     *
414
     * @return The ORCID file.
415
     */
416
    private File getOrcidFile() {
417
        return new File(userDir + "orcid");
21✔
418
    }
419

420
    /**
421
     * Retrieves the file for storing the user's private key.
422
     *
423
     * @return The key file.
424
     */
425
    public File getKeyFile() {
426
        return new File(userDir + "id_rsa");
×
427
    }
428

429
    /**
430
     * Sets the time when the introduction was last published.
431
     */
432
    public void setIntroPublishedNow() {
433
        lastTimeIntroPublished = new Date();
×
434
    }
×
435

436
    /**
437
     * Checks if the introduction has been published.
438
     *
439
     * @return True if the introduction has been published, false otherwise.
440
     */
441
    public boolean hasIntroPublished() {
442
        return lastTimeIntroPublished != null;
×
443
    }
444

445
    /**
446
     * Calculates the time since the last introduction was published.
447
     *
448
     * @return The time in milliseconds since the last introduction was published, or Long.MAX_VALUE if it has never been published.
449
     */
450
    public long getTimeSinceLastIntroPublished() {
451
        if (lastTimeIntroPublished == null) return Long.MAX_VALUE;
×
452
        return new Date().getTime() - lastTimeIntroPublished.getTime();
×
453
    }
454

455
    /**
456
     * Sets the view mode for displaying nanopublication results.
457
     *
458
     * @param viewMode The desired view mode (e.g., GRID or LIST).
459
     */
460
    public void setNanopubResultsViewMode(NanopubResults.ViewMode viewMode) {
461
        this.nanopubResultsViewMode = viewMode;
×
462
    }
×
463

464
    /**
465
     * Retrieves the current view mode for displaying nanopublication results.
466
     *
467
     * @return The current view mode.
468
     */
469
    public NanopubResults.ViewMode getNanopubResultsViewMode() {
470
        return this.nanopubResultsViewMode;
×
471
    }
472

473
    // --- Preview nanopub support ---
474

475
    public static class PreviewNanopub implements Serializable {
476
        private final Nanopub nanopub;
477
        private final PageParameters pageParams;
478
        private final Class<? extends WebPage> confirmPageClass;
479

480
        public PreviewNanopub(Nanopub nanopub, PageParameters pageParams, Class<? extends WebPage> confirmPageClass) {
×
481
            this.nanopub = nanopub;
×
482
            this.pageParams = pageParams;
×
483
            this.confirmPageClass = confirmPageClass;
×
484
        }
×
485

486
        public Nanopub getNanopub() { return nanopub; }
×
487
        public PageParameters getPageParams() { return pageParams; }
×
488
        public Class<? extends WebPage> getConfirmPageClass() { return confirmPageClass; }
×
489
    }
490

491
    private ConcurrentMap<String, PreviewNanopub> previewMap = new ConcurrentHashMap<>();
15✔
492

493
    public void setPreviewNanopub(String id, PreviewNanopub preview) {
494
        previewMap.put(id, preview);
×
495
    }
×
496

497
    public PreviewNanopub getPreviewNanopub(String id) {
498
        return previewMap.get(id);
×
499
    }
500

501
    public PreviewNanopub removePreviewNanopub(String id) {
502
        return previewMap.remove(id);
×
503
    }
504

505
}
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