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

knowledgepixels / nanodash / 24771435393

22 Apr 2026 09:40AM UTC coverage: 17.397% (-0.01%) from 17.408%
24771435393

push

github

web-flow
Merge pull request #447 from knowledgepixels/log-form-object-loss

chore: log form-object loss and LRU eviction

899 of 6202 branches covered (14.5%)

Branch coverage included in aggregate %.

2156 of 11358 relevant lines covered (18.98%)

2.63 hits per line

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

14.6
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.Collections;
32
import java.util.LinkedHashMap;
33
import java.util.List;
34
import java.util.Map;
35
import java.util.concurrent.ConcurrentHashMap;
36
import java.util.concurrent.ConcurrentMap;
37

38
import org.apache.wicket.markup.html.WebPage;
39
import org.nanopub.Nanopub;
40

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

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

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

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

71
    private static ValueFactory vf = SimpleValueFactory.getInstance();
9✔
72

73
//        private IntroExtractor introExtractor;
74

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

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

84
    private Integer localIntroCount = null;
9✔
85
    private IntroNanopub localIntro = null;
9✔
86

87
    private Date lastTimeIntroPublished = null;
9✔
88

89
    // We should store here some sort of form model and not the forms themselves, but I couldn't figure
90
    // how to do it, so doing it like this for the moment...
91
    private static final int MAX_FORMS = 20;
92
    private final Map<String, PublishForm> formMap = Collections.synchronizedMap(new LinkedHashMap<>(16, 0.75f, true) {
57✔
93
        @Override
94
        protected boolean removeEldestEntry(Map.Entry<String, PublishForm> eldest) {
95
            boolean evict = size() > MAX_FORMS;
×
96
            if (evict) {
×
97
                logger.info("Evicting form from session LRU (formobj={}, cap={})", eldest.getKey(), MAX_FORMS);
×
98
            }
99
            return evict;
×
100
        }
101
    });
102

103
    /**
104
     * Associates a form object with a specific ID.
105
     *
106
     * @param formObjId The ID of the form object.
107
     * @param formObj   The form object to associate.
108
     */
109
    public void setForm(String formObjId, PublishForm formObj) {
110
        formMap.put(formObjId, formObj);
×
111
    }
×
112

113
    /**
114
     * Checks if a form object with the given ID exists.
115
     *
116
     * @param formObjId The ID of the form object.
117
     * @return True if the form object exists, false otherwise.
118
     */
119
    public boolean hasForm(String formObjId) {
120
        return formMap.containsKey(formObjId);
×
121
    }
122

123
    /**
124
     * Retrieves the form object associated with the given ID.
125
     *
126
     * @param formObjId The ID of the form object.
127
     * @return The associated form object, or null if not found.
128
     */
129
    public PublishForm getForm(String formObjId) {
130
        return formMap.get(formObjId);
×
131
    }
132

133
    /**
134
     * Removes the form object associated with the given ID.
135
     *
136
     * @param formObjId The ID of the form object to remove.
137
     */
138
    public void removeForm(String formObjId) {
139
        formMap.remove(formObjId);
×
140
    }
×
141

142
    /**
143
     * Loads profile information for the user.
144
     * Initializes user-related data such as keys and introductions.
145
     */
146
    public void loadProfileInfo() {
147
        localIntroCount = null;
9✔
148
        localIntro = null;
9✔
149
        NanodashPreferences prefs = NanodashPreferences.get();
6✔
150
        if (prefs.isOrcidLoginMode()) {
9!
151
            File usersDir = new File(System.getProperty("user.home") + "/.nanopub/nanodash-users/");
×
152
            if (!usersDir.exists()) usersDir.mkdir();
×
153
        }
154
        if (userIri == null && !prefs.isReadOnlyMode() && !prefs.isOrcidLoginMode()) {
27!
155
            if (getOrcidFile().exists()) {
12!
156
                try {
157
                    String orcid = FileUtils.readFileToString(getOrcidFile(), StandardCharsets.UTF_8).trim();
×
158
                    //String orcid = Files.readString(orcidFile.toPath(), StandardCharsets.UTF_8).trim();
159
                    if (orcid.matches(ProfilePage.ORCID_PATTERN)) {
×
160
                        userIri = vf.createIRI("https://orcid.org/" + orcid);
×
161
                        if (httpSession != null) httpSession.setMaxInactiveInterval(24 * 60 * 60);  // 24h
×
162
                    }
163
                } catch (IOException ex) {
×
164
                    logger.error("Couldn't read ORCID file", ex);
×
165
                }
×
166
            }
167
        }
168
        if (userIri != null && keyPair == null) {
9!
169
            File keyFile = getKeyFile();
×
170
            if (keyFile.exists()) {
×
171
                try {
172
                    keyPair = SignNanopub.loadKey(keyFile.getPath(), SignatureAlgorithm.RSA);
×
173
                } catch (Exception ex) {
×
174
                    logger.error("Couldn't load key pair", ex);
×
175
                }
×
176
            } else {
177
                // Automatically generate new keys
178
                makeKeys();
×
179
            }
180
        }
181
        if (userIri != null && keyPair != null && introNps == null) {
9!
182
            introNps = new ConcurrentHashMap<>(User.getIntroNanopubs(getPubkeyString()));
×
183
        }
184
//                checkOrcidLink();
185
    }
3✔
186

187
    /**
188
     * Checks if the user's profile is complete.
189
     *
190
     * @return True if the profile is complete, false otherwise.
191
     */
192
    public boolean isProfileComplete() {
193
        return userIri != null && keyPair != null && introNps != null;
×
194
    }
195

196
    /**
197
     * Redirects the user to the login page if their profile is incomplete.
198
     *
199
     * @param path       The path to redirect to after login.
200
     * @param parameters The page parameters for the redirect.
201
     */
202
    public void redirectToLoginIfNeeded(String path, PageParameters parameters) {
203
        String loginUrl = getLoginUrl(path, parameters);
×
204
        if (loginUrl == null) return;
×
205
        throw new RedirectToUrlException(loginUrl);
×
206
    }
207

208
    /**
209
     * Retrieves the login URL for the user.
210
     *
211
     * @param path       The path to redirect to after login.
212
     * @param parameters The page parameters for the redirect.
213
     * @return The login URL, or null if the user is already logged in.
214
     */
215
    public String getLoginUrl(String path, PageParameters parameters) {
216
        if (isProfileComplete()) return null;
×
217
        if (NanodashPreferences.get().isOrcidLoginMode()) {
×
218
            return OrcidLoginPage.getOrcidLoginUrl(path, parameters);
×
219
        } else {
220
            return ProfilePage.MOUNT_PATH;
×
221
        }
222
    }
223

224
    /**
225
     * Retrieves the public key as a Base64-encoded string.
226
     *
227
     * @return The public key string, or null if the key pair is not set.
228
     */
229
    public String getPubkeyString() {
230
        if (keyPair == null) return null;
×
231
        return DatatypeConverter.printBase64Binary(keyPair.getPublic().getEncoded()).replaceAll("\\s", "");
×
232
    }
233

234
    /**
235
     * Retrieves the public key hash for the user.
236
     *
237
     * @return The SHA-256 hash of the public key, or null if the public key is not set.
238
     */
239
    public String getPubkeyhash() {
240
        String pubkey = getPubkeyString();
×
241
        if (pubkey == null) return null;
×
242
        return Utils.createSha256HexHash(pubkey);
×
243
    }
244

245
    /**
246
     * Checks if the user's public key is approved.
247
     *
248
     * @return True if the public key is approved, false otherwise.
249
     */
250
    public boolean isPubkeyApproved() {
251
        if (keyPair == null || userIri == null) return false;
×
252
        return User.isApprovedPubkeyhashForUser(getPubkeyhash(), userIri);
×
253
    }
254

255
    /**
256
     * Retrieves the user's key pair.
257
     *
258
     * @return The key pair.
259
     */
260
    public KeyPair getKeyPair() {
261
        return keyPair;
×
262
    }
263

264
    /**
265
     * Generates a new key pair for the user.
266
     */
267
    public void makeKeys() {
268
        try {
269
            MakeKeys.make(getKeyFile().getAbsolutePath().replaceFirst("_rsa$", ""), SignatureAlgorithm.RSA);
×
270
            keyPair = SignNanopub.loadKey(getKeyFile().getPath(), SignatureAlgorithm.RSA);
×
271
        } catch (Exception ex) {
×
272
            logger.error("Couldn't create key pair", ex);
×
273
        }
×
274
    }
×
275

276
    /**
277
     * Retrieves the user's IRI.
278
     *
279
     * @return The user's IRI, or null if not set.
280
     */
281
    public IRI getUserIri() {
282
        return userIri;
9✔
283
    }
284

285
    /**
286
     * Retrieves the user's introduction nanopublications.
287
     *
288
     * @return A list of user's introduction nanopublications.
289
     */
290
    public List<IntroNanopub> getUserIntroNanopubs() {
291
        return User.getIntroNanopubs(userIri);
×
292
    }
293

294
    /**
295
     * Counts the number of local introduction nanopublications.
296
     *
297
     * @return The count of local introduction nanopublications.
298
     */
299
    public int getLocalIntroCount() {
300
        if (localIntroCount == null) {
×
301
            localIntroCount = 0;
×
302
            for (IntroNanopub inp : getUserIntroNanopubs()) {
×
303
                if (isIntroWithLocalKey(inp)) {
×
304
                    localIntroCount++;
×
305
                    localIntro = inp;
×
306
                }
307
            }
×
308
            if (localIntroCount > 1) localIntro = null;
×
309
        }
310
        return localIntroCount;
×
311
    }
312

313
    /**
314
     * Retrieves the local introduction nanopublication.
315
     *
316
     * @return The local introduction nanopublication, or null if not found.
317
     */
318
    public IntroNanopub getLocalIntro() {
319
        getLocalIntroCount();
×
320
        return localIntro;
×
321
    }
322

323
    /**
324
     * Checks if the given introduction nanopublication is associated with the local key.
325
     *
326
     * @param inp The introduction nanopublication.
327
     * @return True if associated with the local key, false otherwise.
328
     */
329
    public boolean isIntroWithLocalKey(IntroNanopub inp) {
330
        IRI location = Utils.getLocation(inp);
×
331
        NanopubSignatureElement el = Utils.getNanopubSignatureElement(inp);
×
332
        String siteUrl = NanodashPreferences.get().getWebsiteUrl();
×
333
        if (location != null && siteUrl != null) {
×
334
            String l = location.stringValue();
×
335
            // TODO: Solve the name change recognition in a better way:
336
            if (!l.equals(siteUrl) && !l.replace("nanobench", "nanodash").equals(siteUrl)) return false;
×
337
        }
338
        if (!getPubkeyString().equals(el.getPublicKeyString())) return false;
×
339
        for (KeyDeclaration kd : inp.getKeyDeclarations()) {
×
340
            if (getPubkeyString().equals(kd.getPublicKeyString())) return true;
×
341
        }
×
342
        return false;
×
343
    }
344

345
    /**
346
     * Sets the user's ORCID identifier.
347
     *
348
     * @param orcid The ORCID identifier.
349
     */
350
    public void setOrcid(String orcid) {
351
        if (!orcid.matches(ProfilePage.ORCID_PATTERN)) {
×
352
            throw new RuntimeException("Illegal ORCID identifier: " + orcid);
×
353
        }
354
        if (NanodashPreferences.get().isOrcidLoginMode()) {
×
355
            userDir = System.getProperty("user.home") + "/.nanopub/nanodash-users/" + orcid + "/";
×
356
            File f = new File(userDir);
×
357
            if (!f.exists()) f.mkdir();
×
358
        } else {
×
359
            try {
360
                FileUtils.writeStringToFile(getOrcidFile(), orcid + "\n", StandardCharsets.UTF_8);
×
361
                //                        Files.writeString(orcidFile.toPath(), orcid + "\n");
362
            } catch (IOException ex) {
×
363
                logger.error("Couldn't write ORCID file", ex);
×
364
            }
×
365
        }
366
        userIri = vf.createIRI("https://orcid.org/" + orcid);
×
367
        loadProfileInfo();
×
368
        if (httpSession != null) httpSession.setMaxInactiveInterval(24 * 60 * 60);  // 24h
×
369
    }
×
370

371
    /**
372
     * Logs out the user and invalidates the session.
373
     */
374
    public void logout() {
375
        userIri = null;
×
376
        invalidateNow();
×
377
    }
×
378

379
    /**
380
     * Retrieves the user's introduction nanopublications as a map.
381
     *
382
     * @return A map of introduction nanopublications.
383
     */
384
    public Map<IRI, IntroNanopub> getIntroNanopubs() {
385
        return introNps;
×
386
    }
387

388
//        public void checkOrcidLink() {
389
//                if (isOrcidLinked == null && userIri != null) {
390
//                        orcidLinkError = "";
391
//                        introExtractor = null;
392
//                        try {
393
//                                introExtractor = IntroNanopub.extract(userIri.stringValue(), null);
394
//                                if (introExtractor.getIntroNanopub() == null) {
395
//                                        orcidLinkError = "ORCID account is not linked.";
396
//                                        isOrcidLinked = false;
397
//                                } else {
398
//                                        IntroNanopub inp = IntroNanopub.get(userIri.stringValue(), introExtractor);
399
//                                        if (introNps != null && introNps.containsKey(inp.getNanopub().getUri())) {
400
//                                                // TODO: also check whether introduction contains local key
401
//                                                isOrcidLinked = true;
402
//                                        } else {
403
//                                                isOrcidLinked = false;
404
//                                                orcidLinkError = "Error: ORCID is linked to another introduction nanopublication.";
405
//                                        }
406
//                                }
407
//                        } catch (Exception ex) {
408
//                                logger.error("ORCID check failed");
409
//                                orcidLinkError = "ORCID check failed.";
410
//                        }
411
//                }
412
//        }
413
//
414
//        public void resetOrcidLinked() {
415
//                isOrcidLinked = null;
416
//        }
417
//
418
//        public boolean isOrcidLinked() {
419
//                checkOrcidLink();
420
//                return isOrcidLinked != null && isOrcidLinked == true;
421
//        }
422
//
423
//        public String getOrcidLinkError() {
424
//                return orcidLinkError;
425
//        }
426
//
427
//        public String getOrcidName() {
428
//                if (introExtractor == null || introExtractor.getName() == null) return null;
429
//                if (introExtractor.getName().trim().isEmpty()) return null;
430
//                return introExtractor.getName();
431
//        }
432

433
    /**
434
     * Retrieves the file for storing the user's ORCID identifier.
435
     *
436
     * @return The ORCID file.
437
     */
438
    private File getOrcidFile() {
439
        return new File(userDir + "orcid");
21✔
440
    }
441

442
    /**
443
     * Retrieves the file for storing the user's private key.
444
     *
445
     * @return The key file.
446
     */
447
    public File getKeyFile() {
448
        return new File(userDir + "id_rsa");
×
449
    }
450

451
    /**
452
     * Sets the time when the introduction was last published.
453
     */
454
    public void setIntroPublishedNow() {
455
        lastTimeIntroPublished = new Date();
×
456
    }
×
457

458
    /**
459
     * Checks if the introduction has been published.
460
     *
461
     * @return True if the introduction has been published, false otherwise.
462
     */
463
    public boolean hasIntroPublished() {
464
        return lastTimeIntroPublished != null;
×
465
    }
466

467
    /**
468
     * Calculates the time since the last introduction was published.
469
     *
470
     * @return The time in milliseconds since the last introduction was published, or Long.MAX_VALUE if it has never been published.
471
     */
472
    public long getTimeSinceLastIntroPublished() {
473
        if (lastTimeIntroPublished == null) return Long.MAX_VALUE;
×
474
        return new Date().getTime() - lastTimeIntroPublished.getTime();
×
475
    }
476

477
    /**
478
     * Sets the view mode for displaying nanopublication results.
479
     *
480
     * @param viewMode The desired view mode (e.g., GRID or LIST).
481
     */
482
    public void setNanopubResultsViewMode(NanopubResults.ViewMode viewMode) {
483
        this.nanopubResultsViewMode = viewMode;
×
484
    }
×
485

486
    /**
487
     * Retrieves the current view mode for displaying nanopublication results.
488
     *
489
     * @return The current view mode.
490
     */
491
    public NanopubResults.ViewMode getNanopubResultsViewMode() {
492
        return this.nanopubResultsViewMode;
×
493
    }
494

495
    // --- Preview nanopub support ---
496

497
    public static class PreviewNanopub implements Serializable {
498
        private final Nanopub nanopub;
499
        private final PageParameters pageParams;
500
        private final Class<? extends WebPage> confirmPageClass;
501
        private final boolean consentChecked;
502

503
        public PreviewNanopub(Nanopub nanopub, PageParameters pageParams, Class<? extends WebPage> confirmPageClass, boolean consentChecked) {
×
504
            this.nanopub = nanopub;
×
505
            this.pageParams = pageParams;
×
506
            this.confirmPageClass = confirmPageClass;
×
507
            this.consentChecked = consentChecked;
×
508
        }
×
509

510
        public Nanopub getNanopub() { return nanopub; }
×
511
        public PageParameters getPageParams() { return pageParams; }
×
512
        public Class<? extends WebPage> getConfirmPageClass() { return confirmPageClass; }
×
513
        public boolean isConsentChecked() { return consentChecked; }
×
514
    }
515

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

518
    public void setPreviewNanopub(String id, PreviewNanopub preview) {
519
        previewMap.put(id, preview);
×
520
    }
×
521

522
    public PreviewNanopub getPreviewNanopub(String id) {
523
        return previewMap.get(id);
×
524
    }
525

526
    public PreviewNanopub removePreviewNanopub(String id) {
527
        return previewMap.remove(id);
×
528
    }
529

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