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

knowledgepixels / nanodash / 29574247112

17 Jul 2026 10:40AM UTC coverage: 28.791% (-0.6%) from 29.361%
29574247112

Pull #560

github

web-flow
Merge b16ec84aa into 809bff55b
Pull Request #560: Show dates in a consistent format

1960 of 7501 branches covered (26.13%)

Branch coverage included in aggregate %.

3818 of 12568 relevant lines covered (30.38%)

4.55 hits per line

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

62.73
src/main/java/com/knowledgepixels/nanodash/Utils.java
1
package com.knowledgepixels.nanodash;
2

3
import com.google.common.hash.Hashing;
4
import com.knowledgepixels.nanodash.domain.User;
5
import net.trustyuri.TrustyUriUtils;
6
import org.apache.commons.codec.Charsets;
7
import org.apache.commons.lang.StringUtils;
8
import org.apache.http.client.utils.URIBuilder;
9
import org.apache.wicket.markup.html.link.ExternalLink;
10
import org.apache.wicket.model.IModel;
11
import org.apache.wicket.request.mapper.parameter.PageParameters;
12
import org.apache.wicket.util.string.StringValue;
13
import org.apache.wicket.util.string.Strings;
14
import org.eclipse.rdf4j.model.IRI;
15
import org.eclipse.rdf4j.model.Literal;
16
import org.eclipse.rdf4j.model.Statement;
17
import org.eclipse.rdf4j.model.ValueFactory;
18
import org.eclipse.rdf4j.model.impl.SimpleValueFactory;
19
import org.eclipse.rdf4j.model.util.Literals;
20
import org.eclipse.rdf4j.model.vocabulary.FOAF;
21
import org.eclipse.rdf4j.model.vocabulary.XSD;
22
import org.nanopub.Nanopub;
23
import org.nanopub.NanopubUtils;
24
import org.nanopub.extra.security.KeyDeclaration;
25
import org.nanopub.extra.security.MalformedCryptoElementException;
26
import org.nanopub.extra.security.NanopubSignatureElement;
27
import org.nanopub.extra.security.SignatureUtils;
28
import org.nanopub.extra.server.GetNanopub;
29
import org.nanopub.extra.server.NanopubServerUtils;
30
import org.nanopub.extra.services.ApiResponseEntry;
31
import org.nanopub.extra.services.NotEnoughAPIInstancesException;
32
import org.nanopub.extra.services.QueryCall;
33
import org.nanopub.extra.setting.IntroNanopub;
34
import org.nanopub.vocabulary.FIP;
35
import org.nanopub.vocabulary.NPX;
36
import org.owasp.html.HtmlPolicyBuilder;
37
import org.owasp.html.PolicyFactory;
38
import org.slf4j.Logger;
39
import org.slf4j.LoggerFactory;
40
import org.wicketstuff.select2.Select2Choice;
41

42
import java.io.Serializable;
43
import java.net.URISyntaxException;
44
import java.net.URLDecoder;
45
import java.net.URLEncoder;
46
import java.nio.charset.StandardCharsets;
47
import java.time.LocalDateTime;
48
import java.time.OffsetDateTime;
49
import java.time.format.DateTimeFormatter;
50
import java.time.format.DateTimeParseException;
51
import java.util.*;
52
import java.util.regex.Pattern;
53

54
import static java.nio.charset.StandardCharsets.UTF_8;
55

56
/**
57
 * Utility class providing various helper methods for handling nanopublications, URIs, and other related functionalities.
58
 */
59
public class Utils {
60

61
    private Utils() {
62
    }  // no instances allowed
63

64
    /**
65
     * ValueFactory instance for creating RDF model objects.
66
     */
67
    public static final ValueFactory vf = SimpleValueFactory.getInstance();
6✔
68
    private static final Logger logger = LoggerFactory.getLogger(Utils.class);
9✔
69
    private static final Pattern LEADING_TAG = Pattern.compile("^\\s*<(p|div|span|img|pre)(\\s|>|/).*", Pattern.CASE_INSENSITIVE);
12✔
70
    private static final String DEFAULT_MAIN_QUERY_URL = "https://query.knowledgepixels.com/";
71
    private static final String DEFAULT_MAIN_REGISTRY_URL = "https://registry.knowledgepixels.com/";
72

73
    /**
74
     * Generates a short name from a given IRI object.
75
     *
76
     * @param uri the IRI object
77
     * @return a short representation of the URI
78
     */
79
    public static String getShortNameFromURI(IRI uri) {
80
        return getShortNameFromURI(uri.stringValue());
12✔
81
    }
82

83
    /**
84
     * Generates a short name from a given URI string.
85
     *
86
     * @param uri the URI string
87
     * @return a short representation of the URI
88
     */
89
    public static String getShortNameFromURI(String uri) {
90
        if (uri.startsWith("https://doi.org/") || uri.startsWith("http://dx.doi.org/")) {
24✔
91
            return uri.replaceFirst("^https?://(dx\\.)?doi.org/", "doi:");
15✔
92
        }
93
        uri = uri.replaceFirst("\\?.*$", "");
15✔
94
        uri = uri.replaceFirst("[/#]$", "");
15✔
95
        uri = uri.replaceFirst("^.*[/#]([^/#]*)[/#]([0-9]+)$", "$1/$2");
15✔
96
        if (uri.contains("#")) {
12✔
97
            uri = uri.replaceFirst("^.*#(.*[^0-9].*)$", "$1");
18✔
98
        } else {
99
            uri = uri.replaceFirst("^.*/([^/]*[^0-9/][^/]*)$", "$1");
15✔
100
        }
101
        uri = uri.replaceFirst("((^|[^A-Za-z0-9\\-_])RA[A-Za-z0-9\\-_]{8})[A-Za-z0-9\\-_]{35}$", "$1");
15✔
102
        uri = uri.replaceFirst("(^|[^A-Za-z0-9\\-_])RA[A-Za-z0-9\\-_]{43}[^A-Za-z0-9\\-_](.+)$", "$2");
15✔
103
        uri = URLDecoder.decode(uri, UTF_8);
12✔
104
        return uri;
6✔
105
    }
106

107
    /**
108
     * Generates a short nanopublication ID from a given nanopublication ID or URI.
109
     *
110
     * @param npId the nanopublication ID or URI
111
     * @return the first 10 characters of the artifact code
112
     */
113
    public static String getShortNanopubId(Object npId) {
114
        return TrustyUriUtils.getArtifactCode(npId.toString()).substring(0, 10);
21✔
115
    }
116

117
    private static Map<String, Nanopub> nanopubs = new HashMap<>();
12✔
118

119
    /**
120
     * Adds a nanopublication to the local cache so it can be retrieved immediately
121
     * without needing to fetch it from the registry.
122
     *
123
     * @param np the nanopublication to cache
124
     */
125
    public static void cacheNanopub(Nanopub np) {
126
        String artifactCode = GetNanopub.getArtifactCode(np.getUri().stringValue()).toString();
×
127
        nanopubs.put(artifactCode, np);
×
128
    }
×
129

130
    /**
131
     * Retrieves a Nanopub object based on the given URI or artifact code.
132
     *
133
     * @param uriOrArtifactCode the URI or artifact code of the nanopublication
134
     * @return the Nanopub object, or null if not found
135
     */
136
    public static Nanopub getNanopub(String uriOrArtifactCode) {
137
        String artifactCode = GetNanopub.getArtifactCode(uriOrArtifactCode).toString();
12✔
138
        if (!nanopubs.containsKey(artifactCode)) {
12✔
139
            for (int i = 0; i < 3; i++) {  // Try 3 times to get nanopub
15!
140
                Nanopub np = GetNanopub.get(artifactCode);
9✔
141
                if (np != null) {
6!
142
                    nanopubs.put(artifactCode, np);
15✔
143
                    break;
3✔
144
                }
145
            }
146
        }
147
        return nanopubs.get(artifactCode);
15✔
148
    }
149

150
    /**
151
     * Strips a sub-IRI of a nanopublication (e.g. an embedded resource IRI like
152
     * {@code <np-uri>/template}) down to the nanopublication ID itself, i.e. up to
153
     * and including the trusty artifact code. An ID without a sub-path is returned
154
     * unchanged.
155
     *
156
     * @param id the ID to strip
157
     * @return the nanopublication ID
158
     */
159
    public static String stripToNanopubId(String id) {
160
        return id.replaceFirst("^(.*[^A-Za-z0-9-_]RA[A-Za-z0-9-_]{43})[^A-Za-z0-9-_].*$", "$1");
15✔
161
    }
162

163
    /**
164
     * URL-encodes the string representation of the given object using UTF-8 encoding.
165
     *
166
     * @param o the object to be URL-encoded
167
     * @return the URL-encoded string
168
     */
169
    public static String urlEncode(Object o) {
170
        return URLEncoder.encode((o == null ? "" : o.toString()), Charsets.UTF_8);
27✔
171
    }
172

173
    public static String truncateLabel(String label) {
174
        if (label != null && label.length() > 120) {
×
175
            return label.substring(0, 100) + "...";
×
176
        }
177
        return label;
×
178
    }
179

180
    /**
181
     * Truncates an over-long entity label for display in a link or breadcrumb:
182
     * labels longer than 60 characters are cut to 47 characters with an ellipsis
183
     * ("...") appended, so a single long label (e.g. a full IRI tail) never blows
184
     * up the surrounding UI. Shorter labels are returned unchanged. The full label
185
     * stays available on the entity's own page, which shows it as the title.
186
     *
187
     * @param label the label to truncate, or null
188
     * @return the truncated label, or the original if 60 characters or shorter
189
     */
190
    public static String truncateLinkLabel(String label) {
191
        if (label == null || label.length() <= 60) {
18!
192
            return label;
6✔
193
        }
194
        return label.substring(0, 47).stripTrailing() + "...";
×
195
    }
196

197
    /**
198
     * Builds the HTML body for a menu entry whose label may begin with a leading
199
     * symbol/emoji used as the entry's icon. If {@code label} starts with a token
200
     * of symbol/emoji characters (no letters or digits) followed by whitespace,
201
     * that token is wrapped in the {@code .actionmenu-icon} slot and the remaining
202
     * text follows it (both escaped). Returns {@code null} when there is no such
203
     * leading icon, so callers can fall back to the plain (escaped) label.
204
     *
205
     * @param label the menu entry label
206
     * @return the icon+text HTML body to render with escaping disabled, or null
207
     */
208
    public static String menuEntryIconBodyHtml(String label) {
209
        if (label == null) {
×
210
            return null;
×
211
        }
212
        int sp = -1;
×
213
        for (int i = 0; i < label.length(); i++) {
×
214
            if (Character.isWhitespace(label.charAt(i))) {
×
215
                sp = i;
×
216
                break;
×
217
            }
218
        }
219
        if (sp <= 0) {
×
220
            return null;
×
221
        }
222
        String icon = label.substring(0, sp);
×
223
        String rest = label.substring(sp).replaceFirst("^\\s+", "");
×
224
        if (rest.isEmpty()) {
×
225
            return null;
×
226
        }
227
        // Only a pure symbol/emoji token (no letters or digits) counts as an icon.
228
        if (icon.codePoints().anyMatch(Character::isLetterOrDigit)) {
×
229
            return null;
×
230
        }
231
        return "<span class=\"actionmenu-icon\">" + Strings.escapeMarkup(icon) + "</span>" + Strings.escapeMarkup(rest);
×
232
    }
233

234
    /**
235
     * URL-decodes the string representation of the given object using UTF-8 encoding.
236
     *
237
     * @param o the object to be URL-decoded
238
     * @return the URL-decoded string
239
     */
240
    public static String urlDecode(Object o) {
241
        return URLDecoder.decode((o == null ? "" : o.toString()), Charsets.UTF_8);
27✔
242
    }
243

244
    /**
245
     * Generates a URL with the given base and appends the provided PageParameters as query parameters.
246
     *
247
     * @param base       the base URL
248
     * @param parameters the PageParameters to append
249
     * @return the complete URL with parameters
250
     */
251
    public static String getUrlWithParameters(String base, PageParameters parameters) {
252
        try {
253
            URIBuilder u = new URIBuilder(base);
15✔
254
            for (String key : parameters.getNamedKeys()) {
33✔
255
                for (StringValue value : parameters.getValues(key)) {
36✔
256
                    if (!value.isNull()) {
9!
257
                        u.addParameter(key, value.toString());
18✔
258
                    }
259
                }
3✔
260
            }
3✔
261
            return u.build().toString();
12✔
262
        } catch (URISyntaxException ex) {
3✔
263
            logger.error("Could not build URL with parameters: {} {}", base, parameters, ex);
51✔
264
            return "/";
6✔
265
        }
266
    }
267

268
    /**
269
     * Generates a short name for a public key or public key hash.
270
     *
271
     * @param pubkeyOrPubkeyhash the public key (64 characters) or public key hash (40 characters)
272
     * @return a short representation of the public key or public key hash
273
     */
274
    public static String getShortPubkeyName(String pubkeyOrPubkeyhash) {
275
        if (pubkeyOrPubkeyhash.length() == 64) {
12!
276
            return pubkeyOrPubkeyhash.replaceFirst("^(.{8}).*$", "$1");
×
277
        } else {
278
            return pubkeyOrPubkeyhash.replaceFirst("^(.).{39}(.{5}).*$", "$1..$2..");
15✔
279
        }
280
    }
281

282
    /**
283
     * Generates a short label for a public key or public key hash, including its status (local or approved).
284
     *
285
     * @param pubkeyOrPubkeyhash the public key (64 characters) or public key hash (40 characters)
286
     * @param user               the IRI of the user associated with the public key
287
     * @return a short label indicating the public key and its status
288
     */
289
    public static String getShortPubkeyhashLabel(String pubkeyOrPubkeyhash, IRI user) {
290
        String s = getShortPubkeyName(pubkeyOrPubkeyhash);
×
291
        NanodashSession session = NanodashSession.get();
×
292
        List<String> l = new ArrayList<>();
×
293
        if (pubkeyOrPubkeyhash.equals(session.getPubkeyString()) || pubkeyOrPubkeyhash.equals(session.getPubkeyhash())) {
×
294
            l.add("local");
×
295
        }
296
        // TODO: Make this more efficient:
297
        String hashed = Utils.createSha256HexHash(pubkeyOrPubkeyhash);
×
298
        if (User.getPubkeyhashes(user, true).contains(pubkeyOrPubkeyhash) || User.getPubkeyhashes(user, true).contains(hashed)) {
×
299
            l.add("approved");
×
300
        }
301
        if (!l.isEmpty()) {
×
302
            s += " (" + String.join("/", l) + ")";
×
303
        }
304
        return s;
×
305
    }
306

307
    /**
308
     * Retrieves the name of the public key location based on the public key.
309
     *
310
     * @param pubkeyhash the public key string
311
     * @return the name of the public key location
312
     */
313
    public static String getPubkeyLocationName(String pubkeyhash) {
314
        return getPubkeyLocationName(pubkeyhash, getShortPubkeyName(pubkeyhash));
×
315
    }
316

317
    /**
318
     * Retrieves the name of the public key location, or returns a fallback name if not found.
319
     * If the key location is localhost, it returns "localhost".
320
     *
321
     * @param pubkeyhash the public key string
322
     * @param fallback   the fallback name to return if the key location is not found
323
     * @return the name of the public key location or the fallback name
324
     */
325
    public static String getPubkeyLocationName(String pubkeyhash, String fallback) {
326
        IRI keyLocation = User.getUserData().getKeyLocationForPubkeyHash(pubkeyhash);
×
327
        if (keyLocation == null) {
×
328
            return fallback;
×
329
        }
330
        if (keyLocation.stringValue().equals("http://localhost:37373/")) {
×
331
            return "localhost";
×
332
        }
333
        return keyLocation.stringValue().replaceFirst("https?://(nanobench\\.)?(nanodash\\.(?=.*\\..))?(.*[^/])/?$", "$3");
×
334
    }
335

336
    /**
337
     * Generates a short label for a public key location, including its status (local or approved).
338
     *
339
     * @param pubkeyhash the public key string
340
     * @param user       the IRI of the user associated with the public key
341
     * @return a short label indicating the public key location and its status
342
     */
343
    public static String getShortPubkeyLocationLabel(String pubkeyhash, IRI user) {
344
        String s = getPubkeyLocationName(pubkeyhash);
×
345
        NanodashSession session = NanodashSession.get();
×
346
        List<String> l = new ArrayList<>();
×
347
        if (pubkeyhash.equals(session.getPubkeyhash())) {
×
348
            l.add("local");
×
349
        }
350
        // TODO: Make this more efficient:
351
        if (User.getPubkeyhashes(user, true).contains(pubkeyhash)) {
×
352
            l.add("approved");
×
353
        }
354
        if (!l.isEmpty()) {
×
355
            s += " (" + String.join("/", l) + ")";
×
356
        }
357
        return s;
×
358
    }
359

360
    /**
361
     * Checks if a given public key has a Nanodash location.
362
     * A Nanodash location is identified by specific keywords in the key location.
363
     *
364
     * @param pubkeyhash the public key to check
365
     * @return true if the public key has a Nanodash location, false otherwise
366
     */
367
    public static boolean hasNanodashLocation(String pubkeyhash) {
368
        IRI keyLocation = User.getUserData().getKeyLocationForPubkeyHash(pubkeyhash);
×
369
        if (keyLocation == null) {
×
370
            return true; // potentially a Nanodash location
×
371
        }
372
        if (keyLocation.stringValue().contains("nanodash")) {
×
373
            return true;
×
374
        }
375
        if (keyLocation.stringValue().contains("nanobench")) {
×
376
            return true;
×
377
        }
378
        if (keyLocation.stringValue().contains(":37373")) {
×
379
            return true;
×
380
        }
381
        return false;
×
382
    }
383

384
    /**
385
     * Retrieves the short ORCID ID from an IRI object.
386
     *
387
     * @param orcidIri the IRI object representing the ORCID ID
388
     * @return the short ORCID ID as a string
389
     */
390
    public static String getShortOrcidId(IRI orcidIri) {
391
        return orcidIri.stringValue().replaceFirst("^https://orcid.org/", "");
18✔
392
    }
393

394
    /**
395
     * Retrieves the URI postfix from a given URI object.
396
     *
397
     * @param uri the URI object from which to extract the postfix
398
     * @return the URI postfix as a string
399
     */
400
    public static String getUriPostfix(Object uri) {
401
        String s = uri.toString();
9✔
402
        if (s.contains("#")) {
12✔
403
            return s.replaceFirst("^.*#(.*)$", "$1");
15✔
404
        }
405
        return s.replaceFirst("^.*/(.*)$", "$1");
15✔
406
    }
407

408
    /**
409
     * Retrieves the URI prefix from a given URI object.
410
     *
411
     * @param uri the URI object from which to extract the prefix
412
     * @return the URI prefix as a string
413
     */
414
    public static String getUriPrefix(Object uri) {
415
        String s = uri.toString();
9✔
416
        if (s.contains("#")) {
12✔
417
            return s.replaceFirst("^(.*#).*$", "$1");
15✔
418
        }
419
        return s.replaceFirst("^(.*/).*$", "$1");
15✔
420
    }
421

422
    /**
423
     * Checks if a given string is a valid URI postfix.
424
     * A valid URI postfix does not contain a colon (":").
425
     *
426
     * @param s the string to check
427
     * @return true if the string is a valid URI postfix, false otherwise
428
     */
429
    public static boolean isUriPostfix(String s) {
430
        return !s.contains(":");
24✔
431
    }
432

433
    /**
434
     * Retrieves the location of a given IntroNanopub.
435
     *
436
     * @param inp the IntroNanopub from which to extract the location
437
     * @return the IRI location of the nanopublication, or null if not found
438
     */
439
    public static IRI getLocation(IntroNanopub inp) {
440
        NanopubSignatureElement el = getNanopubSignatureElement(inp);
×
441
        for (KeyDeclaration kd : inp.getKeyDeclarations()) {
×
442
            if (el.getPublicKeyString().equals(kd.getPublicKeyString())) {
×
443
                return kd.getKeyLocation();
×
444
            }
445
        }
×
446
        return null;
×
447
    }
448

449
    /**
450
     * Retrieves the NanopubSignatureElement from a given IntroNanopub.
451
     *
452
     * @param inp the IntroNanopub from which to extract the signature element
453
     * @return the NanopubSignatureElement associated with the nanopublication
454
     */
455
    public static NanopubSignatureElement getNanopubSignatureElement(IntroNanopub inp) {
456
        try {
457
            return SignatureUtils.getSignatureElement(inp.getNanopub());
×
458
        } catch (MalformedCryptoElementException ex) {
×
459
            throw new RuntimeException(ex);
×
460
        }
461
    }
462

463
    /**
464
     * Retrieves a Nanopub object from a given URI if it is a potential Trusty URI.
465
     *
466
     * @param uri the URI to check and retrieve the Nanopub from
467
     * @return the Nanopub object if found, or null if not a known nanopublication
468
     */
469
    public static Nanopub getAsNanopub(String uri) {
470
        if (uri == null) {
6!
471
            return null;
×
472
        }
473
        if (TrustyUriUtils.isPotentialTrustyUri(uri)) {
9!
474
            try {
475
                return Utils.getNanopub(uri);
9✔
476
            } catch (Exception ex) {
×
477
                logger.error("The given URI is not a known nanopublication: {}", uri, ex);
×
478
            }
479
        }
480
        return null;
×
481
    }
482

483
    private static final PolicyFactory htmlSanitizePolicy = new HtmlPolicyBuilder().allowCommonBlockElements().allowCommonInlineFormattingElements().allowUrlProtocols("https", "http", "mailto").allowElements("a").allowAttributes("href").onElements("a").allowElements("img").allowAttributes("src").onElements("img").allowElements("pre").requireRelNofollowOnLinks().toFactory();
216✔
484

485
    /**
486
     * Sanitizes raw HTML input to ensure safe rendering.
487
     *
488
     * @param rawHtml the raw HTML input to sanitize
489
     * @return sanitized HTML string
490
     */
491
    public static String sanitizeHtml(String rawHtml) {
492
        return htmlSanitizePolicy.sanitize(rawHtml);
12✔
493
    }
494

495
    /**
496
     * Checks if a given string is likely to be HTML content.
497
     *
498
     * @param value the string to check
499
     * @return true if the given string is HTML content, false otherwise
500
     */
501
    public static boolean looksLikeHtml(String value) {
502
        return LEADING_TAG.matcher(value).find();
15✔
503
    }
504

505
    public static boolean isDate(String value) {
506
        return isDateLiteral(value) || isDateTimeLiteral(value);
30✔
507
    }
508

509
    public static boolean isDateLiteral(String value) {
510
        if (value == null || value.isBlank() || value.contains("T")) {
27✔
511
            return false;
6✔
512
        }
513
        try {
514
            DateTimeFormatter.ISO_DATE.parse(value);
12✔
515
            return true;
6✔
516
        } catch (DateTimeParseException ex) {
3✔
517
            return false;
6✔
518
        }
519
    }
520

521
    /**
522
     * Checks whether a (raw query-result) string looks like an ISO-8601 date-time literal.
523
     *
524
     * @param value the string to check
525
     * @return true if the string parses as a xsd:dateTime-style value
526
     */
527
    public static boolean isDateTimeLiteral(String value) {
528
        if (value == null || value.isBlank()) {
15✔
529
            return false;
6✔
530
        }
531

532
        try {
533
            OffsetDateTime.parse(value, DateTimeFormatter.ISO_OFFSET_DATE_TIME);
12✔
534
            return true;
6✔
535
        } catch (DateTimeParseException ignored) {
3✔
536
        }
537

538
        try {
539
            LocalDateTime.parse(value, DateTimeFormatter.ISO_LOCAL_DATE_TIME);
12✔
540
            return true;
6✔
541
        } catch (DateTimeParseException ignored) {
3✔
542
            return false;
6✔
543
        }
544
    }
545

546
    /**
547
     * Renders a {@code <time>} element for an ISO-8601 date-time value. The machine-readable
548
     * value goes in the {@code datetime} attribute; client-side script (nanodash.js) rewrites
549
     * the visible text to a relative form ("10 minutes ago") in the viewer's local timezone and
550
     * puts the absolute date-time in the tooltip. If script does not run, {@code fallbackText}
551
     * remains visible.
552
     *
553
     * @param isoValue     the ISO-8601 date-time string (machine-readable)
554
     * @param fallbackText the human-readable text shown when script is unavailable
555
     * @return an HTML {@code <time>} element string (caller must render with escaping disabled)
556
     */
557
    public static String friendlyDateHtml(String isoValue, String fallbackText) {
558
        return "<time class=\"friendly-date\" datetime=\"" + Strings.escapeMarkup(isoValue) + "\">" + Strings.escapeMarkup(fallbackText) + "</time>";
×
559
    }
560

561
    /**
562
     * Converts PageParameters to a URL-encoded string representation.
563
     *
564
     * @param params the PageParameters to convert
565
     * @return a string representation of the parameters in URL-encoded format
566
     */
567
    public static String getPageParametersAsString(PageParameters params) {
568
        String s = "";
6✔
569
        for (String n : params.getNamedKeys()) {
33✔
570
            if (!s.isEmpty()) {
9✔
571
                s += "&";
9✔
572
            }
573
            s += n + "=" + URLEncoder.encode(params.get(n).toString(), Charsets.UTF_8);
30✔
574
        }
3✔
575
        return s;
6✔
576
    }
577

578
    /**
579
     * Sets a minimal escape markup function for a Select2Choice component.
580
     * This function replaces certain characters and formats the display of choices.
581
     *
582
     * @param selectItem the Select2Choice component to set the escape markup for
583
     */
584
    public static void setSelect2ChoiceMinimalEscapeMarkup(Select2Choice<?> selectItem) {
585
        selectItem.getSettings().setEscapeMarkup("function(markup) {" + "return markup" + ".replaceAll('<','&lt;').replaceAll('>', '&gt;')" + ".replace(/^(.*?) - /, '<span class=\"term\">$1</span><br>')" + ".replace(/\\((https?:[\\S]+)\\)$/, '<br><code>$1</code>')" + ".replace(/^([^<].*)$/, '<span class=\"term\">$1</span>')" + ";}");
15✔
586
    }
3✔
587

588
    /**
589
     * Checks if a nanopublication is of a specific class.
590
     *
591
     * @param np       the nanopublication to check
592
     * @param classIri the IRI of the class to check against
593
     * @return true if the nanopublication is of the specified class, false otherwise
594
     */
595
    public static boolean isNanopubOfClass(Nanopub np, IRI classIri) {
596
        return NanopubUtils.getTypes(np).contains(classIri);
15✔
597
    }
598

599
    /**
600
     * Checks if a nanopublication uses a specific predicate in its assertion.
601
     *
602
     * @param np           the nanopublication to check
603
     * @param predicateIri the IRI of the predicate to look for
604
     * @return true if the predicate is used in the assertion, false otherwise
605
     */
606
    public static boolean usesPredicateInAssertion(Nanopub np, IRI predicateIri) {
607
        for (Statement st : np.getAssertion()) {
33✔
608
            if (predicateIri.equals(st.getPredicate())) {
15✔
609
                return true;
6✔
610
            }
611
        }
3✔
612
        return false;
6✔
613
    }
614

615
    /**
616
     * Retrieves a map of FOAF names from the nanopublication's pubinfo.
617
     *
618
     * @param np the nanopublication from which to extract FOAF names
619
     * @return a map where keys are subjects and values are FOAF names
620
     */
621
    public static Map<String, String> getFoafNameMap(Nanopub np) {
622
        Map<String, String> foafNameMap = new HashMap<>();
12✔
623
        for (Statement st : np.getPubinfo()) {
33✔
624
            if (st.getPredicate().equals(FOAF.NAME) && st.getObject() instanceof Literal objL) {
42✔
625
                foafNameMap.put(st.getSubject().stringValue(), objL.stringValue());
24✔
626
            }
627
        }
3✔
628
        return foafNameMap;
6✔
629
    }
630

631
    /**
632
     * Creates an SHA-256 hash of the string representation of an object and returns it as a hexadecimal string.
633
     *
634
     * @param obj the object to hash
635
     * @return the SHA-256 hash of the object's string representation in hexadecimal format
636
     */
637
    public static String createSha256HexHash(Object obj) {
638
        return Hashing.sha256().hashString(obj.toString(), StandardCharsets.UTF_8).toString();
21✔
639
    }
640

641
    /**
642
     * Gets the types of a nanopublication.
643
     *
644
     * @param np the nanopublication from which to extract types
645
     * @return a list of IRI types associated with the nanopublication
646
     */
647
    public static List<IRI> getTypes(Nanopub np) {
648
        List<IRI> l = new ArrayList<>();
12✔
649
        for (IRI t : NanopubUtils.getTypes(np)) {
33✔
650
            if (t.equals(FIP.AVAILABLE_FAIR_ENABLING_RESOURCE)) {
12✔
651
                continue;
3✔
652
            }
653
            if (t.equals(FIP.FAIR_ENABLING_RESOURCE_TO_BE_DEVELOPED)) {
12✔
654
                continue;
3✔
655
            }
656
            if (t.equals(FIP.AVAILABLE_FAIR_SUPPORTING_RESOURCE)) {
12!
657
                continue;
×
658
            }
659
            if (t.equals(FIP.FAIR_SUPPORTING_RESOURCE_TO_BE_DEVELOPED)) {
12!
660
                continue;
×
661
            }
662
            l.add(t);
12✔
663
        }
3✔
664
        return l;
6✔
665
    }
666

667
    /**
668
     * Gets a label for a type IRI.
669
     *
670
     * @param typeIri the IRI of the type
671
     * @return a label for the type, potentially truncated
672
     */
673
    public static String getTypeLabel(IRI typeIri) {
674
        if (typeIri.equals(FIP.FAIR_ENABLING_RESOURCE)) {
12✔
675
            return "FER";
6✔
676
        }
677
        if (typeIri.equals(FIP.FAIR_SUPPORTING_RESOURCE)) {
12✔
678
            return "FSR";
6✔
679
        }
680
        if (typeIri.equals(FIP.FAIR_IMPLEMENTATION_PROFILE)) {
12✔
681
            return "FIP";
6✔
682
        }
683
        if (typeIri.equals(NPX.DECLARED_BY)) {
12✔
684
            return "user intro";
6✔
685
        }
686
        String l = typeIri.stringValue();
9✔
687
        l = l.replaceFirst("^.*[/#]([^/#]+)[/#]?$", "$1");
15✔
688
        l = l.replaceFirst("^(.+)Nanopub$", "$1");
15✔
689
        if (l.length() > 25) {
12✔
690
            l = l.substring(0, 20) + "...";
18✔
691
        }
692
        return l;
6✔
693
    }
694

695
    /**
696
     * Gets a label for a URI.
697
     *
698
     * @param uri the URI to get the label from
699
     * @return a label for the URI, potentially truncated
700
     */
701
    public static String getUriLabel(String uri) {
702
        if (uri == null) {
6✔
703
            return "";
6✔
704
        }
705
        String uriLabel = uri;
6✔
706
        if (uriLabel.matches(".*[^A-Za-z0-9-_]RA[A-Za-z0-9-_]{43}([^A-Za-z0-9-_].*)?")) {
12✔
707
            String newUriLabel = uriLabel.replaceFirst("(.*[^A-Za-z0-9-_]RA[A-Za-z0-9-_]{8})[A-Za-z0-9-_]{35}([^A-Za-z0-9-_].*)?", "$1...$2");
15✔
708
            if (newUriLabel.length() <= 70) {
12!
709
                return newUriLabel;
6✔
710
            }
711
        }
712
        if (uriLabel.length() > 70) {
12✔
713
            return uri.substring(0, 30) + "..." + uri.substring(uri.length() - 30);
36✔
714
        }
715
        return uriLabel;
6✔
716
    }
717

718
    /**
719
     * Gets an ExternalLink with a URI label.
720
     *
721
     * @param markupId the markup ID for the link
722
     * @param uri      the URI to link to
723
     * @return an ExternalLink with the URI label
724
     */
725
    public static ExternalLink getUriLink(String markupId, String uri) {
726
        return new ExternalLink(markupId, (Utils.isLocalURI(uri) ? "" : uri), getUriLabel(uri));
39✔
727
    }
728

729
    /**
730
     * Gets an ExternalLink with a model for the URI label.
731
     *
732
     * @param markupId the markup ID for the link
733
     * @param model    the model containing the URI
734
     * @return an ExternalLink with the URI label
735
     */
736
    public static ExternalLink getUriLink(String markupId, IModel<String> model) {
737
        return new ExternalLink(markupId, model, new UriLabelModel(model));
30✔
738
    }
739

740
    private static class UriLabelModel implements IModel<String> {
741

742
        private IModel<String> uriModel;
743

744
        public UriLabelModel(IModel<String> uriModel) {
6✔
745
            this.uriModel = uriModel;
9✔
746
        }
3✔
747

748
        @Override
749
        public String getObject() {
750
            return getUriLabel(uriModel.getObject());
×
751
        }
752

753
    }
754

755
    /**
756
     * Creates a sublist from a list based on the specified indices.
757
     *
758
     * @param list      the list from which to create the sublist
759
     * @param fromIndex the starting index (inclusive) for the sublist
760
     * @param toIndex   the ending index (exclusive) for the sublist
761
     * @param <E>       the type of elements in the list
762
     * @return an ArrayList containing the elements from the specified range
763
     */
764
    public static <E> ArrayList<E> subList(List<E> list, long fromIndex, long toIndex) {
765
        // So the resulting list is serializable:
766
        return new ArrayList<E>(list.subList((int) fromIndex, (int) toIndex));
×
767
    }
768

769
    /**
770
     * Creates a sublist from an array based on the specified indices.
771
     *
772
     * @param array     the array from which to create the sublist
773
     * @param fromIndex the starting index (inclusive) for the sublist
774
     * @param toIndex   the ending index (exclusive) for the sublist
775
     * @param <E>       the type of elements in the array
776
     * @return an ArrayList containing the elements from the specified range
777
     */
778
    public static <E> ArrayList<E> subList(E[] array, long fromIndex, long toIndex) {
779
        return subList(Arrays.asList(array), fromIndex, toIndex);
×
780
    }
781

782
    /**
783
     * Comparator for sorting ApiResponseEntry objects based on a specified field.
784
     */
785
    // TODO Move this to ApiResponseEntry class?
786
    public static class ApiResponseEntrySorter implements Comparator<ApiResponseEntry>, Serializable {
787

788
        private String field;
789
        private boolean descending;
790

791
        /**
792
         * Constructor for ApiResponseEntrySorter.
793
         *
794
         * @param field      the field to sort by
795
         * @param descending if true, sorts in descending order; if false, sorts in ascending order
796
         */
797
        public ApiResponseEntrySorter(String field, boolean descending) {
×
798
            this.field = field;
×
799
            this.descending = descending;
×
800
        }
×
801

802
        /**
803
         * Compares two ApiResponseEntry objects based on the specified field.
804
         *
805
         * @param o1 the first object to be compared.
806
         * @param o2 the second object to be compared.
807
         * @return a negative integer, zero, or a positive integer as the first argument is less than, equal to, or greater than the second.
808
         */
809
        @Override
810
        public int compare(ApiResponseEntry o1, ApiResponseEntry o2) {
811
            if (descending) {
×
812
                return o2.get(field).compareTo(o1.get(field));
×
813
            } else {
814
                return o1.get(field).compareTo(o2.get(field));
×
815
            }
816
        }
817

818
    }
819

820
    /**
821
     * MIME type for TriG RDF format.
822
     */
823
    public static final String TYPE_TRIG = "application/trig";
824

825
    /**
826
     * MIME type for Jelly RDF format.
827
     */
828
    public static final String TYPE_JELLY = "application/x-jelly-rdf";
829

830
    /**
831
     * MIME type for JSON-LD format.
832
     */
833
    public static final String TYPE_JSONLD = "application/ld+json";
834

835
    /**
836
     * MIME type for N-Quads format.
837
     */
838
    public static final String TYPE_NQUADS = "application/n-quads";
839

840
    /**
841
     * MIME type for Trix format.
842
     */
843
    public static final String TYPE_TRIX = "application/trix";
844

845
    /**
846
     * MIME type for HTML format.
847
     */
848
    public static final String TYPE_HTML = "text/html";
849

850
    /**
851
     * Comma-separated list of supported MIME types for nanopublications.
852
     */
853
    public static final String SUPPORTED_TYPES = TYPE_TRIG + "," + TYPE_JELLY + "," + TYPE_JSONLD + "," + TYPE_NQUADS + "," + TYPE_TRIX + "," + TYPE_HTML;
854

855
    /**
856
     * List of supported MIME types for nanopublications.
857
     */
858
    public static final List<String> SUPPORTED_TYPES_LIST = Arrays.asList(StringUtils.split(SUPPORTED_TYPES, ','));
18✔
859

860
    private static volatile String resolvedMainRegistryUrl;
861
    private static volatile String resolvedMainQueryUrl;
862

863
    /**
864
     * Eagerly resolves the main registry and query URLs. Call at application startup
865
     * so the (potentially slow) first-time discovery does not happen during a user request.
866
     */
867
    public static void initMainUrls() {
868
        getMainRegistryUrl();
6✔
869
        getMainQueryUrl();
6✔
870
    }
3✔
871

872
    /**
873
     * Returns the URL of the main Nanopub Registry for this nanodash instance.
874
     * <p>
875
     * If {@code NANODASH_MAIN_REGISTRY} is set and matches an entry in the library's
876
     * discovered registry instance list, that URL is used. Otherwise the first entry
877
     * of the library list is used. If the library list is empty, the env var value
878
     * (or built-in default) is used unvalidated. The result is cached for the JVM lifetime.
879
     *
880
     * @return Nanopub Registry URL (with trailing slash)
881
     */
882
    public static String getMainRegistryUrl() {
883
        if (resolvedMainRegistryUrl == null) {
6✔
884
            synchronized (Utils.class) {
12✔
885
                if (resolvedMainRegistryUrl == null) {
6!
886
                    resolvedMainRegistryUrl = resolveMainRegistryUrl();
6✔
887
                }
888
            }
9✔
889
        }
890
        return resolvedMainRegistryUrl;
6✔
891
    }
892

893
    /**
894
     * Returns the URL of the main Nanopub Query API for this nanodash instance.
895
     * <p>
896
     * If {@code NANODASH_MAIN_QUERY} is set and matches an entry in the library's
897
     * discovered query instance list, that URL is used. Otherwise the first entry
898
     * of the library list is used. If the library list is empty, the env var value
899
     * (or built-in default) is used unvalidated. The result is cached for the JVM lifetime.
900
     *
901
     * @return Nanopub Query URL (with trailing slash)
902
     */
903
    public static String getMainQueryUrl() {
904
        if (resolvedMainQueryUrl == null) {
6✔
905
            synchronized (Utils.class) {
12✔
906
                if (resolvedMainQueryUrl == null) {
6!
907
                    resolvedMainQueryUrl = resolveMainQueryUrl();
6✔
908
                }
909
            }
9✔
910
        }
911
        return resolvedMainQueryUrl;
6✔
912
    }
913

914
    private static String resolveMainRegistryUrl() {
915
        String envValue = trimToNull(System.getenv("NANODASH_MAIN_REGISTRY"));
12✔
916
        List<String> instances;
917
        try {
918
            instances = NanopubServerUtils.getRegistryServerList();
6✔
919
        } catch (Exception ex) {
×
920
            logger.warn("Could not retrieve registry instance list from nanopub library: {}", ex.toString());
×
921
            instances = Collections.emptyList();
×
922
        }
3✔
923
        return resolveMainUrl("NANODASH_MAIN_REGISTRY", envValue, instances, DEFAULT_MAIN_REGISTRY_URL);
18✔
924
    }
925

926
    private static String resolveMainQueryUrl() {
927
        String envValue = trimToNull(System.getenv("NANODASH_MAIN_QUERY"));
12✔
928
        List<String> instances;
929
        try {
930
            instances = QueryCall.getApiInstances();
6✔
931
        } catch (NotEnoughAPIInstancesException ex) {
×
932
            logger.warn("Nanopub library reports not enough query API instances available: {}", ex.toString());
×
933
            instances = Collections.emptyList();
×
934
        } catch (Exception ex) {
×
935
            logger.warn("Could not retrieve query instance list from nanopub library: {}", ex.toString());
×
936
            instances = Collections.emptyList();
×
937
        }
3✔
938
        return resolveMainUrl("NANODASH_MAIN_QUERY", envValue, instances, DEFAULT_MAIN_QUERY_URL);
18✔
939
    }
940

941
    private static String resolveMainUrl(String envVarName, String envValue, List<String> instances, String builtInDefault) {
942
        if (envValue != null) {
6!
943
            if (containsNormalized(instances, envValue)) {
×
944
                logger.info("Using main URL from {} (validated against library instance list): {}", envVarName, envValue);
×
945
                return ensureTrailingSlash(envValue);
×
946
            }
947
            if (instances.isEmpty()) {
×
948
                logger.warn("Library instance list is empty; using {} unvalidated: {}", envVarName, envValue);
×
949
                return ensureTrailingSlash(envValue);
×
950
            }
951
            logger.warn("{}={} is not in the library instance list {}; falling back to first library instance", envVarName, envValue, instances);
×
952
            return ensureTrailingSlash(instances.get(0));
×
953
        }
954
        if (!instances.isEmpty()) {
9!
955
            String first = instances.get(0);
15✔
956
            logger.info("{} not set; using first library instance: {}", envVarName, first);
15✔
957
            return ensureTrailingSlash(first);
9✔
958
        }
959
        logger.warn("{} not set and library instance list is empty; using built-in default: {}", envVarName, builtInDefault);
×
960
        return builtInDefault;
×
961
    }
962

963
    private static boolean containsNormalized(List<String> urls, String target) {
964
        String normTarget = normalizeUrl(target);
×
965
        for (String url : urls) {
×
966
            if (normalizeUrl(url).equals(normTarget)) {
×
967
                return true;
×
968
            }
969
        }
×
970
        return false;
×
971
    }
972

973
    private static String normalizeUrl(String url) {
974
        if (url == null) {
×
975
            return "";
×
976
        }
977
        return url.trim().replaceFirst("/+$", "").toLowerCase(Locale.ROOT);
×
978
    }
979

980
    private static String ensureTrailingSlash(String url) {
981
        return url.endsWith("/") ? url : url + "/";
21!
982
    }
983

984
    private static String trimToNull(String s) {
985
        if (s == null) {
6!
986
            return null;
6✔
987
        }
988
        s = s.trim();
×
989
        return s.isEmpty() ? null : s;
×
990
    }
991

992
    private static final String PLAIN_LITERAL_PATTERN = "^\"(([^\\\\\\\"]|\\\\\\\\|\\\\\")*)\"";
993
    private static final String LANGTAG_LITERAL_PATTERN = "^\"(([^\\\\\\\"]|\\\\\\\\|\\\\\")*)\"@([0-9a-zA-Z-]{2,})$";
994
    private static final String DATATYPE_LITERAL_PATTERN = "^\"(([^\\\\\\\"]|\\\\\\\\|\\\\\")*)\"\\^\\^<([^ ><\"^]+)>";
995

996
    /**
997
     * Checks whether string is valid literal serialization.
998
     *
999
     * @param literalString the literal string
1000
     * @return true if valid
1001
     */
1002
    public static boolean isValidLiteralSerialization(String literalString) {
1003
        if (literalString.matches(PLAIN_LITERAL_PATTERN)) {
12✔
1004
            return true;
6✔
1005
        } else if (literalString.matches(LANGTAG_LITERAL_PATTERN)) {
12✔
1006
            return true;
6✔
1007
        } else if (literalString.matches(DATATYPE_LITERAL_PATTERN)) {
12✔
1008
            return true;
6✔
1009
        }
1010
        return false;
6✔
1011
    }
1012

1013
    /**
1014
     * Returns a serialized version of the literal.
1015
     *
1016
     * @param literal the literal
1017
     * @return the String serialization of the literal
1018
     */
1019
    public static String getSerializedLiteral(Literal literal) {
1020
        if (literal.getLanguage().isPresent()) {
12✔
1021
            return "\"" + getEscapedLiteralString(literal.stringValue()) + "\"@" + Literals.normalizeLanguageTag(literal.getLanguage().get());
30✔
1022
        } else if (literal.getDatatype().equals(XSD.STRING)) {
15✔
1023
            return "\"" + getEscapedLiteralString(literal.stringValue()) + "\"";
15✔
1024
        } else {
1025
            return "\"" + getEscapedLiteralString(literal.stringValue()) + "\"^^<" + literal.getDatatype() + ">";
24✔
1026
        }
1027
    }
1028

1029
    /**
1030
     * Parses a serialized literal into a Literal object.
1031
     *
1032
     * @param serializedLiteral The serialized String of the literal
1033
     * @return The parse Literal object
1034
     */
1035
    public static Literal getParsedLiteral(String serializedLiteral) {
1036
        if (serializedLiteral.matches(PLAIN_LITERAL_PATTERN)) {
12✔
1037
            return vf.createLiteral(getUnescapedLiteralString(serializedLiteral.replaceFirst(PLAIN_LITERAL_PATTERN, "$1")));
24✔
1038
        } else if (serializedLiteral.matches(LANGTAG_LITERAL_PATTERN)) {
12✔
1039
            String langtag = serializedLiteral.replaceFirst(LANGTAG_LITERAL_PATTERN, "$3");
15✔
1040
            return vf.createLiteral(getUnescapedLiteralString(serializedLiteral.replaceFirst(LANGTAG_LITERAL_PATTERN, "$1")), langtag);
27✔
1041
        } else if (serializedLiteral.matches(DATATYPE_LITERAL_PATTERN)) {
12✔
1042
            IRI datatype = vf.createIRI(serializedLiteral.replaceFirst(DATATYPE_LITERAL_PATTERN, "$3"));
21✔
1043
            return vf.createLiteral(getUnescapedLiteralString(serializedLiteral.replaceFirst(DATATYPE_LITERAL_PATTERN, "$1")), datatype);
27✔
1044
        }
1045
        throw new IllegalArgumentException("Not a valid literal serialization: " + serializedLiteral);
18✔
1046
    }
1047

1048
    /**
1049
     * Escapes quotes (") and slashes (/) of a literal string.
1050
     *
1051
     * @param unescapedString un-escaped string
1052
     * @return escaped string
1053
     */
1054
    public static String getEscapedLiteralString(String unescapedString) {
1055
        return unescapedString.replaceAll("\\\\", "\\\\\\\\").replaceAll("\"", "\\\"");
24✔
1056
    }
1057

1058
    /**
1059
     * Un-escapes quotes (") and slashes (/) of a literal string.
1060
     *
1061
     * @param escapedString escaped string
1062
     * @return un-escaped string
1063
     */
1064
    public static String getUnescapedLiteralString(String escapedString) {
1065
        return escapedString.replaceAll("\\\\(\\\\|\\\")", "$1");
15✔
1066
    }
1067

1068
    /**
1069
     * Checks if a given IRI is a local URI.
1070
     *
1071
     * @param uri the IRI to check
1072
     * @return true if the IRI is a local URI, false otherwise
1073
     */
1074
    public static boolean isLocalURI(IRI uri) {
1075
        return uri != null && isLocalURI(uri.stringValue());
30✔
1076
    }
1077

1078
    /**
1079
     * Checks if a given string is a local URI.
1080
     *
1081
     * @param uriAsString the string to check
1082
     * @return true if the string is a local URI, false otherwise
1083
     */
1084
    public static boolean isLocalURI(String uriAsString) {
1085
        return !uriAsString.isBlank() && uriAsString.startsWith(LocalUri.PREFIX);
33✔
1086
    }
1087

1088
    public static String unescapeMultiValue(String s) {
1089
        StringBuilder sb = new StringBuilder();
12✔
1090
        for (int i = 0; i < s.length(); i++) {
24✔
1091
            if (s.charAt(i) == '\\' && i + 1 < s.length()) {
33✔
1092
                char next = s.charAt(i + 1);
18✔
1093
                if (next == 'n') {
9✔
1094
                    sb.append('\n');
15✔
1095
                } else if (next == '\\') {
9!
1096
                    sb.append('\\');
15✔
1097
                } else {
1098
                    sb.append(next);
×
1099
                }
1100
                i++;
3✔
1101
            } else {
3✔
1102
                sb.append(s.charAt(i));
18✔
1103
            }
1104
        }
1105
        return sb.toString();
9✔
1106
    }
1107

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