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

knowledgepixels / nanodash / 22618039211

03 Mar 2026 10:05AM UTC coverage: 16.058% (+0.2%) from 15.884%
22618039211

Pull #365

github

web-flow
Merge 1e7e700f0 into a8c4b4a77
Pull Request #365: Refactor of `ResourceWithProfile` and related classes

699 of 5287 branches covered (13.22%)

Branch coverage included in aggregate %.

1721 of 9783 relevant lines covered (17.59%)

2.41 hits per line

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

14.35
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.domain.User;
6
import com.knowledgepixels.nanodash.page.OrcidLoginPage;
7
import com.knowledgepixels.nanodash.page.ProfilePage;
8
import jakarta.servlet.http.HttpServletRequest;
9
import jakarta.servlet.http.HttpSession;
10
import jakarta.xml.bind.DatatypeConverter;
11
import org.apache.commons.io.FileUtils;
12
import org.apache.wicket.Session;
13
import org.apache.wicket.protocol.http.WebSession;
14
import org.apache.wicket.request.Request;
15
import org.apache.wicket.request.flow.RedirectToUrlException;
16
import org.apache.wicket.request.mapper.parameter.PageParameters;
17
import org.eclipse.rdf4j.model.IRI;
18
import org.eclipse.rdf4j.model.ValueFactory;
19
import org.eclipse.rdf4j.model.impl.SimpleValueFactory;
20
import org.nanopub.extra.security.*;
21
import org.nanopub.extra.setting.IntroNanopub;
22
import org.slf4j.Logger;
23
import org.slf4j.LoggerFactory;
24

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

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

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

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

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

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

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

71
//        private IntroExtractor introExtractor;
72

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

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

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

85
    private Date lastTimeIntroPublished = null;
9✔
86

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

474
    // --- Preview nanopub support ---
475

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

482
        public PreviewNanopub(Nanopub nanopub, PageParameters pageParams, Class<? extends WebPage> confirmPageClass, boolean consentChecked) {
×
483
            this.nanopub = nanopub;
×
484
            this.pageParams = pageParams;
×
485
            this.confirmPageClass = confirmPageClass;
×
486
            this.consentChecked = consentChecked;
×
487
        }
×
488

489
        public Nanopub getNanopub() { return nanopub; }
×
490
        public PageParameters getPageParams() { return pageParams; }
×
491
        public Class<? extends WebPage> getConfirmPageClass() { return confirmPageClass; }
×
492
        public boolean isConsentChecked() { return consentChecked; }
×
493
    }
494

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

497
    public void setPreviewNanopub(String id, PreviewNanopub preview) {
498
        previewMap.put(id, preview);
×
499
    }
×
500

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

505
    public PreviewNanopub removePreviewNanopub(String id) {
506
        return previewMap.remove(id);
×
507
    }
508

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