• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In
Build has been canceled!

knowledgepixels / nanodash / 24785731352

22 Apr 2026 03:01PM UTC coverage: 17.46% (-0.005%) from 17.465%
24785731352

Pull #449

github

web-flow
Merge e4d6d022c into c9b9c0de8
Pull Request #449: refactor: drop formobj, use PageReference for preview Back

899 of 6188 branches covered (14.53%)

Branch coverage included in aggregate %.

2156 of 11309 relevant lines covered (19.06%)

2.64 hits per line

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

14.86
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.domain.User;
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.PageReference;
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.Collections;
31
import java.util.Date;
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
    // Small LRU of formObjId -> PageReference so that browser-back to
90
    // /publish?...&formobj=Y can recover the live PublishPage instance from
91
    // the page store. Holds PageReferences only, so entries are cheap and the
92
    // cap can stay generous.
93
    private static final int MAX_FORM_PAGE_REFS = 50;
94
    private final Map<String, PageReference> formPageRefMap = Collections.synchronizedMap(new LinkedHashMap<>(16, 0.75f, true) {
57✔
95
        @Override
96
        protected boolean removeEldestEntry(Map.Entry<String, PageReference> eldest) {
97
            return size() > MAX_FORM_PAGE_REFS;
×
98
        }
99
    });
100

101
    public void setFormPageRef(String formObjId, PageReference ref) {
102
        formPageRefMap.put(formObjId, ref);
×
103
    }
×
104

105
    public PageReference getFormPageRef(String formObjId) {
106
        return formPageRefMap.get(formObjId);
×
107
    }
108

109
    public void removeFormPageRef(String formObjId) {
110
        formPageRefMap.remove(formObjId);
×
111
    }
×
112

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

158
    /**
159
     * Checks if the user's profile is complete.
160
     *
161
     * @return True if the profile is complete, false otherwise.
162
     */
163
    public boolean isProfileComplete() {
164
        return userIri != null && keyPair != null && introNps != null;
×
165
    }
166

167
    /**
168
     * Redirects the user to the login page if their profile is incomplete.
169
     *
170
     * @param path       The path to redirect to after login.
171
     * @param parameters The page parameters for the redirect.
172
     */
173
    public void redirectToLoginIfNeeded(String path, PageParameters parameters) {
174
        String loginUrl = getLoginUrl(path, parameters);
×
175
        if (loginUrl == null) return;
×
176
        throw new RedirectToUrlException(loginUrl);
×
177
    }
178

179
    /**
180
     * Retrieves the login URL for the user.
181
     *
182
     * @param path       The path to redirect to after login.
183
     * @param parameters The page parameters for the redirect.
184
     * @return The login URL, or null if the user is already logged in.
185
     */
186
    public String getLoginUrl(String path, PageParameters parameters) {
187
        if (isProfileComplete()) return null;
×
188
        if (NanodashPreferences.get().isOrcidLoginMode()) {
×
189
            return OrcidLoginPage.getOrcidLoginUrl(path, parameters);
×
190
        } else {
191
            return ProfilePage.MOUNT_PATH;
×
192
        }
193
    }
194

195
    /**
196
     * Retrieves the public key as a Base64-encoded string.
197
     *
198
     * @return The public key string, or null if the key pair is not set.
199
     */
200
    public String getPubkeyString() {
201
        if (keyPair == null) return null;
×
202
        return DatatypeConverter.printBase64Binary(keyPair.getPublic().getEncoded()).replaceAll("\\s", "");
×
203
    }
204

205
    /**
206
     * Retrieves the public key hash for the user.
207
     *
208
     * @return The SHA-256 hash of the public key, or null if the public key is not set.
209
     */
210
    public String getPubkeyhash() {
211
        String pubkey = getPubkeyString();
×
212
        if (pubkey == null) return null;
×
213
        return Utils.createSha256HexHash(pubkey);
×
214
    }
215

216
    /**
217
     * Checks if the user's public key is approved.
218
     *
219
     * @return True if the public key is approved, false otherwise.
220
     */
221
    public boolean isPubkeyApproved() {
222
        if (keyPair == null || userIri == null) return false;
×
223
        return User.isApprovedPubkeyhashForUser(getPubkeyhash(), userIri);
×
224
    }
225

226
    /**
227
     * Retrieves the user's key pair.
228
     *
229
     * @return The key pair.
230
     */
231
    public KeyPair getKeyPair() {
232
        return keyPair;
×
233
    }
234

235
    /**
236
     * Generates a new key pair for the user.
237
     */
238
    public void makeKeys() {
239
        try {
240
            MakeKeys.make(getKeyFile().getAbsolutePath().replaceFirst("_rsa$", ""), SignatureAlgorithm.RSA);
×
241
            keyPair = SignNanopub.loadKey(getKeyFile().getPath(), SignatureAlgorithm.RSA);
×
242
        } catch (Exception ex) {
×
243
            logger.error("Couldn't create key pair", ex);
×
244
        }
×
245
    }
×
246

247
    /**
248
     * Retrieves the user's IRI.
249
     *
250
     * @return The user's IRI, or null if not set.
251
     */
252
    public IRI getUserIri() {
253
        return userIri;
9✔
254
    }
255

256
    /**
257
     * Retrieves the user's introduction nanopublications.
258
     *
259
     * @return A list of user's introduction nanopublications.
260
     */
261
    public List<IntroNanopub> getUserIntroNanopubs() {
262
        return User.getIntroNanopubs(userIri);
×
263
    }
264

265
    /**
266
     * Counts the number of local introduction nanopublications.
267
     *
268
     * @return The count of local introduction nanopublications.
269
     */
270
    public int getLocalIntroCount() {
271
        if (localIntroCount == null) {
×
272
            localIntroCount = 0;
×
273
            for (IntroNanopub inp : getUserIntroNanopubs()) {
×
274
                if (isIntroWithLocalKey(inp)) {
×
275
                    localIntroCount++;
×
276
                    localIntro = inp;
×
277
                }
278
            }
×
279
            if (localIntroCount > 1) localIntro = null;
×
280
        }
281
        return localIntroCount;
×
282
    }
283

284
    /**
285
     * Retrieves the local introduction nanopublication.
286
     *
287
     * @return The local introduction nanopublication, or null if not found.
288
     */
289
    public IntroNanopub getLocalIntro() {
290
        getLocalIntroCount();
×
291
        return localIntro;
×
292
    }
293

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

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

342
    /**
343
     * Logs out the user and invalidates the session.
344
     */
345
    public void logout() {
346
        userIri = null;
×
347
        invalidateNow();
×
348
    }
×
349

350
    /**
351
     * Retrieves the user's introduction nanopublications as a map.
352
     *
353
     * @return A map of introduction nanopublications.
354
     */
355
    public Map<IRI, IntroNanopub> getIntroNanopubs() {
356
        return introNps;
×
357
    }
358

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

404
    /**
405
     * Retrieves the file for storing the user's ORCID identifier.
406
     *
407
     * @return The ORCID file.
408
     */
409
    private File getOrcidFile() {
410
        return new File(userDir + "orcid");
21✔
411
    }
412

413
    /**
414
     * Retrieves the file for storing the user's private key.
415
     *
416
     * @return The key file.
417
     */
418
    public File getKeyFile() {
419
        return new File(userDir + "id_rsa");
×
420
    }
421

422
    /**
423
     * Sets the time when the introduction was last published.
424
     */
425
    public void setIntroPublishedNow() {
426
        lastTimeIntroPublished = new Date();
×
427
    }
×
428

429
    /**
430
     * Checks if the introduction has been published.
431
     *
432
     * @return True if the introduction has been published, false otherwise.
433
     */
434
    public boolean hasIntroPublished() {
435
        return lastTimeIntroPublished != null;
×
436
    }
437

438
    /**
439
     * Calculates the time since the last introduction was published.
440
     *
441
     * @return The time in milliseconds since the last introduction was published, or Long.MAX_VALUE if it has never been published.
442
     */
443
    public long getTimeSinceLastIntroPublished() {
444
        if (lastTimeIntroPublished == null) return Long.MAX_VALUE;
×
445
        return new Date().getTime() - lastTimeIntroPublished.getTime();
×
446
    }
447

448
    /**
449
     * Sets the view mode for displaying nanopublication results.
450
     *
451
     * @param viewMode The desired view mode (e.g., GRID or LIST).
452
     */
453
    public void setNanopubResultsViewMode(NanopubResults.ViewMode viewMode) {
454
        this.nanopubResultsViewMode = viewMode;
×
455
    }
×
456

457
    /**
458
     * Retrieves the current view mode for displaying nanopublication results.
459
     *
460
     * @return The current view mode.
461
     */
462
    public NanopubResults.ViewMode getNanopubResultsViewMode() {
463
        return this.nanopubResultsViewMode;
×
464
    }
465

466
    // --- Preview nanopub support ---
467

468
    public static class PreviewNanopub implements Serializable {
469
        private final Nanopub nanopub;
470
        private final PageParameters pageParams;
471
        private final Class<? extends WebPage> confirmPageClass;
472
        private final boolean consentChecked;
473
        private final String formObj;
474

475
        public PreviewNanopub(Nanopub nanopub, PageParameters pageParams, Class<? extends WebPage> confirmPageClass, boolean consentChecked, String formObj) {
×
476
            this.nanopub = nanopub;
×
477
            this.pageParams = pageParams;
×
478
            this.confirmPageClass = confirmPageClass;
×
479
            this.consentChecked = consentChecked;
×
480
            this.formObj = formObj;
×
481
        }
×
482

483
        public Nanopub getNanopub() { return nanopub; }
×
484
        public PageParameters getPageParams() { return pageParams; }
×
485
        public Class<? extends WebPage> getConfirmPageClass() { return confirmPageClass; }
×
486
        public boolean isConsentChecked() { return consentChecked; }
×
487
        public String getFormObj() { return formObj; }
×
488
    }
489

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

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

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

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

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