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

knowledgepixels / nanodash / 23688905990

28 Mar 2026 04:02PM UTC coverage: 16.298% (+0.02%) from 16.274%
23688905990

push

github

tkuhn
feat: support <pre> in HTML sanitization and improve _multi_val handling

- Add <pre> to allowed HTML sanitizer elements and looksLikeHtml pattern
- Unify _multi_val to always split on newlines, checking each part
  individually for IRI vs literal (replaces looksLikeSpaceSeparatedIris)
- Add HTML sanitization support in QueryResultList _multi_val path
- Remove top/bottom margins on <pre> elements

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

756 of 5699 branches covered (13.27%)

Branch coverage included in aggregate %.

1902 of 10610 relevant lines covered (17.93%)

2.46 hits per line

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

66.67
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.services.ApiResponseEntry;
29
import org.nanopub.extra.setting.IntroNanopub;
30
import org.nanopub.vocabulary.FIP;
31
import org.nanopub.vocabulary.NPX;
32
import org.owasp.html.HtmlPolicyBuilder;
33
import org.owasp.html.PolicyFactory;
34
import org.slf4j.Logger;
35
import org.slf4j.LoggerFactory;
36
import org.wicketstuff.select2.Select2Choice;
37

38
import java.io.Serializable;
39
import java.net.URISyntaxException;
40
import java.net.URLDecoder;
41
import java.net.URLEncoder;
42
import java.nio.charset.StandardCharsets;
43
import java.util.*;
44
import java.util.regex.Pattern;
45

46
import static java.nio.charset.StandardCharsets.UTF_8;
47

48
/**
49
 * Utility class providing various helper methods for handling nanopublications, URIs, and other related functionalities.
50
 */
51
public class Utils {
52

53
    private Utils() {
54
    }  // no instances allowed
55

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

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

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

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

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

111
    /**
112
     * Retrieves a Nanopub object based on the given URI or artifact code.
113
     *
114
     * @param uriOrArtifactCode the URI or artifact code of the nanopublication
115
     * @return the Nanopub object, or null if not found
116
     */
117
    public static Nanopub getNanopub(String uriOrArtifactCode) {
118
        String artifactCode = GetNanopub.getArtifactCode(uriOrArtifactCode);
9✔
119
        if (!nanopubs.containsKey(artifactCode)) {
12✔
120
            for (int i = 0; i < 3; i++) {  // Try 3 times to get nanopub
15!
121
                Nanopub np = GetNanopub.get(artifactCode);
9✔
122
                if (np != null) {
6!
123
                    nanopubs.put(artifactCode, np);
15✔
124
                    break;
3✔
125
                }
126
            }
127
        }
128
        return nanopubs.get(artifactCode);
15✔
129
    }
130

131
    /**
132
     * URL-encodes the string representation of the given object using UTF-8 encoding.
133
     *
134
     * @param o the object to be URL-encoded
135
     * @return the URL-encoded string
136
     */
137
    public static String urlEncode(Object o) {
138
        return URLEncoder.encode((o == null ? "" : o.toString()), Charsets.UTF_8);
27✔
139
    }
140

141
    /**
142
     * URL-decodes the string representation of the given object using UTF-8 encoding.
143
     *
144
     * @param o the object to be URL-decoded
145
     * @return the URL-decoded string
146
     */
147
    public static String urlDecode(Object o) {
148
        return URLDecoder.decode((o == null ? "" : o.toString()), Charsets.UTF_8);
27✔
149
    }
150

151
    /**
152
     * Generates a URL with the given base and appends the provided PageParameters as query parameters.
153
     *
154
     * @param base       the base URL
155
     * @param parameters the PageParameters to append
156
     * @return the complete URL with parameters
157
     */
158
    public static String getUrlWithParameters(String base, PageParameters parameters) {
159
        try {
160
            URIBuilder u = new URIBuilder(base);
15✔
161
            for (String key : parameters.getNamedKeys()) {
33✔
162
                for (StringValue value : parameters.getValues(key)) {
36✔
163
                    if (!value.isNull()) u.addParameter(key, value.toString());
27!
164
                }
3✔
165
            }
3✔
166
            return u.build().toString();
12✔
167
        } catch (URISyntaxException ex) {
3✔
168
            logger.error("Could not build URL with parameters: {} {}", base, parameters, ex);
51✔
169
            return "/";
6✔
170
        }
171
    }
172

173
    /**
174
     * Generates a short name for a public key or public key hash.
175
     *
176
     * @param pubkeyOrPubkeyhash the public key (64 characters) or public key hash (40 characters)
177
     * @return a short representation of the public key or public key hash
178
     */
179
    public static String getShortPubkeyName(String pubkeyOrPubkeyhash) {
180
        if (pubkeyOrPubkeyhash.length() == 64) {
12!
181
            return pubkeyOrPubkeyhash.replaceFirst("^(.{8}).*$", "$1");
×
182
        } else {
183
            return pubkeyOrPubkeyhash.replaceFirst("^(.).{39}(.{5}).*$", "$1..$2..");
15✔
184
        }
185
    }
186

187
    /**
188
     * Generates a short label for a public key or public key hash, including its status (local or approved).
189
     *
190
     * @param pubkeyOrPubkeyhash the public key (64 characters) or public key hash (40 characters)
191
     * @param user               the IRI of the user associated with the public key
192
     * @return a short label indicating the public key and its status
193
     */
194
    public static String getShortPubkeyhashLabel(String pubkeyOrPubkeyhash, IRI user) {
195
        String s = getShortPubkeyName(pubkeyOrPubkeyhash);
×
196
        NanodashSession session = NanodashSession.get();
×
197
        List<String> l = new ArrayList<>();
×
198
        if (pubkeyOrPubkeyhash.equals(session.getPubkeyString()) || pubkeyOrPubkeyhash.equals(session.getPubkeyhash()))
×
199
            l.add("local");
×
200
        // TODO: Make this more efficient:
201
        String hashed = Utils.createSha256HexHash(pubkeyOrPubkeyhash);
×
202
        if (User.getPubkeyhashes(user, true).contains(pubkeyOrPubkeyhash) || User.getPubkeyhashes(user, true).contains(hashed))
×
203
            l.add("approved");
×
204
        if (!l.isEmpty()) s += " (" + String.join("/", l) + ")";
×
205
        return s;
×
206
    }
207

208
    /**
209
     * Retrieves the name of the public key location based on the public key.
210
     *
211
     * @param pubkeyhash the public key string
212
     * @return the name of the public key location
213
     */
214
    public static String getPubkeyLocationName(String pubkeyhash) {
215
        return getPubkeyLocationName(pubkeyhash, getShortPubkeyName(pubkeyhash));
×
216
    }
217

218
    /**
219
     * Retrieves the name of the public key location, or returns a fallback name if not found.
220
     * If the key location is localhost, it returns "localhost".
221
     *
222
     * @param pubkeyhash the public key string
223
     * @param fallback   the fallback name to return if the key location is not found
224
     * @return the name of the public key location or the fallback name
225
     */
226
    public static String getPubkeyLocationName(String pubkeyhash, String fallback) {
227
        IRI keyLocation = User.getUserData().getKeyLocationForPubkeyHash(pubkeyhash);
×
228
        if (keyLocation == null) return fallback;
×
229
        if (keyLocation.stringValue().equals("http://localhost:37373/")) return "localhost";
×
230
        return keyLocation.stringValue().replaceFirst("https?://(nanobench\\.)?(nanodash\\.(?=.*\\..))?(.*[^/])/?$", "$3");
×
231
    }
232

233
    /**
234
     * Generates a short label for a public key location, including its status (local or approved).
235
     *
236
     * @param pubkeyhash the public key string
237
     * @param user       the IRI of the user associated with the public key
238
     * @return a short label indicating the public key location and its status
239
     */
240
    public static String getShortPubkeyLocationLabel(String pubkeyhash, IRI user) {
241
        String s = getPubkeyLocationName(pubkeyhash);
×
242
        NanodashSession session = NanodashSession.get();
×
243
        List<String> l = new ArrayList<>();
×
244
        if (pubkeyhash.equals(session.getPubkeyhash())) l.add("local");
×
245
        // TODO: Make this more efficient:
246
        if (User.getPubkeyhashes(user, true).contains(pubkeyhash)) l.add("approved");
×
247
        if (!l.isEmpty()) s += " (" + String.join("/", l) + ")";
×
248
        return s;
×
249
    }
250

251
    /**
252
     * Checks if a given public key has a Nanodash location.
253
     * A Nanodash location is identified by specific keywords in the key location.
254
     *
255
     * @param pubkeyhash the public key to check
256
     * @return true if the public key has a Nanodash location, false otherwise
257
     */
258
    public static boolean hasNanodashLocation(String pubkeyhash) {
259
        IRI keyLocation = User.getUserData().getKeyLocationForPubkeyHash(pubkeyhash);
×
260
        if (keyLocation == null) return true; // potentially a Nanodash location
×
261
        if (keyLocation.stringValue().contains("nanodash")) return true;
×
262
        if (keyLocation.stringValue().contains("nanobench")) return true;
×
263
        if (keyLocation.stringValue().contains(":37373")) return true;
×
264
        return false;
×
265
    }
266

267
    /**
268
     * Retrieves the short ORCID ID from an IRI object.
269
     *
270
     * @param orcidIri the IRI object representing the ORCID ID
271
     * @return the short ORCID ID as a string
272
     */
273
    public static String getShortOrcidId(IRI orcidIri) {
274
        return orcidIri.stringValue().replaceFirst("^https://orcid.org/", "");
18✔
275
    }
276

277
    /**
278
     * Retrieves the URI postfix from a given URI object.
279
     *
280
     * @param uri the URI object from which to extract the postfix
281
     * @return the URI postfix as a string
282
     */
283
    public static String getUriPostfix(Object uri) {
284
        String s = uri.toString();
9✔
285
        if (s.contains("#")) return s.replaceFirst("^.*#(.*)$", "$1");
27✔
286
        return s.replaceFirst("^.*/(.*)$", "$1");
15✔
287
    }
288

289
    /**
290
     * Retrieves the URI prefix from a given URI object.
291
     *
292
     * @param uri the URI object from which to extract the prefix
293
     * @return the URI prefix as a string
294
     */
295
    public static String getUriPrefix(Object uri) {
296
        String s = uri.toString();
9✔
297
        if (s.contains("#")) return s.replaceFirst("^(.*#).*$", "$1");
27✔
298
        return s.replaceFirst("^(.*/).*$", "$1");
15✔
299
    }
300

301
    /**
302
     * Checks if a given string is a valid URI postfix.
303
     * A valid URI postfix does not contain a colon (":").
304
     *
305
     * @param s the string to check
306
     * @return true if the string is a valid URI postfix, false otherwise
307
     */
308
    public static boolean isUriPostfix(String s) {
309
        return !s.contains(":");
24✔
310
    }
311

312
    /**
313
     * Retrieves the location of a given IntroNanopub.
314
     *
315
     * @param inp the IntroNanopub from which to extract the location
316
     * @return the IRI location of the nanopublication, or null if not found
317
     */
318
    public static IRI getLocation(IntroNanopub inp) {
319
        NanopubSignatureElement el = getNanopubSignatureElement(inp);
×
320
        for (KeyDeclaration kd : inp.getKeyDeclarations()) {
×
321
            if (el.getPublicKeyString().equals(kd.getPublicKeyString())) {
×
322
                return kd.getKeyLocation();
×
323
            }
324
        }
×
325
        return null;
×
326
    }
327

328
    /**
329
     * Retrieves the NanopubSignatureElement from a given IntroNanopub.
330
     *
331
     * @param inp the IntroNanopub from which to extract the signature element
332
     * @return the NanopubSignatureElement associated with the nanopublication
333
     */
334
    public static NanopubSignatureElement getNanopubSignatureElement(IntroNanopub inp) {
335
        try {
336
            return SignatureUtils.getSignatureElement(inp.getNanopub());
×
337
        } catch (MalformedCryptoElementException ex) {
×
338
            throw new RuntimeException(ex);
×
339
        }
340
    }
341

342
    /**
343
     * Retrieves a Nanopub object from a given URI if it is a potential Trusty URI.
344
     *
345
     * @param uri the URI to check and retrieve the Nanopub from
346
     * @return the Nanopub object if found, or null if not a known nanopublication
347
     */
348
    public static Nanopub getAsNanopub(String uri) {
349
        if (uri == null) return null;
6!
350
        if (TrustyUriUtils.isPotentialTrustyUri(uri)) {
9!
351
            try {
352
                return Utils.getNanopub(uri);
9✔
353
            } catch (Exception ex) {
×
354
                logger.error("The given URI is not a known nanopublication: {}", uri, ex);
×
355
            }
356
        }
357
        return null;
×
358
    }
359

360
    private static final PolicyFactory htmlSanitizePolicy = new HtmlPolicyBuilder()
9✔
361
            .allowCommonBlockElements()
3✔
362
            .allowCommonInlineFormattingElements()
45✔
363
            .allowUrlProtocols("https", "http", "mailto")
21✔
364
            .allowElements("a")
21✔
365
            .allowAttributes("href").onElements("a")
42✔
366
            .allowElements("img")
21✔
367
            .allowAttributes("src").onElements("img")
42✔
368
            .allowElements("pre")
3✔
369
            .requireRelNofollowOnLinks()
3✔
370
            .toFactory();
6✔
371

372
    /**
373
     * Sanitizes raw HTML input to ensure safe rendering.
374
     *
375
     * @param rawHtml the raw HTML input to sanitize
376
     * @return sanitized HTML string
377
     */
378
    public static String sanitizeHtml(String rawHtml) {
379
        return htmlSanitizePolicy.sanitize(rawHtml);
12✔
380
    }
381

382
    /**
383
     * Checks if a given string is likely to be HTML content.
384
     *
385
     * @param value the string to check
386
     * @return true if the given string is HTML content, false otherwise
387
     */
388
    public static boolean looksLikeHtml(String value) {
389
        return LEADING_TAG.matcher(value).find();
×
390
    }
391

392
    /**
393
     * Converts PageParameters to a URL-encoded string representation.
394
     *
395
     * @param params the PageParameters to convert
396
     * @return a string representation of the parameters in URL-encoded format
397
     */
398
    public static String getPageParametersAsString(PageParameters params) {
399
        String s = "";
6✔
400
        for (String n : params.getNamedKeys()) {
33✔
401
            if (!s.isEmpty()) s += "&";
18✔
402
            s += n + "=" + URLEncoder.encode(params.get(n).toString(), Charsets.UTF_8);
30✔
403
        }
3✔
404
        return s;
6✔
405
    }
406

407
    /**
408
     * Sets a minimal escape markup function for a Select2Choice component.
409
     * This function replaces certain characters and formats the display of choices.
410
     *
411
     * @param selectItem the Select2Choice component to set the escape markup for
412
     */
413
    public static void setSelect2ChoiceMinimalEscapeMarkup(Select2Choice<?> selectItem) {
414
        selectItem.getSettings().setEscapeMarkup("function(markup) {" +
×
415
                                                 "return markup" +
416
                                                 ".replaceAll('<','&lt;').replaceAll('>', '&gt;')" +
417
                                                 ".replace(/^(.*?) - /, '<span class=\"term\">$1</span><br>')" +
418
                                                 ".replace(/\\((https?:[\\S]+)\\)$/, '<br><code>$1</code>')" +
419
                                                 ".replace(/^([^<].*)$/, '<span class=\"term\">$1</span>')" +
420
                                                 ";}"
421
        );
422
    }
×
423

424
    /**
425
     * Checks if a nanopublication is of a specific class.
426
     *
427
     * @param np       the nanopublication to check
428
     * @param classIri the IRI of the class to check against
429
     * @return true if the nanopublication is of the specified class, false otherwise
430
     */
431
    public static boolean isNanopubOfClass(Nanopub np, IRI classIri) {
432
        return NanopubUtils.getTypes(np).contains(classIri);
15✔
433
    }
434

435
    /**
436
     * Checks if a nanopublication uses a specific predicate in its assertion.
437
     *
438
     * @param np           the nanopublication to check
439
     * @param predicateIri the IRI of the predicate to look for
440
     * @return true if the predicate is used in the assertion, false otherwise
441
     */
442
    public static boolean usesPredicateInAssertion(Nanopub np, IRI predicateIri) {
443
        for (Statement st : np.getAssertion()) {
33✔
444
            if (predicateIri.equals(st.getPredicate())) {
15✔
445
                return true;
6✔
446
            }
447
        }
3✔
448
        return false;
6✔
449
    }
450

451
    /**
452
     * Retrieves a map of FOAF names from the nanopublication's pubinfo.
453
     *
454
     * @param np the nanopublication from which to extract FOAF names
455
     * @return a map where keys are subjects and values are FOAF names
456
     */
457
    public static Map<String, String> getFoafNameMap(Nanopub np) {
458
        Map<String, String> foafNameMap = new HashMap<>();
12✔
459
        for (Statement st : np.getPubinfo()) {
33✔
460
            if (st.getPredicate().equals(FOAF.NAME) && st.getObject() instanceof Literal objL) {
42✔
461
                foafNameMap.put(st.getSubject().stringValue(), objL.stringValue());
24✔
462
            }
463
        }
3✔
464
        return foafNameMap;
6✔
465
    }
466

467
    /**
468
     * Creates an SHA-256 hash of the string representation of an object and returns it as a hexadecimal string.
469
     *
470
     * @param obj the object to hash
471
     * @return the SHA-256 hash of the object's string representation in hexadecimal format
472
     */
473
    public static String createSha256HexHash(Object obj) {
474
        return Hashing.sha256().hashString(obj.toString(), StandardCharsets.UTF_8).toString();
21✔
475
    }
476

477
    /**
478
     * Gets the types of a nanopublication.
479
     *
480
     * @param np the nanopublication from which to extract types
481
     * @return a list of IRI types associated with the nanopublication
482
     */
483
    public static List<IRI> getTypes(Nanopub np) {
484
        List<IRI> l = new ArrayList<>();
12✔
485
        for (IRI t : NanopubUtils.getTypes(np)) {
33✔
486
            if (t.equals(FIP.AVAILABLE_FAIR_ENABLING_RESOURCE)) continue;
15✔
487
            if (t.equals(FIP.FAIR_ENABLING_RESOURCE_TO_BE_DEVELOPED))
12✔
488
                continue;
3✔
489
            if (t.equals(FIP.AVAILABLE_FAIR_SUPPORTING_RESOURCE)) continue;
12!
490
            if (t.equals(FIP.FAIR_SUPPORTING_RESOURCE_TO_BE_DEVELOPED))
12!
491
                continue;
×
492
            l.add(t);
12✔
493
        }
3✔
494
        return l;
6✔
495
    }
496

497
    /**
498
     * Gets a label for a type IRI.
499
     *
500
     * @param typeIri the IRI of the type
501
     * @return a label for the type, potentially truncated
502
     */
503
    public static String getTypeLabel(IRI typeIri) {
504
        if (typeIri.equals(FIP.FAIR_ENABLING_RESOURCE)) return "FER";
18✔
505
        if (typeIri.equals(FIP.FAIR_SUPPORTING_RESOURCE)) return "FSR";
18✔
506
        if (typeIri.equals(FIP.FAIR_IMPLEMENTATION_PROFILE)) return "FIP";
18✔
507
        if (typeIri.equals(NPX.DECLARED_BY)) return "user intro";
18✔
508
        String l = typeIri.stringValue();
9✔
509
        l = l.replaceFirst("^.*[/#]([^/#]+)[/#]?$", "$1");
15✔
510
        l = l.replaceFirst("^(.+)Nanopub$", "$1");
15✔
511
        if (l.length() > 25) l = l.substring(0, 20) + "...";
30✔
512
        return l;
6✔
513
    }
514

515
    /**
516
     * Gets a label for a URI.
517
     *
518
     * @param uri the URI to get the label from
519
     * @return a label for the URI, potentially truncated
520
     */
521
    public static String getUriLabel(String uri) {
522
        if (uri == null) return "";
12✔
523
        String uriLabel = uri;
6✔
524
        if (uriLabel.matches(".*[^A-Za-z0-9-_]RA[A-Za-z0-9-_]{43}([^A-Za-z0-9-_].*)?")) {
12✔
525
            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✔
526
            if (newUriLabel.length() <= 70) return newUriLabel;
18!
527
        }
528
        if (uriLabel.length() > 70) return uri.substring(0, 30) + "..." + uri.substring(uri.length() - 30);
48✔
529
        return uriLabel;
6✔
530
    }
531

532
    /**
533
     * Gets an ExternalLink with a URI label.
534
     *
535
     * @param markupId the markup ID for the link
536
     * @param uri      the URI to link to
537
     * @return an ExternalLink with the URI label
538
     */
539
    public static ExternalLink getUriLink(String markupId, String uri) {
540
        return new ExternalLink(markupId, (Utils.isLocalURI(uri) ? "" : uri), getUriLabel(uri));
39✔
541
    }
542

543
    /**
544
     * Gets an ExternalLink with a model for the URI label.
545
     *
546
     * @param markupId the markup ID for the link
547
     * @param model    the model containing the URI
548
     * @return an ExternalLink with the URI label
549
     */
550
    public static ExternalLink getUriLink(String markupId, IModel<String> model) {
551
        return new ExternalLink(markupId, model, new UriLabelModel(model));
×
552
    }
553

554
    private static class UriLabelModel implements IModel<String> {
555

556
        private IModel<String> uriModel;
557

558
        public UriLabelModel(IModel<String> uriModel) {
×
559
            this.uriModel = uriModel;
×
560
        }
×
561

562
        @Override
563
        public String getObject() {
564
            return getUriLabel(uriModel.getObject());
×
565
        }
566

567
    }
568

569
    /**
570
     * Creates a sublist from a list based on the specified indices.
571
     *
572
     * @param list      the list from which to create the sublist
573
     * @param fromIndex the starting index (inclusive) for the sublist
574
     * @param toIndex   the ending index (exclusive) for the sublist
575
     * @param <E>       the type of elements in the list
576
     * @return an ArrayList containing the elements from the specified range
577
     */
578
    public static <E> ArrayList<E> subList(List<E> list, long fromIndex, long toIndex) {
579
        // So the resulting list is serializable:
580
        return new ArrayList<E>(list.subList((int) fromIndex, (int) toIndex));
×
581
    }
582

583
    /**
584
     * Creates a sublist from an array based on the specified indices.
585
     *
586
     * @param array     the array from which to create the sublist
587
     * @param fromIndex the starting index (inclusive) for the sublist
588
     * @param toIndex   the ending index (exclusive) for the sublist
589
     * @param <E>       the type of elements in the array
590
     * @return an ArrayList containing the elements from the specified range
591
     */
592
    public static <E> ArrayList<E> subList(E[] array, long fromIndex, long toIndex) {
593
        return subList(Arrays.asList(array), fromIndex, toIndex);
×
594
    }
595

596
    /**
597
     * Comparator for sorting ApiResponseEntry objects based on a specified field.
598
     */
599
    // TODO Move this to ApiResponseEntry class?
600
    public static class ApiResponseEntrySorter implements Comparator<ApiResponseEntry>, Serializable {
601

602
        private String field;
603
        private boolean descending;
604

605
        /**
606
         * Constructor for ApiResponseEntrySorter.
607
         *
608
         * @param field      the field to sort by
609
         * @param descending if true, sorts in descending order; if false, sorts in ascending order
610
         */
611
        public ApiResponseEntrySorter(String field, boolean descending) {
×
612
            this.field = field;
×
613
            this.descending = descending;
×
614
        }
×
615

616
        /**
617
         * Compares two ApiResponseEntry objects based on the specified field.
618
         *
619
         * @param o1 the first object to be compared.
620
         * @param o2 the second object to be compared.
621
         * @return a negative integer, zero, or a positive integer as the first argument is less than, equal to, or greater than the second.
622
         */
623
        @Override
624
        public int compare(ApiResponseEntry o1, ApiResponseEntry o2) {
625
            if (descending) {
×
626
                return o2.get(field).compareTo(o1.get(field));
×
627
            } else {
628
                return o1.get(field).compareTo(o2.get(field));
×
629
            }
630
        }
631

632
    }
633

634
    /**
635
     * MIME type for TriG RDF format.
636
     */
637
    public static final String TYPE_TRIG = "application/trig";
638

639
    /**
640
     * MIME type for Jelly RDF format.
641
     */
642
    public static final String TYPE_JELLY = "application/x-jelly-rdf";
643

644
    /**
645
     * MIME type for JSON-LD format.
646
     */
647
    public static final String TYPE_JSONLD = "application/ld+json";
648

649
    /**
650
     * MIME type for N-Quads format.
651
     */
652
    public static final String TYPE_NQUADS = "application/n-quads";
653

654
    /**
655
     * MIME type for Trix format.
656
     */
657
    public static final String TYPE_TRIX = "application/trix";
658

659
    /**
660
     * MIME type for HTML format.
661
     */
662
    public static final String TYPE_HTML = "text/html";
663

664
    /**
665
     * Comma-separated list of supported MIME types for nanopublications.
666
     */
667
    public static final String SUPPORTED_TYPES =
668
            TYPE_TRIG + "," +
669
            TYPE_JELLY + "," +
670
            TYPE_JSONLD + "," +
671
            TYPE_NQUADS + "," +
672
            TYPE_TRIX + "," +
673
            TYPE_HTML;
674

675
    /**
676
     * List of supported MIME types for nanopublications.
677
     */
678
    public static final List<String> SUPPORTED_TYPES_LIST = Arrays.asList(StringUtils.split(SUPPORTED_TYPES, ','));
18✔
679

680
    /**
681
     * Returns the URL of the default Nanopub Registry as configured by the given instance.
682
     *
683
     * @return Nanopub Registry URL
684
     */
685
    public static String getMainRegistryUrl() {
686
        String envValue = System.getenv("NANODASH_MAIN_REGISTRY");
9✔
687
        if (envValue != null) {
6!
688
            logger.info("Found environment variable NANODASH_MAIN_REGISTRY with value: {}", envValue);
×
689
            return envValue;
×
690
        } else {
691
            logger.info("Environment variable NANODASH_MAIN_REGISTRY not set, using default: {}", DEFAULT_MAIN_REGISTRY_URL);
12✔
692
            return DEFAULT_MAIN_REGISTRY_URL;
6✔
693
        }
694
    }
695

696
    /**
697
     * Returns the URL of the default Nanopub Query as configured by the given instance.
698
     *
699
     * @return Nanopub Query URL
700
     */
701
    public static String getMainQueryUrl() {
702
        String envValue = System.getenv("NANODASH_MAIN_QUERY");
×
703
        if (envValue != null) {
×
704
            logger.info("Found environment variable NANODASH_MAIN_QUERY with value: {}", envValue);
×
705
            return envValue;
×
706
        } else {
707
            logger.info("Environment variable NANODASH_MAIN_QUERY not set, using default: {}", DEFAULT_MAIN_QUERY_URL);
×
708
            return DEFAULT_MAIN_QUERY_URL;
×
709
        }
710
    }
711

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

716
    /**
717
     * Checks whether string is valid literal serialization.
718
     *
719
     * @param literalString the literal string
720
     * @return true if valid
721
     */
722
    public static boolean isValidLiteralSerialization(String literalString) {
723
        if (literalString.matches(PLAIN_LITERAL_PATTERN)) {
12✔
724
            return true;
6✔
725
        } else if (literalString.matches(LANGTAG_LITERAL_PATTERN)) {
12✔
726
            return true;
6✔
727
        } else if (literalString.matches(DATATYPE_LITERAL_PATTERN)) {
12✔
728
            return true;
6✔
729
        }
730
        return false;
6✔
731
    }
732

733
    /**
734
     * Returns a serialized version of the literal.
735
     *
736
     * @param literal the literal
737
     * @return the String serialization of the literal
738
     */
739
    public static String getSerializedLiteral(Literal literal) {
740
        if (literal.getLanguage().isPresent()) {
12✔
741
            return "\"" + getEscapedLiteralString(literal.stringValue()) + "\"@" + Literals.normalizeLanguageTag(literal.getLanguage().get());
30✔
742
        } else if (literal.getDatatype().equals(XSD.STRING)) {
15✔
743
            return "\"" + getEscapedLiteralString(literal.stringValue()) + "\"";
15✔
744
        } else {
745
            return "\"" + getEscapedLiteralString(literal.stringValue()) + "\"^^<" + literal.getDatatype() + ">";
24✔
746
        }
747
    }
748

749
    /**
750
     * Parses a serialized literal into a Literal object.
751
     *
752
     * @param serializedLiteral The serialized String of the literal
753
     * @return The parse Literal object
754
     */
755
    public static Literal getParsedLiteral(String serializedLiteral) {
756
        if (serializedLiteral.matches(PLAIN_LITERAL_PATTERN)) {
12✔
757
            return vf.createLiteral(getUnescapedLiteralString(serializedLiteral.replaceFirst(PLAIN_LITERAL_PATTERN, "$1")));
24✔
758
        } else if (serializedLiteral.matches(LANGTAG_LITERAL_PATTERN)) {
12✔
759
            String langtag = serializedLiteral.replaceFirst(LANGTAG_LITERAL_PATTERN, "$3");
15✔
760
            return vf.createLiteral(getUnescapedLiteralString(serializedLiteral.replaceFirst(LANGTAG_LITERAL_PATTERN, "$1")), langtag);
27✔
761
        } else if (serializedLiteral.matches(DATATYPE_LITERAL_PATTERN)) {
12✔
762
            IRI datatype = vf.createIRI(serializedLiteral.replaceFirst(DATATYPE_LITERAL_PATTERN, "$3"));
21✔
763
            return vf.createLiteral(getUnescapedLiteralString(serializedLiteral.replaceFirst(DATATYPE_LITERAL_PATTERN, "$1")), datatype);
27✔
764
        }
765
        throw new IllegalArgumentException("Not a valid literal serialization: " + serializedLiteral);
18✔
766
    }
767

768
    /**
769
     * Escapes quotes (") and slashes (/) of a literal string.
770
     *
771
     * @param unescapedString un-escaped string
772
     * @return escaped string
773
     */
774
    public static String getEscapedLiteralString(String unescapedString) {
775
        return unescapedString.replaceAll("\\\\", "\\\\\\\\").replaceAll("\"", "\\\"");
24✔
776
    }
777

778
    /**
779
     * Un-escapes quotes (") and slashes (/) of a literal string.
780
     *
781
     * @param escapedString escaped string
782
     * @return un-escaped string
783
     */
784
    public static String getUnescapedLiteralString(String escapedString) {
785
        return escapedString.replaceAll("\\\\(\\\\|\\\")", "$1");
15✔
786
    }
787

788
    /**
789
     * Checks if a given IRI is a local URI.
790
     *
791
     * @param uri the IRI to check
792
     * @return true if the IRI is a local URI, false otherwise
793
     */
794
    public static boolean isLocalURI(IRI uri) {
795
        return uri != null && isLocalURI(uri.stringValue());
30✔
796
    }
797

798
    /**
799
     * Checks if a given string is a local URI.
800
     *
801
     * @param uriAsString the string to check
802
     * @return true if the string is a local URI, false otherwise
803
     */
804
    public static boolean isLocalURI(String uriAsString) {
805
        return !uriAsString.isBlank() && uriAsString.startsWith(LocalUri.PREFIX);
33✔
806
    }
807

808
    /**
809
     * Unescape a multi-value entry where backslashes and newlines are escaped with backslash.
810
     *
811
     * @param s the escaped string
812
     * @return the unescaped string
813
     */
814
    public static boolean looksLikeSpaceSeparatedIris(String value) {
815
        if (value == null || value.isBlank()) return false;
×
816
        for (String part : value.split(" ")) {
×
817
            if (!part.matches("https?://.+")) return false;
×
818
        }
819
        return true;
×
820
    }
821

822
    public static String unescapeMultiValue(String s) {
823
        StringBuilder sb = new StringBuilder();
12✔
824
        for (int i = 0; i < s.length(); i++) {
24✔
825
            if (s.charAt(i) == '\\' && i + 1 < s.length()) {
33✔
826
                char next = s.charAt(i + 1);
18✔
827
                if (next == 'n') {
9✔
828
                    sb.append('\n');
15✔
829
                } else if (next == '\\') {
9!
830
                    sb.append('\\');
15✔
831
                } else {
832
                    sb.append(next);
×
833
                }
834
                i++;
3✔
835
            } else {
3✔
836
                sb.append(s.charAt(i));
18✔
837
            }
838
        }
839
        return sb.toString();
9✔
840
    }
841

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