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

knowledgepixels / nanodash / 25791103212

13 May 2026 09:40AM UTC coverage: 20.6% (+0.1%) from 20.478%
25791103212

push

github

web-flow
Merge pull request #461 from knowledgepixels/validate-main-urls-against-library

Validate main registry/query URLs against library instance list

1022 of 6264 branches covered (16.32%)

Branch coverage included in aggregate %.

2618 of 11406 relevant lines covered (22.95%)

3.28 hits per line

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

65.7
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.eclipse.rdf4j.model.IRI;
14
import org.eclipse.rdf4j.model.Literal;
15
import org.eclipse.rdf4j.model.Statement;
16
import org.eclipse.rdf4j.model.ValueFactory;
17
import org.eclipse.rdf4j.model.impl.SimpleValueFactory;
18
import org.eclipse.rdf4j.model.util.Literals;
19
import org.eclipse.rdf4j.model.vocabulary.FOAF;
20
import org.eclipse.rdf4j.model.vocabulary.XSD;
21
import org.nanopub.Nanopub;
22
import org.nanopub.NanopubUtils;
23
import org.nanopub.extra.security.KeyDeclaration;
24
import org.nanopub.extra.security.MalformedCryptoElementException;
25
import org.nanopub.extra.security.NanopubSignatureElement;
26
import org.nanopub.extra.security.SignatureUtils;
27
import org.nanopub.extra.server.GetNanopub;
28
import org.nanopub.extra.server.NanopubServerUtils;
29
import org.nanopub.extra.services.ApiResponseEntry;
30
import org.nanopub.extra.services.NotEnoughAPIInstancesException;
31
import org.nanopub.extra.services.QueryCall;
32
import org.nanopub.extra.setting.IntroNanopub;
33
import org.nanopub.vocabulary.FIP;
34
import org.nanopub.vocabulary.NPX;
35
import org.owasp.html.HtmlPolicyBuilder;
36
import org.owasp.html.PolicyFactory;
37
import org.slf4j.Logger;
38
import org.slf4j.LoggerFactory;
39
import org.wicketstuff.select2.Select2Choice;
40

41
import java.io.Serializable;
42
import java.net.URISyntaxException;
43
import java.net.URLDecoder;
44
import java.net.URLEncoder;
45
import java.nio.charset.StandardCharsets;
46
import java.util.*;
47
import java.util.regex.Pattern;
48

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

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

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

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

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

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

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

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

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

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

145
    /**
146
     * URL-encodes the string representation of the given object using UTF-8 encoding.
147
     *
148
     * @param o the object to be URL-encoded
149
     * @return the URL-encoded string
150
     */
151
    public static String urlEncode(Object o) {
152
        return URLEncoder.encode((o == null ? "" : o.toString()), Charsets.UTF_8);
27✔
153
    }
154

155
    public static String truncateLabel(String label) {
156
        if (label != null && label.length() > 120) {
×
157
            return label.substring(0, 100) + "...";
×
158
        }
159
        return label;
×
160
    }
161

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

172
    /**
173
     * Generates a URL with the given base and appends the provided PageParameters as query parameters.
174
     *
175
     * @param base       the base URL
176
     * @param parameters the PageParameters to append
177
     * @return the complete URL with parameters
178
     */
179
    public static String getUrlWithParameters(String base, PageParameters parameters) {
180
        try {
181
            URIBuilder u = new URIBuilder(base);
15✔
182
            for (String key : parameters.getNamedKeys()) {
33✔
183
                for (StringValue value : parameters.getValues(key)) {
36✔
184
                    if (!value.isNull()) u.addParameter(key, value.toString());
27!
185
                }
3✔
186
            }
3✔
187
            return u.build().toString();
12✔
188
        } catch (URISyntaxException ex) {
3✔
189
            logger.error("Could not build URL with parameters: {} {}", base, parameters, ex);
51✔
190
            return "/";
6✔
191
        }
192
    }
193

194
    /**
195
     * Generates a short name for a public key or public key hash.
196
     *
197
     * @param pubkeyOrPubkeyhash the public key (64 characters) or public key hash (40 characters)
198
     * @return a short representation of the public key or public key hash
199
     */
200
    public static String getShortPubkeyName(String pubkeyOrPubkeyhash) {
201
        if (pubkeyOrPubkeyhash.length() == 64) {
12!
202
            return pubkeyOrPubkeyhash.replaceFirst("^(.{8}).*$", "$1");
×
203
        } else {
204
            return pubkeyOrPubkeyhash.replaceFirst("^(.).{39}(.{5}).*$", "$1..$2..");
15✔
205
        }
206
    }
207

208
    /**
209
     * Generates a short label for a public key or public key hash, including its status (local or approved).
210
     *
211
     * @param pubkeyOrPubkeyhash the public key (64 characters) or public key hash (40 characters)
212
     * @param user               the IRI of the user associated with the public key
213
     * @return a short label indicating the public key and its status
214
     */
215
    public static String getShortPubkeyhashLabel(String pubkeyOrPubkeyhash, IRI user) {
216
        String s = getShortPubkeyName(pubkeyOrPubkeyhash);
×
217
        NanodashSession session = NanodashSession.get();
×
218
        List<String> l = new ArrayList<>();
×
219
        if (pubkeyOrPubkeyhash.equals(session.getPubkeyString()) || pubkeyOrPubkeyhash.equals(session.getPubkeyhash()))
×
220
            l.add("local");
×
221
        // TODO: Make this more efficient:
222
        String hashed = Utils.createSha256HexHash(pubkeyOrPubkeyhash);
×
223
        if (User.getPubkeyhashes(user, true).contains(pubkeyOrPubkeyhash) || User.getPubkeyhashes(user, true).contains(hashed))
×
224
            l.add("approved");
×
225
        if (!l.isEmpty()) s += " (" + String.join("/", l) + ")";
×
226
        return s;
×
227
    }
228

229
    /**
230
     * Retrieves the name of the public key location based on the public key.
231
     *
232
     * @param pubkeyhash the public key string
233
     * @return the name of the public key location
234
     */
235
    public static String getPubkeyLocationName(String pubkeyhash) {
236
        return getPubkeyLocationName(pubkeyhash, getShortPubkeyName(pubkeyhash));
×
237
    }
238

239
    /**
240
     * Retrieves the name of the public key location, or returns a fallback name if not found.
241
     * If the key location is localhost, it returns "localhost".
242
     *
243
     * @param pubkeyhash the public key string
244
     * @param fallback   the fallback name to return if the key location is not found
245
     * @return the name of the public key location or the fallback name
246
     */
247
    public static String getPubkeyLocationName(String pubkeyhash, String fallback) {
248
        IRI keyLocation = User.getUserData().getKeyLocationForPubkeyHash(pubkeyhash);
×
249
        if (keyLocation == null) return fallback;
×
250
        if (keyLocation.stringValue().equals("http://localhost:37373/")) return "localhost";
×
251
        return keyLocation.stringValue().replaceFirst("https?://(nanobench\\.)?(nanodash\\.(?=.*\\..))?(.*[^/])/?$", "$3");
×
252
    }
253

254
    /**
255
     * Generates a short label for a public key location, including its status (local or approved).
256
     *
257
     * @param pubkeyhash the public key string
258
     * @param user       the IRI of the user associated with the public key
259
     * @return a short label indicating the public key location and its status
260
     */
261
    public static String getShortPubkeyLocationLabel(String pubkeyhash, IRI user) {
262
        String s = getPubkeyLocationName(pubkeyhash);
×
263
        NanodashSession session = NanodashSession.get();
×
264
        List<String> l = new ArrayList<>();
×
265
        if (pubkeyhash.equals(session.getPubkeyhash())) l.add("local");
×
266
        // TODO: Make this more efficient:
267
        if (User.getPubkeyhashes(user, true).contains(pubkeyhash)) l.add("approved");
×
268
        if (!l.isEmpty()) s += " (" + String.join("/", l) + ")";
×
269
        return s;
×
270
    }
271

272
    /**
273
     * Checks if a given public key has a Nanodash location.
274
     * A Nanodash location is identified by specific keywords in the key location.
275
     *
276
     * @param pubkeyhash the public key to check
277
     * @return true if the public key has a Nanodash location, false otherwise
278
     */
279
    public static boolean hasNanodashLocation(String pubkeyhash) {
280
        IRI keyLocation = User.getUserData().getKeyLocationForPubkeyHash(pubkeyhash);
×
281
        if (keyLocation == null) return true; // potentially a Nanodash location
×
282
        if (keyLocation.stringValue().contains("nanodash")) return true;
×
283
        if (keyLocation.stringValue().contains("nanobench")) return true;
×
284
        if (keyLocation.stringValue().contains(":37373")) return true;
×
285
        return false;
×
286
    }
287

288
    /**
289
     * Retrieves the short ORCID ID from an IRI object.
290
     *
291
     * @param orcidIri the IRI object representing the ORCID ID
292
     * @return the short ORCID ID as a string
293
     */
294
    public static String getShortOrcidId(IRI orcidIri) {
295
        return orcidIri.stringValue().replaceFirst("^https://orcid.org/", "");
18✔
296
    }
297

298
    /**
299
     * Retrieves the URI postfix from a given URI object.
300
     *
301
     * @param uri the URI object from which to extract the postfix
302
     * @return the URI postfix as a string
303
     */
304
    public static String getUriPostfix(Object uri) {
305
        String s = uri.toString();
9✔
306
        if (s.contains("#")) return s.replaceFirst("^.*#(.*)$", "$1");
27✔
307
        return s.replaceFirst("^.*/(.*)$", "$1");
15✔
308
    }
309

310
    /**
311
     * Retrieves the URI prefix from a given URI object.
312
     *
313
     * @param uri the URI object from which to extract the prefix
314
     * @return the URI prefix as a string
315
     */
316
    public static String getUriPrefix(Object uri) {
317
        String s = uri.toString();
9✔
318
        if (s.contains("#")) return s.replaceFirst("^(.*#).*$", "$1");
27✔
319
        return s.replaceFirst("^(.*/).*$", "$1");
15✔
320
    }
321

322
    /**
323
     * Checks if a given string is a valid URI postfix.
324
     * A valid URI postfix does not contain a colon (":").
325
     *
326
     * @param s the string to check
327
     * @return true if the string is a valid URI postfix, false otherwise
328
     */
329
    public static boolean isUriPostfix(String s) {
330
        return !s.contains(":");
24✔
331
    }
332

333
    /**
334
     * Retrieves the location of a given IntroNanopub.
335
     *
336
     * @param inp the IntroNanopub from which to extract the location
337
     * @return the IRI location of the nanopublication, or null if not found
338
     */
339
    public static IRI getLocation(IntroNanopub inp) {
340
        NanopubSignatureElement el = getNanopubSignatureElement(inp);
×
341
        for (KeyDeclaration kd : inp.getKeyDeclarations()) {
×
342
            if (el.getPublicKeyString().equals(kd.getPublicKeyString())) {
×
343
                return kd.getKeyLocation();
×
344
            }
345
        }
×
346
        return null;
×
347
    }
348

349
    /**
350
     * Retrieves the NanopubSignatureElement from a given IntroNanopub.
351
     *
352
     * @param inp the IntroNanopub from which to extract the signature element
353
     * @return the NanopubSignatureElement associated with the nanopublication
354
     */
355
    public static NanopubSignatureElement getNanopubSignatureElement(IntroNanopub inp) {
356
        try {
357
            return SignatureUtils.getSignatureElement(inp.getNanopub());
×
358
        } catch (MalformedCryptoElementException ex) {
×
359
            throw new RuntimeException(ex);
×
360
        }
361
    }
362

363
    /**
364
     * Retrieves a Nanopub object from a given URI if it is a potential Trusty URI.
365
     *
366
     * @param uri the URI to check and retrieve the Nanopub from
367
     * @return the Nanopub object if found, or null if not a known nanopublication
368
     */
369
    public static Nanopub getAsNanopub(String uri) {
370
        if (uri == null) return null;
6!
371
        if (TrustyUriUtils.isPotentialTrustyUri(uri)) {
9!
372
            try {
373
                return Utils.getNanopub(uri);
9✔
374
            } catch (Exception ex) {
×
375
                logger.error("The given URI is not a known nanopublication: {}", uri, ex);
×
376
            }
377
        }
378
        return null;
×
379
    }
380

381
    private static final PolicyFactory htmlSanitizePolicy = new HtmlPolicyBuilder()
9✔
382
            .allowCommonBlockElements()
3✔
383
            .allowCommonInlineFormattingElements()
45✔
384
            .allowUrlProtocols("https", "http", "mailto")
21✔
385
            .allowElements("a")
21✔
386
            .allowAttributes("href").onElements("a")
42✔
387
            .allowElements("img")
21✔
388
            .allowAttributes("src").onElements("img")
42✔
389
            .allowElements("pre")
3✔
390
            .requireRelNofollowOnLinks()
3✔
391
            .toFactory();
6✔
392

393
    /**
394
     * Sanitizes raw HTML input to ensure safe rendering.
395
     *
396
     * @param rawHtml the raw HTML input to sanitize
397
     * @return sanitized HTML string
398
     */
399
    public static String sanitizeHtml(String rawHtml) {
400
        return htmlSanitizePolicy.sanitize(rawHtml);
12✔
401
    }
402

403
    /**
404
     * Checks if a given string is likely to be HTML content.
405
     *
406
     * @param value the string to check
407
     * @return true if the given string is HTML content, false otherwise
408
     */
409
    public static boolean looksLikeHtml(String value) {
410
        return LEADING_TAG.matcher(value).find();
×
411
    }
412

413
    /**
414
     * Converts PageParameters to a URL-encoded string representation.
415
     *
416
     * @param params the PageParameters to convert
417
     * @return a string representation of the parameters in URL-encoded format
418
     */
419
    public static String getPageParametersAsString(PageParameters params) {
420
        String s = "";
6✔
421
        for (String n : params.getNamedKeys()) {
33✔
422
            if (!s.isEmpty()) s += "&";
18✔
423
            s += n + "=" + URLEncoder.encode(params.get(n).toString(), Charsets.UTF_8);
30✔
424
        }
3✔
425
        return s;
6✔
426
    }
427

428
    /**
429
     * Sets a minimal escape markup function for a Select2Choice component.
430
     * This function replaces certain characters and formats the display of choices.
431
     *
432
     * @param selectItem the Select2Choice component to set the escape markup for
433
     */
434
    public static void setSelect2ChoiceMinimalEscapeMarkup(Select2Choice<?> selectItem) {
435
        selectItem.getSettings().setEscapeMarkup("function(markup) {" +
15✔
436
                                                 "return markup" +
437
                                                 ".replaceAll('<','&lt;').replaceAll('>', '&gt;')" +
438
                                                 ".replace(/^(.*?) - /, '<span class=\"term\">$1</span><br>')" +
439
                                                 ".replace(/\\((https?:[\\S]+)\\)$/, '<br><code>$1</code>')" +
440
                                                 ".replace(/^([^<].*)$/, '<span class=\"term\">$1</span>')" +
441
                                                 ";}"
442
        );
443
    }
3✔
444

445
    /**
446
     * Checks if a nanopublication is of a specific class.
447
     *
448
     * @param np       the nanopublication to check
449
     * @param classIri the IRI of the class to check against
450
     * @return true if the nanopublication is of the specified class, false otherwise
451
     */
452
    public static boolean isNanopubOfClass(Nanopub np, IRI classIri) {
453
        return NanopubUtils.getTypes(np).contains(classIri);
15✔
454
    }
455

456
    /**
457
     * Checks if a nanopublication uses a specific predicate in its assertion.
458
     *
459
     * @param np           the nanopublication to check
460
     * @param predicateIri the IRI of the predicate to look for
461
     * @return true if the predicate is used in the assertion, false otherwise
462
     */
463
    public static boolean usesPredicateInAssertion(Nanopub np, IRI predicateIri) {
464
        for (Statement st : np.getAssertion()) {
33✔
465
            if (predicateIri.equals(st.getPredicate())) {
15✔
466
                return true;
6✔
467
            }
468
        }
3✔
469
        return false;
6✔
470
    }
471

472
    /**
473
     * Retrieves a map of FOAF names from the nanopublication's pubinfo.
474
     *
475
     * @param np the nanopublication from which to extract FOAF names
476
     * @return a map where keys are subjects and values are FOAF names
477
     */
478
    public static Map<String, String> getFoafNameMap(Nanopub np) {
479
        Map<String, String> foafNameMap = new HashMap<>();
12✔
480
        for (Statement st : np.getPubinfo()) {
33✔
481
            if (st.getPredicate().equals(FOAF.NAME) && st.getObject() instanceof Literal objL) {
42✔
482
                foafNameMap.put(st.getSubject().stringValue(), objL.stringValue());
24✔
483
            }
484
        }
3✔
485
        return foafNameMap;
6✔
486
    }
487

488
    /**
489
     * Creates an SHA-256 hash of the string representation of an object and returns it as a hexadecimal string.
490
     *
491
     * @param obj the object to hash
492
     * @return the SHA-256 hash of the object's string representation in hexadecimal format
493
     */
494
    public static String createSha256HexHash(Object obj) {
495
        return Hashing.sha256().hashString(obj.toString(), StandardCharsets.UTF_8).toString();
21✔
496
    }
497

498
    /**
499
     * Gets the types of a nanopublication.
500
     *
501
     * @param np the nanopublication from which to extract types
502
     * @return a list of IRI types associated with the nanopublication
503
     */
504
    public static List<IRI> getTypes(Nanopub np) {
505
        List<IRI> l = new ArrayList<>();
12✔
506
        for (IRI t : NanopubUtils.getTypes(np)) {
33✔
507
            if (t.equals(FIP.AVAILABLE_FAIR_ENABLING_RESOURCE)) continue;
15✔
508
            if (t.equals(FIP.FAIR_ENABLING_RESOURCE_TO_BE_DEVELOPED))
12✔
509
                continue;
3✔
510
            if (t.equals(FIP.AVAILABLE_FAIR_SUPPORTING_RESOURCE)) continue;
12!
511
            if (t.equals(FIP.FAIR_SUPPORTING_RESOURCE_TO_BE_DEVELOPED))
12!
512
                continue;
×
513
            l.add(t);
12✔
514
        }
3✔
515
        return l;
6✔
516
    }
517

518
    /**
519
     * Gets a label for a type IRI.
520
     *
521
     * @param typeIri the IRI of the type
522
     * @return a label for the type, potentially truncated
523
     */
524
    public static String getTypeLabel(IRI typeIri) {
525
        if (typeIri.equals(FIP.FAIR_ENABLING_RESOURCE)) return "FER";
18✔
526
        if (typeIri.equals(FIP.FAIR_SUPPORTING_RESOURCE)) return "FSR";
18✔
527
        if (typeIri.equals(FIP.FAIR_IMPLEMENTATION_PROFILE)) return "FIP";
18✔
528
        if (typeIri.equals(NPX.DECLARED_BY)) return "user intro";
18✔
529
        String l = typeIri.stringValue();
9✔
530
        l = l.replaceFirst("^.*[/#]([^/#]+)[/#]?$", "$1");
15✔
531
        l = l.replaceFirst("^(.+)Nanopub$", "$1");
15✔
532
        if (l.length() > 25) l = l.substring(0, 20) + "...";
30✔
533
        return l;
6✔
534
    }
535

536
    /**
537
     * Gets a label for a URI.
538
     *
539
     * @param uri the URI to get the label from
540
     * @return a label for the URI, potentially truncated
541
     */
542
    public static String getUriLabel(String uri) {
543
        if (uri == null) return "";
12✔
544
        String uriLabel = uri;
6✔
545
        if (uriLabel.matches(".*[^A-Za-z0-9-_]RA[A-Za-z0-9-_]{43}([^A-Za-z0-9-_].*)?")) {
12✔
546
            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✔
547
            if (newUriLabel.length() <= 70) return newUriLabel;
18!
548
        }
549
        if (uriLabel.length() > 70) return uri.substring(0, 30) + "..." + uri.substring(uri.length() - 30);
48✔
550
        return uriLabel;
6✔
551
    }
552

553
    /**
554
     * Gets an ExternalLink with a URI label.
555
     *
556
     * @param markupId the markup ID for the link
557
     * @param uri      the URI to link to
558
     * @return an ExternalLink with the URI label
559
     */
560
    public static ExternalLink getUriLink(String markupId, String uri) {
561
        return new ExternalLink(markupId, (Utils.isLocalURI(uri) ? "" : uri), getUriLabel(uri));
39✔
562
    }
563

564
    /**
565
     * Gets an ExternalLink with a model for the URI label.
566
     *
567
     * @param markupId the markup ID for the link
568
     * @param model    the model containing the URI
569
     * @return an ExternalLink with the URI label
570
     */
571
    public static ExternalLink getUriLink(String markupId, IModel<String> model) {
572
        return new ExternalLink(markupId, model, new UriLabelModel(model));
30✔
573
    }
574

575
    private static class UriLabelModel implements IModel<String> {
576

577
        private IModel<String> uriModel;
578

579
        public UriLabelModel(IModel<String> uriModel) {
6✔
580
            this.uriModel = uriModel;
9✔
581
        }
3✔
582

583
        @Override
584
        public String getObject() {
585
            return getUriLabel(uriModel.getObject());
×
586
        }
587

588
    }
589

590
    /**
591
     * Creates a sublist from a list based on the specified indices.
592
     *
593
     * @param list      the list from which to create the sublist
594
     * @param fromIndex the starting index (inclusive) for the sublist
595
     * @param toIndex   the ending index (exclusive) for the sublist
596
     * @param <E>       the type of elements in the list
597
     * @return an ArrayList containing the elements from the specified range
598
     */
599
    public static <E> ArrayList<E> subList(List<E> list, long fromIndex, long toIndex) {
600
        // So the resulting list is serializable:
601
        return new ArrayList<E>(list.subList((int) fromIndex, (int) toIndex));
×
602
    }
603

604
    /**
605
     * Creates a sublist from an array based on the specified indices.
606
     *
607
     * @param array     the array from which to create the sublist
608
     * @param fromIndex the starting index (inclusive) for the sublist
609
     * @param toIndex   the ending index (exclusive) for the sublist
610
     * @param <E>       the type of elements in the array
611
     * @return an ArrayList containing the elements from the specified range
612
     */
613
    public static <E> ArrayList<E> subList(E[] array, long fromIndex, long toIndex) {
614
        return subList(Arrays.asList(array), fromIndex, toIndex);
×
615
    }
616

617
    /**
618
     * Comparator for sorting ApiResponseEntry objects based on a specified field.
619
     */
620
    // TODO Move this to ApiResponseEntry class?
621
    public static class ApiResponseEntrySorter implements Comparator<ApiResponseEntry>, Serializable {
622

623
        private String field;
624
        private boolean descending;
625

626
        /**
627
         * Constructor for ApiResponseEntrySorter.
628
         *
629
         * @param field      the field to sort by
630
         * @param descending if true, sorts in descending order; if false, sorts in ascending order
631
         */
632
        public ApiResponseEntrySorter(String field, boolean descending) {
×
633
            this.field = field;
×
634
            this.descending = descending;
×
635
        }
×
636

637
        /**
638
         * Compares two ApiResponseEntry objects based on the specified field.
639
         *
640
         * @param o1 the first object to be compared.
641
         * @param o2 the second object to be compared.
642
         * @return a negative integer, zero, or a positive integer as the first argument is less than, equal to, or greater than the second.
643
         */
644
        @Override
645
        public int compare(ApiResponseEntry o1, ApiResponseEntry o2) {
646
            if (descending) {
×
647
                return o2.get(field).compareTo(o1.get(field));
×
648
            } else {
649
                return o1.get(field).compareTo(o2.get(field));
×
650
            }
651
        }
652

653
    }
654

655
    /**
656
     * MIME type for TriG RDF format.
657
     */
658
    public static final String TYPE_TRIG = "application/trig";
659

660
    /**
661
     * MIME type for Jelly RDF format.
662
     */
663
    public static final String TYPE_JELLY = "application/x-jelly-rdf";
664

665
    /**
666
     * MIME type for JSON-LD format.
667
     */
668
    public static final String TYPE_JSONLD = "application/ld+json";
669

670
    /**
671
     * MIME type for N-Quads format.
672
     */
673
    public static final String TYPE_NQUADS = "application/n-quads";
674

675
    /**
676
     * MIME type for Trix format.
677
     */
678
    public static final String TYPE_TRIX = "application/trix";
679

680
    /**
681
     * MIME type for HTML format.
682
     */
683
    public static final String TYPE_HTML = "text/html";
684

685
    /**
686
     * Comma-separated list of supported MIME types for nanopublications.
687
     */
688
    public static final String SUPPORTED_TYPES =
689
            TYPE_TRIG + "," +
690
            TYPE_JELLY + "," +
691
            TYPE_JSONLD + "," +
692
            TYPE_NQUADS + "," +
693
            TYPE_TRIX + "," +
694
            TYPE_HTML;
695

696
    /**
697
     * List of supported MIME types for nanopublications.
698
     */
699
    public static final List<String> SUPPORTED_TYPES_LIST = Arrays.asList(StringUtils.split(SUPPORTED_TYPES, ','));
18✔
700

701
    private static volatile String resolvedMainRegistryUrl;
702
    private static volatile String resolvedMainQueryUrl;
703

704
    /**
705
     * Eagerly resolves the main registry and query URLs. Call at application startup
706
     * so the (potentially slow) first-time discovery does not happen during a user request.
707
     */
708
    public static void initMainUrls() {
709
        getMainRegistryUrl();
6✔
710
        getMainQueryUrl();
6✔
711
    }
3✔
712

713
    /**
714
     * Returns the URL of the main Nanopub Registry for this nanodash instance.
715
     * <p>
716
     * If {@code NANODASH_MAIN_REGISTRY} is set and matches an entry in the library's
717
     * discovered registry instance list, that URL is used. Otherwise the first entry
718
     * of the library list is used. If the library list is empty, the env var value
719
     * (or built-in default) is used unvalidated. The result is cached for the JVM lifetime.
720
     *
721
     * @return Nanopub Registry URL (with trailing slash)
722
     */
723
    public static String getMainRegistryUrl() {
724
        if (resolvedMainRegistryUrl == null) {
6✔
725
            synchronized (Utils.class) {
12✔
726
                if (resolvedMainRegistryUrl == null) {
6!
727
                    resolvedMainRegistryUrl = resolveMainRegistryUrl();
6✔
728
                }
729
            }
9✔
730
        }
731
        return resolvedMainRegistryUrl;
6✔
732
    }
733

734
    /**
735
     * Returns the URL of the main Nanopub Query API for this nanodash instance.
736
     * <p>
737
     * If {@code NANODASH_MAIN_QUERY} is set and matches an entry in the library's
738
     * discovered query instance list, that URL is used. Otherwise the first entry
739
     * of the library list is used. If the library list is empty, the env var value
740
     * (or built-in default) is used unvalidated. The result is cached for the JVM lifetime.
741
     *
742
     * @return Nanopub Query URL (with trailing slash)
743
     */
744
    public static String getMainQueryUrl() {
745
        if (resolvedMainQueryUrl == null) {
6✔
746
            synchronized (Utils.class) {
12✔
747
                if (resolvedMainQueryUrl == null) {
6!
748
                    resolvedMainQueryUrl = resolveMainQueryUrl();
6✔
749
                }
750
            }
9✔
751
        }
752
        return resolvedMainQueryUrl;
6✔
753
    }
754

755
    private static String resolveMainRegistryUrl() {
756
        String envValue = trimToNull(System.getenv("NANODASH_MAIN_REGISTRY"));
12✔
757
        List<String> instances;
758
        try {
759
            instances = NanopubServerUtils.getRegistryServerList();
6✔
760
        } catch (Exception ex) {
×
761
            logger.warn("Could not retrieve registry instance list from nanopub library: {}", ex.toString());
×
762
            instances = Collections.emptyList();
×
763
        }
3✔
764
        return resolveMainUrl("NANODASH_MAIN_REGISTRY", envValue, instances, DEFAULT_MAIN_REGISTRY_URL);
18✔
765
    }
766

767
    private static String resolveMainQueryUrl() {
768
        String envValue = trimToNull(System.getenv("NANODASH_MAIN_QUERY"));
12✔
769
        List<String> instances;
770
        try {
771
            instances = QueryCall.getApiInstances();
6✔
772
        } catch (NotEnoughAPIInstancesException ex) {
×
773
            logger.warn("Nanopub library reports not enough query API instances available: {}", ex.toString());
×
774
            instances = Collections.emptyList();
×
775
        } catch (Exception ex) {
×
776
            logger.warn("Could not retrieve query instance list from nanopub library: {}", ex.toString());
×
777
            instances = Collections.emptyList();
×
778
        }
3✔
779
        return resolveMainUrl("NANODASH_MAIN_QUERY", envValue, instances, DEFAULT_MAIN_QUERY_URL);
18✔
780
    }
781

782
    private static String resolveMainUrl(String envVarName, String envValue, List<String> instances, String builtInDefault) {
783
        if (envValue != null) {
6!
784
            if (containsNormalized(instances, envValue)) {
×
785
                logger.info("Using main URL from {} (validated against library instance list): {}", envVarName, envValue);
×
786
                return ensureTrailingSlash(envValue);
×
787
            }
788
            if (instances.isEmpty()) {
×
789
                logger.warn("Library instance list is empty; using {} unvalidated: {}", envVarName, envValue);
×
790
                return ensureTrailingSlash(envValue);
×
791
            }
792
            logger.warn("{}={} is not in the library instance list {}; falling back to first library instance",
×
793
                    envVarName, envValue, instances);
794
            return ensureTrailingSlash(instances.get(0));
×
795
        }
796
        if (!instances.isEmpty()) {
9!
797
            String first = instances.get(0);
15✔
798
            logger.info("{} not set; using first library instance: {}", envVarName, first);
15✔
799
            return ensureTrailingSlash(first);
9✔
800
        }
801
        logger.warn("{} not set and library instance list is empty; using built-in default: {}", envVarName, builtInDefault);
×
802
        return builtInDefault;
×
803
    }
804

805
    private static boolean containsNormalized(List<String> urls, String target) {
806
        String normTarget = normalizeUrl(target);
×
807
        for (String url : urls) {
×
808
            if (normalizeUrl(url).equals(normTarget)) return true;
×
809
        }
×
810
        return false;
×
811
    }
812

813
    private static String normalizeUrl(String url) {
814
        if (url == null) return "";
×
815
        return url.trim().replaceFirst("/+$", "").toLowerCase(Locale.ROOT);
×
816
    }
817

818
    private static String ensureTrailingSlash(String url) {
819
        return url.endsWith("/") ? url : url + "/";
21!
820
    }
821

822
    private static String trimToNull(String s) {
823
        if (s == null) return null;
12!
824
        s = s.trim();
×
825
        return s.isEmpty() ? null : s;
×
826
    }
827

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

832
    /**
833
     * Checks whether string is valid literal serialization.
834
     *
835
     * @param literalString the literal string
836
     * @return true if valid
837
     */
838
    public static boolean isValidLiteralSerialization(String literalString) {
839
        if (literalString.matches(PLAIN_LITERAL_PATTERN)) {
12✔
840
            return true;
6✔
841
        } else if (literalString.matches(LANGTAG_LITERAL_PATTERN)) {
12✔
842
            return true;
6✔
843
        } else if (literalString.matches(DATATYPE_LITERAL_PATTERN)) {
12✔
844
            return true;
6✔
845
        }
846
        return false;
6✔
847
    }
848

849
    /**
850
     * Returns a serialized version of the literal.
851
     *
852
     * @param literal the literal
853
     * @return the String serialization of the literal
854
     */
855
    public static String getSerializedLiteral(Literal literal) {
856
        if (literal.getLanguage().isPresent()) {
12✔
857
            return "\"" + getEscapedLiteralString(literal.stringValue()) + "\"@" + Literals.normalizeLanguageTag(literal.getLanguage().get());
30✔
858
        } else if (literal.getDatatype().equals(XSD.STRING)) {
15✔
859
            return "\"" + getEscapedLiteralString(literal.stringValue()) + "\"";
15✔
860
        } else {
861
            return "\"" + getEscapedLiteralString(literal.stringValue()) + "\"^^<" + literal.getDatatype() + ">";
24✔
862
        }
863
    }
864

865
    /**
866
     * Parses a serialized literal into a Literal object.
867
     *
868
     * @param serializedLiteral The serialized String of the literal
869
     * @return The parse Literal object
870
     */
871
    public static Literal getParsedLiteral(String serializedLiteral) {
872
        if (serializedLiteral.matches(PLAIN_LITERAL_PATTERN)) {
12✔
873
            return vf.createLiteral(getUnescapedLiteralString(serializedLiteral.replaceFirst(PLAIN_LITERAL_PATTERN, "$1")));
24✔
874
        } else if (serializedLiteral.matches(LANGTAG_LITERAL_PATTERN)) {
12✔
875
            String langtag = serializedLiteral.replaceFirst(LANGTAG_LITERAL_PATTERN, "$3");
15✔
876
            return vf.createLiteral(getUnescapedLiteralString(serializedLiteral.replaceFirst(LANGTAG_LITERAL_PATTERN, "$1")), langtag);
27✔
877
        } else if (serializedLiteral.matches(DATATYPE_LITERAL_PATTERN)) {
12✔
878
            IRI datatype = vf.createIRI(serializedLiteral.replaceFirst(DATATYPE_LITERAL_PATTERN, "$3"));
21✔
879
            return vf.createLiteral(getUnescapedLiteralString(serializedLiteral.replaceFirst(DATATYPE_LITERAL_PATTERN, "$1")), datatype);
27✔
880
        }
881
        throw new IllegalArgumentException("Not a valid literal serialization: " + serializedLiteral);
18✔
882
    }
883

884
    /**
885
     * Escapes quotes (") and slashes (/) of a literal string.
886
     *
887
     * @param unescapedString un-escaped string
888
     * @return escaped string
889
     */
890
    public static String getEscapedLiteralString(String unescapedString) {
891
        return unescapedString.replaceAll("\\\\", "\\\\\\\\").replaceAll("\"", "\\\"");
24✔
892
    }
893

894
    /**
895
     * Un-escapes quotes (") and slashes (/) of a literal string.
896
     *
897
     * @param escapedString escaped string
898
     * @return un-escaped string
899
     */
900
    public static String getUnescapedLiteralString(String escapedString) {
901
        return escapedString.replaceAll("\\\\(\\\\|\\\")", "$1");
15✔
902
    }
903

904
    /**
905
     * Checks if a given IRI is a local URI.
906
     *
907
     * @param uri the IRI to check
908
     * @return true if the IRI is a local URI, false otherwise
909
     */
910
    public static boolean isLocalURI(IRI uri) {
911
        return uri != null && isLocalURI(uri.stringValue());
30✔
912
    }
913

914
    /**
915
     * Checks if a given string is a local URI.
916
     *
917
     * @param uriAsString the string to check
918
     * @return true if the string is a local URI, false otherwise
919
     */
920
    public static boolean isLocalURI(String uriAsString) {
921
        return !uriAsString.isBlank() && uriAsString.startsWith(LocalUri.PREFIX);
33✔
922
    }
923

924
    public static String unescapeMultiValue(String s) {
925
        StringBuilder sb = new StringBuilder();
12✔
926
        for (int i = 0; i < s.length(); i++) {
24✔
927
            if (s.charAt(i) == '\\' && i + 1 < s.length()) {
33✔
928
                char next = s.charAt(i + 1);
18✔
929
                if (next == 'n') {
9✔
930
                    sb.append('\n');
15✔
931
                } else if (next == '\\') {
9!
932
                    sb.append('\\');
15✔
933
                } else {
934
                    sb.append(next);
×
935
                }
936
                i++;
3✔
937
            } else {
3✔
938
                sb.append(s.charAt(i));
18✔
939
            }
940
        }
941
        return sb.toString();
9✔
942
    }
943

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