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

knowledgepixels / nanopub-registry / 28113618611

24 Jun 2026 04:28PM UTC coverage: 31.926% (-0.2%) from 32.089%
28113618611

Pull #116

github

web-flow
Merge d931f8afc into eebd16ba4
Pull Request #116: Enhance and standardize logging across multiple components

313 of 1106 branches covered (28.3%)

Branch coverage included in aggregate %.

1048 of 3157 relevant lines covered (33.2%)

5.51 hits per line

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

91.35
src/main/java/com/knowledgepixels/registry/Utils.java
1
package com.knowledgepixels.registry;
2

3
import com.github.jsonldjava.shaded.com.google.common.base.Charsets;
4
import com.github.jsonldjava.shaded.com.google.common.reflect.TypeToken;
5
import com.google.common.hash.Hashing;
6
import com.google.gson.Gson;
7
import com.google.gson.JsonIOException;
8
import com.google.gson.JsonSyntaxException;
9
import com.mongodb.client.ClientSession;
10
import io.vertx.ext.web.RoutingContext;
11
import net.trustyuri.TrustyUriUtils;
12
import org.apache.commons.lang.StringUtils;
13
import org.commonjava.mimeparse.MIMEParse;
14
import org.eclipse.rdf4j.common.exception.RDF4JException;
15
import org.eclipse.rdf4j.model.IRI;
16
import org.eclipse.rdf4j.model.Literal;
17
import org.eclipse.rdf4j.model.Resource;
18
import org.eclipse.rdf4j.model.Statement;
19
import org.eclipse.rdf4j.model.util.Values;
20
import org.eclipse.rdf4j.model.vocabulary.FOAF;
21
import org.nanopub.MalformedNanopubException;
22
import org.nanopub.Nanopub;
23
import org.nanopub.NanopubImpl;
24
import org.nanopub.NanopubUtils;
25
import org.nanopub.extra.setting.IntroNanopub;
26
import org.nanopub.extra.setting.NanopubSetting;
27
import org.nanopub.vocabulary.NPX;
28
import org.slf4j.Logger;
29
import org.slf4j.LoggerFactory;
30

31
import java.io.File;
32
import java.io.IOException;
33
import java.io.InputStream;
34
import java.io.InputStreamReader;
35
import java.lang.reflect.Type;
36
import java.net.URI;
37
import java.net.URISyntaxException;
38
import java.net.URLEncoder;
39
import java.nio.charset.StandardCharsets;
40
import java.util.*;
41

42
/**
43
 * Utility class for the Nanopub Registry.
44
 */
45
public class Utils {
46

47
    private static final Logger logger = LoggerFactory.getLogger(Utils.class);
9✔
48

49
    private Utils() {
50
    }  // no instances allowed
51

52
    private static volatile String version;
53

54
    /**
55
     * Returns the registry's version (from Maven at build time, via a filtered
56
     * {@code version.properties} resource). Returns {@code "unknown"} if the
57
     * resource is unavailable.
58
     */
59
    public static String getVersion() {
60
        String v = version;
6✔
61
        if (v != null) {
6✔
62
            return v;
6✔
63
        }
64
        Properties p = new Properties();
12✔
65
        try (InputStream in = Utils.class.getResourceAsStream("/version.properties")) {
12✔
66
            if (in != null) {
6!
67
                p.load(in);
12✔
68
            } else {
69
                logger.warn("version.properties resource not found on classpath; version will be reported as 'unknown'");
×
70
            }
71
        } catch (IOException ex) {
×
72
            logger.warn("Could not read version.properties", ex);
×
73
        }
3✔
74
        v = p.getProperty("version", "unknown");
15✔
75
        version = v;
6✔
76
        logger.info("Registry version resolved to '{}'", v);
12✔
77
        return v;
6✔
78
    }
79

80
    public static String getMimeType(RoutingContext context, String supported) {
81
        List<String> supportedList = Arrays.asList(StringUtils.split(supported, ','));
15✔
82
        String mimeType = supportedList.getFirst();
12✔
83
        String acceptHeader = context.request().getHeader("Accept");
15✔
84
        if (acceptHeader == null) {
6✔
85
            logger.trace("No Accept header present; defaulting to '{}'", mimeType);
12✔
86
            return mimeType;
6✔
87
        }
88
        try {
89
            mimeType = MIMEParse.bestMatch(supportedList, acceptHeader);
12✔
90
            logger.trace("Resolved Accept header '{}' to mime type '{}' (supported: {})", acceptHeader, mimeType, supported);
51✔
91
        } catch (Exception ex) {
×
92
            logger.error("Failed to parse Accept header '{}': {}", acceptHeader, ex.getMessage(), ex);
×
93
        }
3✔
94
        return mimeType;
6✔
95
    }
96

97
    public static String urlEncode(Object o) {
98
        return URLEncoder.encode((o == null ? "" : o.toString()), StandardCharsets.UTF_8);
27✔
99
    }
100

101
    public static String getHash(String s) {
102
        return Hashing.sha256().hashString(s, Charsets.UTF_8).toString();
18✔
103
    }
104

105
    /**
106
     * Get the IDs of nanopublications invalidated by the given nanopublication.
107
     *
108
     * @param np the nanopublication to check
109
     * @return a set of IRI IDs of invalidated nanopublications
110
     */
111
    public static Set<IRI> getInvalidatedNanopubIds(Nanopub np) {
112
        Set<IRI> invalidatedNanopubs = new HashSet<>();
12✔
113
        for (Statement st : NanopubUtils.getStatements(np)) {
33✔
114
            if (!(st.getObject() instanceof IRI)) {
12✔
115
                continue;
3✔
116
            }
117
            Resource subject = st.getSubject();
9✔
118
            IRI predicate = st.getPredicate();
9✔
119
            if ((predicate.equals(NPX.RETRACTS) || predicate.equals(NPX.INVALIDATES)) || (predicate.equals(NPX.SUPERSEDES) && subject.equals(np.getUri()))) {
51!
120
                if (TrustyUriUtils.isPotentialTrustyUri(st.getObject().stringValue())) {
15!
121
                    invalidatedNanopubs.add((IRI) st.getObject());
18✔
122
                }
123
            }
124
        }
3✔
125
        if (!invalidatedNanopubs.isEmpty()) {
9✔
126
            logger.debug("Nanopub {} invalidates {} other nanopub(s): {}", np.getUri(), invalidatedNanopubs.size(), invalidatedNanopubs);
60✔
127
        }
128
        return invalidatedNanopubs;
6✔
129
    }
130

131
    private static ReadsEnvironment ENV_READER = new ReadsEnvironment(System::getenv);
15✔
132

133
    /**
134
     * Set the environment reader (used for testing purposes).
135
     *
136
     * @param reader the environment reader to set
137
     */
138
    public static void setEnvReader(ReadsEnvironment reader) {
139
        logger.debug("Environment reader replaced (likely test setup)");
9✔
140
        ENV_READER = reader;
6✔
141
    }
3✔
142

143
    /**
144
     * Get an environment variable, returning a default value if not set.
145
     *
146
     * @param name         the name of the environment variable
147
     * @param defaultValue the default value to return if the variable is not set
148
     * @return the value of the environment variable, or the default value if not set
149
     */
150
    public static String getEnv(String name, String defaultValue) {
151
        logger.debug("Reading environment variable '{}'", name);
12✔
152
        String value = ENV_READER.getEnv(name);
12✔
153
        if (value == null) {
6✔
154
            value = defaultValue;
6✔
155
            logger.debug("Environment variable '{}' not set; using default: '{}'", name, defaultValue);
15✔
156
        }
157
        return value;
6✔
158
    }
159

160
    /**
161
     * Get the type hash for a given type, recording it in the database if necessary.
162
     *
163
     * @param mongoSession the MongoDB client session
164
     * @param type         the type to get the hash for
165
     * @return the type hash
166
     */
167
    public static String getTypeHash(ClientSession mongoSession, Object type) {
168
        String typeHash = Utils.getHash(type.toString());
12✔
169
        if (type.toString().equals("$")) {
15✔
170
            typeHash = "$";
6✔
171
            logger.trace("Type '$' (unrestricted) mapped to special hash '$'");
12✔
172
        } else {
173
            logger.trace("Type '{}' hashed to '{}'; recording in database", type, typeHash);
15✔
174
            RegistryDB.recordHash(mongoSession, type.toString());
12✔
175
        }
176
        return typeHash;
6✔
177
    }
178

179
    /**
180
     * Get a label for an agent ID, truncating if necessary.
181
     *
182
     * @param agentId the agent ID
183
     * @return the agent label
184
     */
185
    public static String getAgentLabel(String agentId) {
186
        if (agentId == null || agentId.isBlank()) {
15✔
187
            logger.warn("getAgentLabel called with null or blank agentId");
9✔
188
            throw new IllegalArgumentException("Agent ID cannot be null or blank");
15✔
189
        }
190
        agentId = agentId.replaceFirst("^https://orcid\\.org/", "orcid:");
15✔
191
        if (agentId.length() > 55) {
12✔
192
            return agentId.substring(0, 50) + "...";
18✔
193
        }
194
        return agentId;
6✔
195
    }
196

197
    /**
198
     * Check if the given status indicates an unloaded entry.
199
     *
200
     * @param status the status to check
201
     * @return true if the status indicates an unloaded entry, false otherwise
202
     */
203
    public static boolean isUnloadedStatus(String status) {
204
        if (status.equals(EntryStatus.seen.getValue())) {
15✔
205
            return true;  // only exists in "accounts_loading"?
6✔
206
        }
207
        return status.equals(EntryStatus.skipped.getValue());
15✔
208
    }
209

210
    /**
211
     * Check if the given status indicates a core loaded entry.
212
     *
213
     * @param status the status to check
214
     * @return true if the status indicates a core loaded entry, false otherwise
215
     */
216
    public static boolean isCoreLoadedStatus(String status) {
217
        if (status.equals(EntryStatus.visited.getValue())) {
15✔
218
            return true;  // only exists in "accounts_loading"?
6✔
219
        }
220
        if (status.equals(EntryStatus.expanded.getValue())) {
15✔
221
            return true;  // only exists in "accounts_loading"?
6✔
222
        }
223
        if (status.equals(EntryStatus.processed.getValue())) {
15✔
224
            return true;  // only exists in "accounts_loading"?
6✔
225
        }
226
        if (status.equals(EntryStatus.aggregated.getValue())) {
15✔
227
            return true;  // only exists in "accounts_loading"?
6✔
228
        }
229
        if (status.equals(EntryStatus.approved.getValue())) {
15✔
230
            return true;  // only exists in "accounts_loading"?
6✔
231
        }
232
        if (status.equals(EntryStatus.contested.getValue())) {
15✔
233
            return true;
6✔
234
        }
235
        if (status.equals(EntryStatus.toLoad.getValue())) {
15✔
236
            return true;  // only exists in "accounts_loading"?
6✔
237
        }
238
        return status.equals(EntryStatus.loaded.getValue());
15✔
239
    }
240

241
    /**
242
     * Check if the given status indicates a fully loaded entry.
243
     *
244
     * @param status the status to check
245
     * @return true if the status indicates a fully loaded entry, false otherwise
246
     */
247
    public static boolean isFullyLoadedStatus(String status) {
248
        return status.equals(EntryStatus.loaded.getValue());
15✔
249
    }
250

251
    public static final IRI APPROVES_OF = Values.iri("http://purl.org/nanopub/x/approvesOf");
9✔
252

253
    public static final String TYPE_JSON = "application/json";
254
    public static final String TYPE_TRIG = "application/trig";
255
    public static final String TYPE_JELLY = "application/x-jelly-rdf";
256
    public static final String TYPE_JSONLD = "application/ld+json";
257
    public static final String TYPE_NQUADS = "application/n-quads";
258
    public static final String TYPE_TRIX = "application/trix";
259
    public static final String TYPE_HTML = "text/html";
260

261
    // Content types supported on a ListPage
262
    public static final String SUPPORTED_TYPES_LIST = TYPE_JSON + "," + TYPE_JELLY + "," + TYPE_HTML;
263
    // Content types supported on a NanopubPage
264
    public static final String SUPPORTED_TYPES_NANOPUB = TYPE_TRIG + "," + TYPE_JELLY + "," + TYPE_JSONLD + "," + TYPE_NQUADS + "," + TYPE_TRIX + "," + TYPE_HTML;
265

266
    private static Map<String, String> extensionTypeMap;
267

268
    /**
269
     * Get the type corresponding to a given file extension.
270
     *
271
     * @param extension the file extension
272
     * @return the corresponding MIME type, or null if not found
273
     */
274
    public static String getType(String extension) {
275
        if (extension == null) {
6✔
276
            return null;
6✔
277
        }
278
        if (extensionTypeMap == null) {
6✔
279
            extensionTypeMap = new HashMap<>();
12✔
280
            extensionTypeMap.put("trig", TYPE_TRIG);
15✔
281
            extensionTypeMap.put("jelly", TYPE_JELLY);
15✔
282
            extensionTypeMap.put("jsonld", TYPE_JSONLD);
15✔
283
            extensionTypeMap.put("nq", TYPE_NQUADS);
15✔
284
            extensionTypeMap.put("xml", TYPE_TRIX);
15✔
285
            extensionTypeMap.put("html", TYPE_HTML);
15✔
286
            extensionTypeMap.put("json", TYPE_JSON);
15✔
287
        }
288
        String type = extensionTypeMap.get(extension);
15✔
289
        if (type == null) {
6✔
290
            logger.debug("No known MIME type for extension '{}'", extension);
12✔
291
        }
292
        return type;
6✔
293
    }
294

295
    private static List<String> peerUrls;
296

297
    /**
298
     * Get the list of peer registry URLs.
299
     *
300
     * @return the list of peer registry URLs
301
     */
302
    public static List<String> getPeerUrls() {
303
        if (peerUrls == null) {
6✔
304
            peerUrls = new ArrayList<>();
12✔
305
            String envPeerUrls = getEnv("REGISTRY_PEER_URLS", "");
12✔
306
            String thisRegistryUrl = getEnv("REGISTRY_SERVICE_URL", "");
12✔
307
            if (!envPeerUrls.isEmpty()) {
9✔
308
                logger.debug("Resolving peer URLs from REGISTRY_PEER_URLS env var");
9✔
309
                for (String peerUrl : envPeerUrls.trim().split("\\s+")) {
60✔
310
                    if (thisRegistryUrl.equals(peerUrl)) {
12!
311
                        logger.debug("Excluding self ('{}') from peer URL list", peerUrl);
×
312
                        continue;
×
313
                    }
314
                    peerUrls.add(peerUrl);
12✔
315
                }
316
            } else {
317
                logger.debug("REGISTRY_PEER_URLS not set; resolving peer URLs from registry setting's bootstrap services");
9✔
318
                NanopubSetting setting;
319
                try {
320
                    setting = getSetting();
6✔
321
                } catch (MalformedNanopubException | IOException ex) {
3✔
322
                    logger.error("Failed to load registry setting from file", ex);
12✔
323
                    throw new RuntimeException(ex);
15✔
324
                }
3✔
325
                for (IRI iri : setting.getBootstrapServices()) {
33✔
326
                    String peerUrl = iri.stringValue();
9✔
327
                    if (thisRegistryUrl.equals(peerUrl)) {
12!
328
                        logger.debug("Excluding self ('{}') from peer URL list", peerUrl);
×
329
                        continue;
×
330
                    }
331
                    peerUrls.add(peerUrl);
12✔
332
                }
3✔
333
            }
334
            logger.info("Resolved {} peer URL(s): {}", peerUrls.size(), peerUrls);
21✔
335
        }
336
        return peerUrls;
6✔
337
    }
338

339
    private static volatile NanopubSetting settingNp;
340

341
    /**
342
     * Get the nanopublication setting.
343
     *
344
     * @return the nanopublication setting
345
     * @throws RDF4JException            if there is an error retrieving the setting
346
     * @throws MalformedNanopubException if the setting nanopublication is malformed
347
     * @throws IOException               if there is an I/O error
348
     */
349
    public static NanopubSetting getSetting() throws RDF4JException, MalformedNanopubException, IOException {
350
        if (settingNp == null) {
6✔
351
            synchronized (Utils.class) {
12✔
352
                if (settingNp == null) {
6!
353
                    String settingPath = getEnv("REGISTRY_SETTING_FILE", "/data/setting.trig");
12✔
354
                    logger.info("Loading registry setting from '{}'", settingPath);
12✔
355
                    try {
356
                        settingNp = new NanopubSetting(new NanopubImpl(new File(settingPath)));
33✔
357
                        logger.info("Registry setting loaded successfully from '{}'", settingPath);
12✔
358
                    } catch (RDF4JException | MalformedNanopubException | IOException ex) {
3✔
359
                        logger.error("Failed to load registry setting from '{}'", settingPath, ex);
15✔
360
                        throw ex;
6✔
361
                    }
3✔
362
                }
363
            }
9✔
364
        }
365
        return settingNp;
6✔
366
    }
367

368
    /**
369
     * Get a random peer registry URL.
370
     *
371
     * @return a random peer registry URL
372
     * @throws RDF4JException if there is an error retrieving the peer URLs
373
     */
374
    public static String getRandomPeer() throws RDF4JException {
375
        List<String> peerUrls = getPeerUrls();
6✔
376
        if (peerUrls.isEmpty()) {
9!
377
            logger.warn("getRandomPeer called but no peer URLs are configured");
×
378
        }
379
        String peer = peerUrls.get(random.nextInt(peerUrls.size()));
24✔
380
        logger.trace("Selected random peer: '{}'", peer);
12✔
381
        return peer;
6✔
382
    }
383

384
    private static final Random random = new Random();
12✔
385

386
    /**
387
     * Get the random number generator.
388
     *
389
     * @return the random number generator
390
     */
391
    public static Random getRandom() {
392
        return random;
6✔
393
    }
394

395
    private static final Gson g = new Gson();
12✔
396
    private static Type listType = new TypeToken<List<String>>() {
21✔
397
    }.getType();
6✔
398

399
    /**
400
     * Retrieve a list of strings from a JSON URL.
401
     *
402
     * @param url the URL to retrieve the JSON from
403
     * @return the list of strings
404
     * @throws JsonIOException     if there is an error reading the JSON
405
     * @throws JsonSyntaxException if the JSON syntax is invalid
406
     * @throws IOException         if there is an I/O error
407
     * @throws URISyntaxException  if the URL syntax is invalid
408
     */
409
    public static List<String> retrieveListFromJsonUrl(String url) throws JsonIOException, JsonSyntaxException, IOException, URISyntaxException {
410
        logger.debug("Retrieving JSON list from '{}'", url);
12✔
411
        try {
412
            List<String> result = g.fromJson(new InputStreamReader(new URI(url).toURL().openStream()), listType);
×
413
            logger.debug("Retrieved {} entries from '{}'", result == null ? 0 : result.size(), url);
×
414
            return result;
×
415
        } catch (JsonIOException | JsonSyntaxException | IOException | URISyntaxException ex) {
3✔
416
            logger.warn("Failed to retrieve or parse JSON list from '{}': {}", url, ex.getMessage());
18✔
417
            throw ex;
6✔
418
        }
419
    }
420

421
    /**
422
     * Extracts the {@code foaf:name} literal asserted on the intro's user IRI.
423
     * Returns {@code null} when the assertion declares no such name. When multiple
424
     * {@code foaf:name} literals are asserted on the same agent, the lexicographic
425
     * minimum is returned for deterministic behaviour across rebuilds.
426
     */
427
    public static String extractIntroName(IntroNanopub agentIntro) {
428
        IRI agentIri = agentIntro.getUser();
9✔
429
        if (agentIri == null) {
6✔
430
            logger.debug("Intro nanopub has no user IRI; cannot extract name");
9✔
431
            return null;
6✔
432
        }
433
        String chosen = null;
6✔
434
        for (Statement st : agentIntro.getNanopub().getAssertion()) {
36✔
435
            if (!st.getSubject().equals(agentIri)) {
15✔
436
                continue;
3✔
437
            }
438
            if (!st.getPredicate().equals(FOAF.NAME)) {
15✔
439
                continue;
3✔
440
            }
441
            if (!(st.getObject() instanceof Literal)) {
12✔
442
                continue;
3✔
443
            }
444
            String candidate = st.getObject().stringValue();
12✔
445
            if (chosen == null || candidate.compareTo(chosen) < 0) {
18✔
446
                chosen = candidate;
6✔
447
            }
448
        }
3✔
449
        return chosen;
6✔
450
    }
451

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