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

knowledgepixels / nanodash / 28936601485

08 Jul 2026 10:43AM UTC coverage: 28.314% (+0.2%) from 28.115%
28936601485

Pull #545

github

web-flow
Merge 2842605b1 into 3cb376d20
Pull Request #545: feat: identify templates by embedded IRIs with dual-mode parser

1838 of 7333 branches covered (25.06%)

Branch coverage included in aggregate %.

3749 of 12399 relevant lines covered (30.24%)

4.5 hits per line

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

61.63
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.util.*;
48
import java.util.regex.Pattern;
49

50
import static java.nio.charset.StandardCharsets.UTF_8;
51

52
/**
53
 * Utility class providing various helper methods for handling nanopublications, URIs, and other related functionalities.
54
 */
55
public class Utils {
56

57
    private Utils() {
58
    }  // no instances allowed
59

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

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

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

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

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

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

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

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

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

169
    public static String truncateLabel(String label) {
170
        if (label != null && label.length() > 120) {
×
171
            return label.substring(0, 100) + "...";
×
172
        }
173
        return label;
×
174
    }
175

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

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

221
    /**
222
     * URL-decodes the string representation of the given object using UTF-8 encoding.
223
     *
224
     * @param o the object to be URL-decoded
225
     * @return the URL-decoded string
226
     */
227
    public static String urlDecode(Object o) {
228
        return URLDecoder.decode((o == null ? "" : o.toString()), Charsets.UTF_8);
27✔
229
    }
230

231
    /**
232
     * Generates a URL with the given base and appends the provided PageParameters as query parameters.
233
     *
234
     * @param base       the base URL
235
     * @param parameters the PageParameters to append
236
     * @return the complete URL with parameters
237
     */
238
    public static String getUrlWithParameters(String base, PageParameters parameters) {
239
        try {
240
            URIBuilder u = new URIBuilder(base);
15✔
241
            for (String key : parameters.getNamedKeys()) {
33✔
242
                for (StringValue value : parameters.getValues(key)) {
36✔
243
                    if (!value.isNull()) u.addParameter(key, value.toString());
27!
244
                }
3✔
245
            }
3✔
246
            return u.build().toString();
12✔
247
        } catch (URISyntaxException ex) {
3✔
248
            logger.error("Could not build URL with parameters: {} {}", base, parameters, ex);
51✔
249
            return "/";
6✔
250
        }
251
    }
252

253
    /**
254
     * Generates a short name for a public key or public key hash.
255
     *
256
     * @param pubkeyOrPubkeyhash the public key (64 characters) or public key hash (40 characters)
257
     * @return a short representation of the public key or public key hash
258
     */
259
    public static String getShortPubkeyName(String pubkeyOrPubkeyhash) {
260
        if (pubkeyOrPubkeyhash.length() == 64) {
12!
261
            return pubkeyOrPubkeyhash.replaceFirst("^(.{8}).*$", "$1");
×
262
        } else {
263
            return pubkeyOrPubkeyhash.replaceFirst("^(.).{39}(.{5}).*$", "$1..$2..");
15✔
264
        }
265
    }
266

267
    /**
268
     * Generates a short label for a public key or public key hash, including its status (local or approved).
269
     *
270
     * @param pubkeyOrPubkeyhash the public key (64 characters) or public key hash (40 characters)
271
     * @param user               the IRI of the user associated with the public key
272
     * @return a short label indicating the public key and its status
273
     */
274
    public static String getShortPubkeyhashLabel(String pubkeyOrPubkeyhash, IRI user) {
275
        String s = getShortPubkeyName(pubkeyOrPubkeyhash);
×
276
        NanodashSession session = NanodashSession.get();
×
277
        List<String> l = new ArrayList<>();
×
278
        if (pubkeyOrPubkeyhash.equals(session.getPubkeyString()) || pubkeyOrPubkeyhash.equals(session.getPubkeyhash()))
×
279
            l.add("local");
×
280
        // TODO: Make this more efficient:
281
        String hashed = Utils.createSha256HexHash(pubkeyOrPubkeyhash);
×
282
        if (User.getPubkeyhashes(user, true).contains(pubkeyOrPubkeyhash) || User.getPubkeyhashes(user, true).contains(hashed))
×
283
            l.add("approved");
×
284
        if (!l.isEmpty()) s += " (" + String.join("/", l) + ")";
×
285
        return s;
×
286
    }
287

288
    /**
289
     * Retrieves the name of the public key location based on the public key.
290
     *
291
     * @param pubkeyhash the public key string
292
     * @return the name of the public key location
293
     */
294
    public static String getPubkeyLocationName(String pubkeyhash) {
295
        return getPubkeyLocationName(pubkeyhash, getShortPubkeyName(pubkeyhash));
×
296
    }
297

298
    /**
299
     * Retrieves the name of the public key location, or returns a fallback name if not found.
300
     * If the key location is localhost, it returns "localhost".
301
     *
302
     * @param pubkeyhash the public key string
303
     * @param fallback   the fallback name to return if the key location is not found
304
     * @return the name of the public key location or the fallback name
305
     */
306
    public static String getPubkeyLocationName(String pubkeyhash, String fallback) {
307
        IRI keyLocation = User.getUserData().getKeyLocationForPubkeyHash(pubkeyhash);
×
308
        if (keyLocation == null) return fallback;
×
309
        if (keyLocation.stringValue().equals("http://localhost:37373/")) return "localhost";
×
310
        return keyLocation.stringValue().replaceFirst("https?://(nanobench\\.)?(nanodash\\.(?=.*\\..))?(.*[^/])/?$", "$3");
×
311
    }
312

313
    /**
314
     * Generates a short label for a public key location, including its status (local or approved).
315
     *
316
     * @param pubkeyhash the public key string
317
     * @param user       the IRI of the user associated with the public key
318
     * @return a short label indicating the public key location and its status
319
     */
320
    public static String getShortPubkeyLocationLabel(String pubkeyhash, IRI user) {
321
        String s = getPubkeyLocationName(pubkeyhash);
×
322
        NanodashSession session = NanodashSession.get();
×
323
        List<String> l = new ArrayList<>();
×
324
        if (pubkeyhash.equals(session.getPubkeyhash())) l.add("local");
×
325
        // TODO: Make this more efficient:
326
        if (User.getPubkeyhashes(user, true).contains(pubkeyhash)) l.add("approved");
×
327
        if (!l.isEmpty()) s += " (" + String.join("/", l) + ")";
×
328
        return s;
×
329
    }
330

331
    /**
332
     * Checks if a given public key has a Nanodash location.
333
     * A Nanodash location is identified by specific keywords in the key location.
334
     *
335
     * @param pubkeyhash the public key to check
336
     * @return true if the public key has a Nanodash location, false otherwise
337
     */
338
    public static boolean hasNanodashLocation(String pubkeyhash) {
339
        IRI keyLocation = User.getUserData().getKeyLocationForPubkeyHash(pubkeyhash);
×
340
        if (keyLocation == null) return true; // potentially a Nanodash location
×
341
        if (keyLocation.stringValue().contains("nanodash")) return true;
×
342
        if (keyLocation.stringValue().contains("nanobench")) return true;
×
343
        if (keyLocation.stringValue().contains(":37373")) return true;
×
344
        return false;
×
345
    }
346

347
    /**
348
     * Retrieves the short ORCID ID from an IRI object.
349
     *
350
     * @param orcidIri the IRI object representing the ORCID ID
351
     * @return the short ORCID ID as a string
352
     */
353
    public static String getShortOrcidId(IRI orcidIri) {
354
        return orcidIri.stringValue().replaceFirst("^https://orcid.org/", "");
18✔
355
    }
356

357
    /**
358
     * Retrieves the URI postfix from a given URI object.
359
     *
360
     * @param uri the URI object from which to extract the postfix
361
     * @return the URI postfix as a string
362
     */
363
    public static String getUriPostfix(Object uri) {
364
        String s = uri.toString();
9✔
365
        if (s.contains("#")) return s.replaceFirst("^.*#(.*)$", "$1");
27✔
366
        return s.replaceFirst("^.*/(.*)$", "$1");
15✔
367
    }
368

369
    /**
370
     * Retrieves the URI prefix from a given URI object.
371
     *
372
     * @param uri the URI object from which to extract the prefix
373
     * @return the URI prefix as a string
374
     */
375
    public static String getUriPrefix(Object uri) {
376
        String s = uri.toString();
9✔
377
        if (s.contains("#")) return s.replaceFirst("^(.*#).*$", "$1");
27✔
378
        return s.replaceFirst("^(.*/).*$", "$1");
15✔
379
    }
380

381
    /**
382
     * Checks if a given string is a valid URI postfix.
383
     * A valid URI postfix does not contain a colon (":").
384
     *
385
     * @param s the string to check
386
     * @return true if the string is a valid URI postfix, false otherwise
387
     */
388
    public static boolean isUriPostfix(String s) {
389
        return !s.contains(":");
24✔
390
    }
391

392
    /**
393
     * Retrieves the location of a given IntroNanopub.
394
     *
395
     * @param inp the IntroNanopub from which to extract the location
396
     * @return the IRI location of the nanopublication, or null if not found
397
     */
398
    public static IRI getLocation(IntroNanopub inp) {
399
        NanopubSignatureElement el = getNanopubSignatureElement(inp);
×
400
        for (KeyDeclaration kd : inp.getKeyDeclarations()) {
×
401
            if (el.getPublicKeyString().equals(kd.getPublicKeyString())) {
×
402
                return kd.getKeyLocation();
×
403
            }
404
        }
×
405
        return null;
×
406
    }
407

408
    /**
409
     * Retrieves the NanopubSignatureElement from a given IntroNanopub.
410
     *
411
     * @param inp the IntroNanopub from which to extract the signature element
412
     * @return the NanopubSignatureElement associated with the nanopublication
413
     */
414
    public static NanopubSignatureElement getNanopubSignatureElement(IntroNanopub inp) {
415
        try {
416
            return SignatureUtils.getSignatureElement(inp.getNanopub());
×
417
        } catch (MalformedCryptoElementException ex) {
×
418
            throw new RuntimeException(ex);
×
419
        }
420
    }
421

422
    /**
423
     * Retrieves a Nanopub object from a given URI if it is a potential Trusty URI.
424
     *
425
     * @param uri the URI to check and retrieve the Nanopub from
426
     * @return the Nanopub object if found, or null if not a known nanopublication
427
     */
428
    public static Nanopub getAsNanopub(String uri) {
429
        if (uri == null) return null;
6!
430
        if (TrustyUriUtils.isPotentialTrustyUri(uri)) {
9!
431
            try {
432
                return Utils.getNanopub(uri);
9✔
433
            } catch (Exception ex) {
×
434
                logger.error("The given URI is not a known nanopublication: {}", uri, ex);
×
435
            }
436
        }
437
        return null;
×
438
    }
439

440
    private static final PolicyFactory htmlSanitizePolicy = new HtmlPolicyBuilder()
9✔
441
            .allowCommonBlockElements()
3✔
442
            .allowCommonInlineFormattingElements()
45✔
443
            .allowUrlProtocols("https", "http", "mailto")
21✔
444
            .allowElements("a")
21✔
445
            .allowAttributes("href").onElements("a")
42✔
446
            .allowElements("img")
21✔
447
            .allowAttributes("src").onElements("img")
42✔
448
            .allowElements("pre")
3✔
449
            .requireRelNofollowOnLinks()
3✔
450
            .toFactory();
6✔
451

452
    /**
453
     * Sanitizes raw HTML input to ensure safe rendering.
454
     *
455
     * @param rawHtml the raw HTML input to sanitize
456
     * @return sanitized HTML string
457
     */
458
    public static String sanitizeHtml(String rawHtml) {
459
        return htmlSanitizePolicy.sanitize(rawHtml);
12✔
460
    }
461

462
    /**
463
     * Checks if a given string is likely to be HTML content.
464
     *
465
     * @param value the string to check
466
     * @return true if the given string is HTML content, false otherwise
467
     */
468
    public static boolean looksLikeHtml(String value) {
469
        return LEADING_TAG.matcher(value).find();
15✔
470
    }
471

472
    /**
473
     * Matches an xsd:dateTime-style literal with a time component, e.g.
474
     * "2026-04-16T08:27:12.954Z" or "2026-04-16T08:27:12+02:00".
475
     */
476
    private static final Pattern DATETIME_LITERAL =
3✔
477
            Pattern.compile("^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(\\.\\d+)?(Z|[+-]\\d{2}:\\d{2})?$");
6✔
478

479
    /**
480
     * Checks whether a (raw query-result) string looks like an ISO-8601 date-time literal.
481
     *
482
     * @param value the string to check
483
     * @return true if the string parses as an xsd:dateTime-style value
484
     */
485
    public static boolean isDateTimeLiteral(String value) {
486
        return value != null && DATETIME_LITERAL.matcher(value).matches();
×
487
    }
488

489
    /**
490
     * Renders a {@code <time>} element for an ISO-8601 date-time value. The machine-readable
491
     * value goes in the {@code datetime} attribute; client-side script (nanodash.js) rewrites
492
     * the visible text to a relative form ("10 minutes ago") in the viewer's local timezone and
493
     * puts the absolute date-time in the tooltip. If script does not run, {@code fallbackText}
494
     * remains visible.
495
     *
496
     * @param isoValue     the ISO-8601 date-time string (machine-readable)
497
     * @param fallbackText the human-readable text shown when script is unavailable
498
     * @return an HTML {@code <time>} element string (caller must render with escaping disabled)
499
     */
500
    public static String friendlyDateHtml(String isoValue, String fallbackText) {
501
        return "<time class=\"friendly-date\" datetime=\"" + Strings.escapeMarkup(isoValue) + "\">"
×
502
                + Strings.escapeMarkup(fallbackText) + "</time>";
×
503
    }
504

505
    /**
506
     * Converts PageParameters to a URL-encoded string representation.
507
     *
508
     * @param params the PageParameters to convert
509
     * @return a string representation of the parameters in URL-encoded format
510
     */
511
    public static String getPageParametersAsString(PageParameters params) {
512
        String s = "";
6✔
513
        for (String n : params.getNamedKeys()) {
33✔
514
            if (!s.isEmpty()) s += "&";
18✔
515
            s += n + "=" + URLEncoder.encode(params.get(n).toString(), Charsets.UTF_8);
30✔
516
        }
3✔
517
        return s;
6✔
518
    }
519

520
    /**
521
     * Sets a minimal escape markup function for a Select2Choice component.
522
     * This function replaces certain characters and formats the display of choices.
523
     *
524
     * @param selectItem the Select2Choice component to set the escape markup for
525
     */
526
    public static void setSelect2ChoiceMinimalEscapeMarkup(Select2Choice<?> selectItem) {
527
        selectItem.getSettings().setEscapeMarkup("function(markup) {" +
15✔
528
                                                 "return markup" +
529
                                                 ".replaceAll('<','&lt;').replaceAll('>', '&gt;')" +
530
                                                 ".replace(/^(.*?) - /, '<span class=\"term\">$1</span><br>')" +
531
                                                 ".replace(/\\((https?:[\\S]+)\\)$/, '<br><code>$1</code>')" +
532
                                                 ".replace(/^([^<].*)$/, '<span class=\"term\">$1</span>')" +
533
                                                 ";}"
534
        );
535
    }
3✔
536

537
    /**
538
     * Checks if a nanopublication is of a specific class.
539
     *
540
     * @param np       the nanopublication to check
541
     * @param classIri the IRI of the class to check against
542
     * @return true if the nanopublication is of the specified class, false otherwise
543
     */
544
    public static boolean isNanopubOfClass(Nanopub np, IRI classIri) {
545
        return NanopubUtils.getTypes(np).contains(classIri);
15✔
546
    }
547

548
    /**
549
     * Checks if a nanopublication uses a specific predicate in its assertion.
550
     *
551
     * @param np           the nanopublication to check
552
     * @param predicateIri the IRI of the predicate to look for
553
     * @return true if the predicate is used in the assertion, false otherwise
554
     */
555
    public static boolean usesPredicateInAssertion(Nanopub np, IRI predicateIri) {
556
        for (Statement st : np.getAssertion()) {
33✔
557
            if (predicateIri.equals(st.getPredicate())) {
15✔
558
                return true;
6✔
559
            }
560
        }
3✔
561
        return false;
6✔
562
    }
563

564
    /**
565
     * Retrieves a map of FOAF names from the nanopublication's pubinfo.
566
     *
567
     * @param np the nanopublication from which to extract FOAF names
568
     * @return a map where keys are subjects and values are FOAF names
569
     */
570
    public static Map<String, String> getFoafNameMap(Nanopub np) {
571
        Map<String, String> foafNameMap = new HashMap<>();
12✔
572
        for (Statement st : np.getPubinfo()) {
33✔
573
            if (st.getPredicate().equals(FOAF.NAME) && st.getObject() instanceof Literal objL) {
42✔
574
                foafNameMap.put(st.getSubject().stringValue(), objL.stringValue());
24✔
575
            }
576
        }
3✔
577
        return foafNameMap;
6✔
578
    }
579

580
    /**
581
     * Creates an SHA-256 hash of the string representation of an object and returns it as a hexadecimal string.
582
     *
583
     * @param obj the object to hash
584
     * @return the SHA-256 hash of the object's string representation in hexadecimal format
585
     */
586
    public static String createSha256HexHash(Object obj) {
587
        return Hashing.sha256().hashString(obj.toString(), StandardCharsets.UTF_8).toString();
21✔
588
    }
589

590
    /**
591
     * Gets the types of a nanopublication.
592
     *
593
     * @param np the nanopublication from which to extract types
594
     * @return a list of IRI types associated with the nanopublication
595
     */
596
    public static List<IRI> getTypes(Nanopub np) {
597
        List<IRI> l = new ArrayList<>();
12✔
598
        for (IRI t : NanopubUtils.getTypes(np)) {
33✔
599
            if (t.equals(FIP.AVAILABLE_FAIR_ENABLING_RESOURCE)) continue;
15✔
600
            if (t.equals(FIP.FAIR_ENABLING_RESOURCE_TO_BE_DEVELOPED))
12✔
601
                continue;
3✔
602
            if (t.equals(FIP.AVAILABLE_FAIR_SUPPORTING_RESOURCE)) continue;
12!
603
            if (t.equals(FIP.FAIR_SUPPORTING_RESOURCE_TO_BE_DEVELOPED))
12!
604
                continue;
×
605
            l.add(t);
12✔
606
        }
3✔
607
        return l;
6✔
608
    }
609

610
    /**
611
     * Gets a label for a type IRI.
612
     *
613
     * @param typeIri the IRI of the type
614
     * @return a label for the type, potentially truncated
615
     */
616
    public static String getTypeLabel(IRI typeIri) {
617
        if (typeIri.equals(FIP.FAIR_ENABLING_RESOURCE)) return "FER";
18✔
618
        if (typeIri.equals(FIP.FAIR_SUPPORTING_RESOURCE)) return "FSR";
18✔
619
        if (typeIri.equals(FIP.FAIR_IMPLEMENTATION_PROFILE)) return "FIP";
18✔
620
        if (typeIri.equals(NPX.DECLARED_BY)) return "user intro";
18✔
621
        String l = typeIri.stringValue();
9✔
622
        l = l.replaceFirst("^.*[/#]([^/#]+)[/#]?$", "$1");
15✔
623
        l = l.replaceFirst("^(.+)Nanopub$", "$1");
15✔
624
        if (l.length() > 25) l = l.substring(0, 20) + "...";
30✔
625
        return l;
6✔
626
    }
627

628
    /**
629
     * Gets a label for a URI.
630
     *
631
     * @param uri the URI to get the label from
632
     * @return a label for the URI, potentially truncated
633
     */
634
    public static String getUriLabel(String uri) {
635
        if (uri == null) return "";
12✔
636
        String uriLabel = uri;
6✔
637
        if (uriLabel.matches(".*[^A-Za-z0-9-_]RA[A-Za-z0-9-_]{43}([^A-Za-z0-9-_].*)?")) {
12✔
638
            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✔
639
            if (newUriLabel.length() <= 70) return newUriLabel;
18!
640
        }
641
        if (uriLabel.length() > 70) return uri.substring(0, 30) + "..." + uri.substring(uri.length() - 30);
48✔
642
        return uriLabel;
6✔
643
    }
644

645
    /**
646
     * Gets an ExternalLink with a URI label.
647
     *
648
     * @param markupId the markup ID for the link
649
     * @param uri      the URI to link to
650
     * @return an ExternalLink with the URI label
651
     */
652
    public static ExternalLink getUriLink(String markupId, String uri) {
653
        return new ExternalLink(markupId, (Utils.isLocalURI(uri) ? "" : uri), getUriLabel(uri));
39✔
654
    }
655

656
    /**
657
     * Gets an ExternalLink with a model for the URI label.
658
     *
659
     * @param markupId the markup ID for the link
660
     * @param model    the model containing the URI
661
     * @return an ExternalLink with the URI label
662
     */
663
    public static ExternalLink getUriLink(String markupId, IModel<String> model) {
664
        return new ExternalLink(markupId, model, new UriLabelModel(model));
30✔
665
    }
666

667
    private static class UriLabelModel implements IModel<String> {
668

669
        private IModel<String> uriModel;
670

671
        public UriLabelModel(IModel<String> uriModel) {
6✔
672
            this.uriModel = uriModel;
9✔
673
        }
3✔
674

675
        @Override
676
        public String getObject() {
677
            return getUriLabel(uriModel.getObject());
×
678
        }
679

680
    }
681

682
    /**
683
     * Creates a sublist from a list based on the specified indices.
684
     *
685
     * @param list      the list from which to create the sublist
686
     * @param fromIndex the starting index (inclusive) for the sublist
687
     * @param toIndex   the ending index (exclusive) for the sublist
688
     * @param <E>       the type of elements in the list
689
     * @return an ArrayList containing the elements from the specified range
690
     */
691
    public static <E> ArrayList<E> subList(List<E> list, long fromIndex, long toIndex) {
692
        // So the resulting list is serializable:
693
        return new ArrayList<E>(list.subList((int) fromIndex, (int) toIndex));
×
694
    }
695

696
    /**
697
     * Creates a sublist from an array based on the specified indices.
698
     *
699
     * @param array     the array from which to create the sublist
700
     * @param fromIndex the starting index (inclusive) for the sublist
701
     * @param toIndex   the ending index (exclusive) for the sublist
702
     * @param <E>       the type of elements in the array
703
     * @return an ArrayList containing the elements from the specified range
704
     */
705
    public static <E> ArrayList<E> subList(E[] array, long fromIndex, long toIndex) {
706
        return subList(Arrays.asList(array), fromIndex, toIndex);
×
707
    }
708

709
    /**
710
     * Comparator for sorting ApiResponseEntry objects based on a specified field.
711
     */
712
    // TODO Move this to ApiResponseEntry class?
713
    public static class ApiResponseEntrySorter implements Comparator<ApiResponseEntry>, Serializable {
714

715
        private String field;
716
        private boolean descending;
717

718
        /**
719
         * Constructor for ApiResponseEntrySorter.
720
         *
721
         * @param field      the field to sort by
722
         * @param descending if true, sorts in descending order; if false, sorts in ascending order
723
         */
724
        public ApiResponseEntrySorter(String field, boolean descending) {
×
725
            this.field = field;
×
726
            this.descending = descending;
×
727
        }
×
728

729
        /**
730
         * Compares two ApiResponseEntry objects based on the specified field.
731
         *
732
         * @param o1 the first object to be compared.
733
         * @param o2 the second object to be compared.
734
         * @return a negative integer, zero, or a positive integer as the first argument is less than, equal to, or greater than the second.
735
         */
736
        @Override
737
        public int compare(ApiResponseEntry o1, ApiResponseEntry o2) {
738
            if (descending) {
×
739
                return o2.get(field).compareTo(o1.get(field));
×
740
            } else {
741
                return o1.get(field).compareTo(o2.get(field));
×
742
            }
743
        }
744

745
    }
746

747
    /**
748
     * MIME type for TriG RDF format.
749
     */
750
    public static final String TYPE_TRIG = "application/trig";
751

752
    /**
753
     * MIME type for Jelly RDF format.
754
     */
755
    public static final String TYPE_JELLY = "application/x-jelly-rdf";
756

757
    /**
758
     * MIME type for JSON-LD format.
759
     */
760
    public static final String TYPE_JSONLD = "application/ld+json";
761

762
    /**
763
     * MIME type for N-Quads format.
764
     */
765
    public static final String TYPE_NQUADS = "application/n-quads";
766

767
    /**
768
     * MIME type for Trix format.
769
     */
770
    public static final String TYPE_TRIX = "application/trix";
771

772
    /**
773
     * MIME type for HTML format.
774
     */
775
    public static final String TYPE_HTML = "text/html";
776

777
    /**
778
     * Comma-separated list of supported MIME types for nanopublications.
779
     */
780
    public static final String SUPPORTED_TYPES =
781
            TYPE_TRIG + "," +
782
            TYPE_JELLY + "," +
783
            TYPE_JSONLD + "," +
784
            TYPE_NQUADS + "," +
785
            TYPE_TRIX + "," +
786
            TYPE_HTML;
787

788
    /**
789
     * List of supported MIME types for nanopublications.
790
     */
791
    public static final List<String> SUPPORTED_TYPES_LIST = Arrays.asList(StringUtils.split(SUPPORTED_TYPES, ','));
18✔
792

793
    private static volatile String resolvedMainRegistryUrl;
794
    private static volatile String resolvedMainQueryUrl;
795

796
    /**
797
     * Eagerly resolves the main registry and query URLs. Call at application startup
798
     * so the (potentially slow) first-time discovery does not happen during a user request.
799
     */
800
    public static void initMainUrls() {
801
        getMainRegistryUrl();
6✔
802
        getMainQueryUrl();
6✔
803
    }
3✔
804

805
    /**
806
     * Returns the URL of the main Nanopub Registry for this nanodash instance.
807
     * <p>
808
     * If {@code NANODASH_MAIN_REGISTRY} is set and matches an entry in the library's
809
     * discovered registry instance list, that URL is used. Otherwise the first entry
810
     * of the library list is used. If the library list is empty, the env var value
811
     * (or built-in default) is used unvalidated. The result is cached for the JVM lifetime.
812
     *
813
     * @return Nanopub Registry URL (with trailing slash)
814
     */
815
    public static String getMainRegistryUrl() {
816
        if (resolvedMainRegistryUrl == null) {
6✔
817
            synchronized (Utils.class) {
12✔
818
                if (resolvedMainRegistryUrl == null) {
6!
819
                    resolvedMainRegistryUrl = resolveMainRegistryUrl();
6✔
820
                }
821
            }
9✔
822
        }
823
        return resolvedMainRegistryUrl;
6✔
824
    }
825

826
    /**
827
     * Returns the URL of the main Nanopub Query API for this nanodash instance.
828
     * <p>
829
     * If {@code NANODASH_MAIN_QUERY} is set and matches an entry in the library's
830
     * discovered query instance list, that URL is used. Otherwise the first entry
831
     * of the library list is used. If the library list is empty, the env var value
832
     * (or built-in default) is used unvalidated. The result is cached for the JVM lifetime.
833
     *
834
     * @return Nanopub Query URL (with trailing slash)
835
     */
836
    public static String getMainQueryUrl() {
837
        if (resolvedMainQueryUrl == null) {
6✔
838
            synchronized (Utils.class) {
12✔
839
                if (resolvedMainQueryUrl == null) {
6!
840
                    resolvedMainQueryUrl = resolveMainQueryUrl();
6✔
841
                }
842
            }
9✔
843
        }
844
        return resolvedMainQueryUrl;
6✔
845
    }
846

847
    private static String resolveMainRegistryUrl() {
848
        String envValue = trimToNull(System.getenv("NANODASH_MAIN_REGISTRY"));
12✔
849
        List<String> instances;
850
        try {
851
            instances = NanopubServerUtils.getRegistryServerList();
6✔
852
        } catch (Exception ex) {
×
853
            logger.warn("Could not retrieve registry instance list from nanopub library: {}", ex.toString());
×
854
            instances = Collections.emptyList();
×
855
        }
3✔
856
        return resolveMainUrl("NANODASH_MAIN_REGISTRY", envValue, instances, DEFAULT_MAIN_REGISTRY_URL);
18✔
857
    }
858

859
    private static String resolveMainQueryUrl() {
860
        String envValue = trimToNull(System.getenv("NANODASH_MAIN_QUERY"));
12✔
861
        List<String> instances;
862
        try {
863
            instances = QueryCall.getApiInstances();
6✔
864
        } catch (NotEnoughAPIInstancesException ex) {
×
865
            logger.warn("Nanopub library reports not enough query API instances available: {}", ex.toString());
×
866
            instances = Collections.emptyList();
×
867
        } catch (Exception ex) {
×
868
            logger.warn("Could not retrieve query instance list from nanopub library: {}", ex.toString());
×
869
            instances = Collections.emptyList();
×
870
        }
3✔
871
        return resolveMainUrl("NANODASH_MAIN_QUERY", envValue, instances, DEFAULT_MAIN_QUERY_URL);
18✔
872
    }
873

874
    private static String resolveMainUrl(String envVarName, String envValue, List<String> instances, String builtInDefault) {
875
        if (envValue != null) {
6!
876
            if (containsNormalized(instances, envValue)) {
×
877
                logger.info("Using main URL from {} (validated against library instance list): {}", envVarName, envValue);
×
878
                return ensureTrailingSlash(envValue);
×
879
            }
880
            if (instances.isEmpty()) {
×
881
                logger.warn("Library instance list is empty; using {} unvalidated: {}", envVarName, envValue);
×
882
                return ensureTrailingSlash(envValue);
×
883
            }
884
            logger.warn("{}={} is not in the library instance list {}; falling back to first library instance",
×
885
                    envVarName, envValue, instances);
886
            return ensureTrailingSlash(instances.get(0));
×
887
        }
888
        if (!instances.isEmpty()) {
9!
889
            String first = instances.get(0);
15✔
890
            logger.info("{} not set; using first library instance: {}", envVarName, first);
15✔
891
            return ensureTrailingSlash(first);
9✔
892
        }
893
        logger.warn("{} not set and library instance list is empty; using built-in default: {}", envVarName, builtInDefault);
×
894
        return builtInDefault;
×
895
    }
896

897
    private static boolean containsNormalized(List<String> urls, String target) {
898
        String normTarget = normalizeUrl(target);
×
899
        for (String url : urls) {
×
900
            if (normalizeUrl(url).equals(normTarget)) return true;
×
901
        }
×
902
        return false;
×
903
    }
904

905
    private static String normalizeUrl(String url) {
906
        if (url == null) return "";
×
907
        return url.trim().replaceFirst("/+$", "").toLowerCase(Locale.ROOT);
×
908
    }
909

910
    private static String ensureTrailingSlash(String url) {
911
        return url.endsWith("/") ? url : url + "/";
21!
912
    }
913

914
    private static String trimToNull(String s) {
915
        if (s == null) return null;
12!
916
        s = s.trim();
×
917
        return s.isEmpty() ? null : s;
×
918
    }
919

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

924
    /**
925
     * Checks whether string is valid literal serialization.
926
     *
927
     * @param literalString the literal string
928
     * @return true if valid
929
     */
930
    public static boolean isValidLiteralSerialization(String literalString) {
931
        if (literalString.matches(PLAIN_LITERAL_PATTERN)) {
12✔
932
            return true;
6✔
933
        } else if (literalString.matches(LANGTAG_LITERAL_PATTERN)) {
12✔
934
            return true;
6✔
935
        } else if (literalString.matches(DATATYPE_LITERAL_PATTERN)) {
12✔
936
            return true;
6✔
937
        }
938
        return false;
6✔
939
    }
940

941
    /**
942
     * Returns a serialized version of the literal.
943
     *
944
     * @param literal the literal
945
     * @return the String serialization of the literal
946
     */
947
    public static String getSerializedLiteral(Literal literal) {
948
        if (literal.getLanguage().isPresent()) {
12✔
949
            return "\"" + getEscapedLiteralString(literal.stringValue()) + "\"@" + Literals.normalizeLanguageTag(literal.getLanguage().get());
30✔
950
        } else if (literal.getDatatype().equals(XSD.STRING)) {
15✔
951
            return "\"" + getEscapedLiteralString(literal.stringValue()) + "\"";
15✔
952
        } else {
953
            return "\"" + getEscapedLiteralString(literal.stringValue()) + "\"^^<" + literal.getDatatype() + ">";
24✔
954
        }
955
    }
956

957
    /**
958
     * Parses a serialized literal into a Literal object.
959
     *
960
     * @param serializedLiteral The serialized String of the literal
961
     * @return The parse Literal object
962
     */
963
    public static Literal getParsedLiteral(String serializedLiteral) {
964
        if (serializedLiteral.matches(PLAIN_LITERAL_PATTERN)) {
12✔
965
            return vf.createLiteral(getUnescapedLiteralString(serializedLiteral.replaceFirst(PLAIN_LITERAL_PATTERN, "$1")));
24✔
966
        } else if (serializedLiteral.matches(LANGTAG_LITERAL_PATTERN)) {
12✔
967
            String langtag = serializedLiteral.replaceFirst(LANGTAG_LITERAL_PATTERN, "$3");
15✔
968
            return vf.createLiteral(getUnescapedLiteralString(serializedLiteral.replaceFirst(LANGTAG_LITERAL_PATTERN, "$1")), langtag);
27✔
969
        } else if (serializedLiteral.matches(DATATYPE_LITERAL_PATTERN)) {
12✔
970
            IRI datatype = vf.createIRI(serializedLiteral.replaceFirst(DATATYPE_LITERAL_PATTERN, "$3"));
21✔
971
            return vf.createLiteral(getUnescapedLiteralString(serializedLiteral.replaceFirst(DATATYPE_LITERAL_PATTERN, "$1")), datatype);
27✔
972
        }
973
        throw new IllegalArgumentException("Not a valid literal serialization: " + serializedLiteral);
18✔
974
    }
975

976
    /**
977
     * Escapes quotes (") and slashes (/) of a literal string.
978
     *
979
     * @param unescapedString un-escaped string
980
     * @return escaped string
981
     */
982
    public static String getEscapedLiteralString(String unescapedString) {
983
        return unescapedString.replaceAll("\\\\", "\\\\\\\\").replaceAll("\"", "\\\"");
24✔
984
    }
985

986
    /**
987
     * Un-escapes quotes (") and slashes (/) of a literal string.
988
     *
989
     * @param escapedString escaped string
990
     * @return un-escaped string
991
     */
992
    public static String getUnescapedLiteralString(String escapedString) {
993
        return escapedString.replaceAll("\\\\(\\\\|\\\")", "$1");
15✔
994
    }
995

996
    /**
997
     * Checks if a given IRI is a local URI.
998
     *
999
     * @param uri the IRI to check
1000
     * @return true if the IRI is a local URI, false otherwise
1001
     */
1002
    public static boolean isLocalURI(IRI uri) {
1003
        return uri != null && isLocalURI(uri.stringValue());
30✔
1004
    }
1005

1006
    /**
1007
     * Checks if a given string is a local URI.
1008
     *
1009
     * @param uriAsString the string to check
1010
     * @return true if the string is a local URI, false otherwise
1011
     */
1012
    public static boolean isLocalURI(String uriAsString) {
1013
        return !uriAsString.isBlank() && uriAsString.startsWith(LocalUri.PREFIX);
33✔
1014
    }
1015

1016
    public static String unescapeMultiValue(String s) {
1017
        StringBuilder sb = new StringBuilder();
12✔
1018
        for (int i = 0; i < s.length(); i++) {
24✔
1019
            if (s.charAt(i) == '\\' && i + 1 < s.length()) {
33✔
1020
                char next = s.charAt(i + 1);
18✔
1021
                if (next == 'n') {
9✔
1022
                    sb.append('\n');
15✔
1023
                } else if (next == '\\') {
9!
1024
                    sb.append('\\');
15✔
1025
                } else {
1026
                    sb.append(next);
×
1027
                }
1028
                i++;
3✔
1029
            } else {
3✔
1030
                sb.append(s.charAt(i));
18✔
1031
            }
1032
        }
1033
        return sb.toString();
9✔
1034
    }
1035

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