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

knowledgepixels / nanopub-registry / 23539426481

25 Mar 2026 11:47AM UTC coverage: 30.645% (+0.2%) from 30.443%
23539426481

push

github

web-flow
Merge pull request #88 from knowledgepixels/perf/eliminate-redundant-signature-verification

perf: eliminate redundant signature verification in POST and peer sync

190 of 700 branches covered (27.14%)

Branch coverage included in aggregate %.

646 of 2028 relevant lines covered (31.85%)

5.39 hits per line

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

10.43
src/main/java/com/knowledgepixels/registry/NanopubLoader.java
1
package com.knowledgepixels.registry;
2

3
import com.mongodb.ErrorCategory;
4
import com.mongodb.MongoWriteException;
5
import com.mongodb.client.ClientSession;
6
import com.mongodb.client.MongoCursor;
7
import net.trustyuri.TrustyUriUtils;
8
import net.trustyuri.rdf.RdfModule;
9
import org.apache.http.Header;
10
import org.apache.http.HttpResponse;
11
import org.apache.http.client.HttpClient;
12
import org.apache.http.client.methods.CloseableHttpResponse;
13
import org.apache.http.client.methods.HttpGet;
14
import org.apache.http.util.EntityUtils;
15
import org.bson.Document;
16
import org.bson.types.Binary;
17
import org.eclipse.rdf4j.common.exception.RDF4JException;
18
import org.eclipse.rdf4j.rio.RDFFormat;
19
import org.nanopub.MalformedNanopubException;
20
import org.nanopub.Nanopub;
21
import org.nanopub.NanopubImpl;
22
import org.nanopub.NanopubUtils;
23
import org.nanopub.extra.server.GetNanopub;
24
import org.nanopub.jelly.JellyUtils;
25
import org.nanopub.jelly.MaybeNanopub;
26
import org.nanopub.jelly.NanopubStream;
27
import org.nanopub.trusty.TrustyNanopubUtils;
28
import org.nanopub.vocabulary.NPX;
29
import org.slf4j.Logger;
30
import org.slf4j.LoggerFactory;
31

32
import java.io.IOException;
33
import java.io.InputStream;
34
import java.util.ArrayList;
35
import java.util.Collections;
36
import java.util.List;
37
import java.util.stream.Stream;
38

39
import static com.knowledgepixels.registry.RegistryDB.has;
40
import static com.knowledgepixels.registry.RegistryDB.insert;
41

42
public class NanopubLoader {
43

44
    private NanopubLoader() {
45
    }
46

47
    public final static String INTRO_TYPE = NPX.DECLARED_BY.stringValue();
9✔
48
    public final static String INTRO_TYPE_HASH = Utils.getHash(INTRO_TYPE);
9✔
49
    public final static String ENDORSE_TYPE = Utils.APPROVES_OF.stringValue();
9✔
50
    public final static String ENDORSE_TYPE_HASH = Utils.getHash(ENDORSE_TYPE);
9✔
51
    private static final Logger log = LoggerFactory.getLogger(NanopubLoader.class);
12✔
52

53
    // TODO Distinguish and support these cases:
54
    //      1. Simple load: load to all core lists if pubkey is "core-loaded", or load to all lists if pubkey is "full-loaded"
55
    //      2. Core load: load to all core lists (initialize if needed), or load to all lists if pubkey is "full-loaded"
56
    //      3. Full load: load to all lists (initialize if needed)
57

58
    public static void simpleLoad(ClientSession mongoSession, String nanopubId) {
59
        simpleLoad(mongoSession, retrieveNanopub(mongoSession, nanopubId));
×
60
    }
×
61

62
    public static void simpleLoad(ClientSession mongoSession, Nanopub np) {
63
        String pubkey = RegistryDB.getPubkey(np);
×
64
        if (pubkey == null) {
×
65
            log.info("Ignore (not signed): {}", np.getUri());
×
66
            return;
×
67
        }
68
        simpleLoad(mongoSession, np, pubkey);
×
69
    }
×
70

71
    /**
72
     * Loads a nanopub to the appropriate lists, using a pre-verified public key
73
     * to skip redundant signature verification.
74
     */
75
    public static void simpleLoad(ClientSession mongoSession, Nanopub np, String verifiedPubkey) {
76
        String pubkeyHash = Utils.getHash(verifiedPubkey);
9✔
77
        // TODO Do we need to load anything else here, into the other DB collections?
78
        if (has(mongoSession, "lists", new Document("pubkey", pubkeyHash).append("type", "$").append("status", "loaded"))) {
45!
79
            RegistryDB.loadNanopubVerified(mongoSession, np, verifiedPubkey, pubkeyHash, "$");
×
80
        } else if (has(mongoSession, "lists", new Document("pubkey", pubkeyHash).append("type", INTRO_TYPE_HASH).append("status", "loaded"))) {
45!
81
            RegistryDB.loadNanopubVerified(mongoSession, np, verifiedPubkey, pubkeyHash, INTRO_TYPE, ENDORSE_TYPE);
×
82
        } else if (!has(mongoSession, "lists", new Document("pubkey", pubkeyHash).append("type", INTRO_TYPE_HASH))) {
36!
83
            // Unknown pubkey: create encountered intro list so RUN_OPTIONAL_LOAD picks it up
84
            try {
85
                insert(mongoSession, "lists", new Document("pubkey", pubkeyHash)
30✔
86
                        .append("type", INTRO_TYPE_HASH)
9✔
87
                        .append("status", EntryStatus.encountered.getValue()));
6✔
88
            } catch (MongoWriteException e) {
×
89
                if (e.getError().getCategory() != ErrorCategory.DUPLICATE_KEY) throw e;
×
90
            }
3✔
91
        }
92
    }
3✔
93

94
    /**
95
     * Retrieve Nanopubs from the peers of this Nanopub Registry.
96
     *
97
     * @param typeHash   The hash of the type of the Nanopub to retrieve.
98
     * @param pubkeyHash The hash of the pubkey of the Nanopub to retrieve.
99
     * @return A stream of MaybeNanopub objects, or an empty stream if no peer is available.
100
     */
101
    public static Stream<MaybeNanopub> retrieveNanopubsFromPeers(String typeHash, String pubkeyHash) {
102
        // TODO Move the code of this method to nanopub-java library.
103

104
        List<String> peerUrlsToTry = new ArrayList<>(Utils.getPeerUrls());
×
105
        Collections.shuffle(peerUrlsToTry);
×
106
        while (!peerUrlsToTry.isEmpty()) {
×
107
            String peerUrl = peerUrlsToTry.removeFirst();
×
108

109
            String requestUrl = peerUrl + "list/" + pubkeyHash + "/" + typeHash + ".jelly";
×
110
            log.info("Request: {}", requestUrl);
×
111
            try {
112
                CloseableHttpResponse resp = NanopubUtils.getHttpClient().execute(new HttpGet(requestUrl));
×
113
                int httpStatus = resp.getStatusLine().getStatusCode();
×
114
                if (httpStatus < 200 || httpStatus >= 300) {
×
115
                    log.info("Request failed: {} {}", peerUrl, httpStatus);
×
116
                    EntityUtils.consumeQuietly(resp.getEntity());
×
117
                    continue;
×
118
                }
119
                Header nrStatus = resp.getFirstHeader("Nanopub-Registry-Status");
×
120
                if (nrStatus == null) {
×
121
                    log.info("Nanopub-Registry-Status header not found at: {}", peerUrl);
×
122
                    EntityUtils.consumeQuietly(resp.getEntity());
×
123
                    continue;
×
124
                } else if (!nrStatus.getValue().equals("ready") && !nrStatus.getValue().equals("updating")) {
×
125
                    log.info("Peer in non-ready state: {} {}", peerUrl, nrStatus.getValue());
×
126
                    EntityUtils.consumeQuietly(resp.getEntity());
×
127
                    continue;
×
128
                }
129
                InputStream is = resp.getEntity().getContent();
×
130
                return NanopubStream.fromByteStream(is).getAsNanopubs().onClose(() -> {
×
131
                    try {
132
                        resp.close();
×
133
                    } catch (IOException e) {
×
134
                        log.debug("Error closing HTTP response", e);
×
135
                    }
×
136
                });
×
137
            } catch (UnsupportedOperationException | IOException ex) {
×
138
                log.info("Request failed: ", ex);
×
139
            }
140
        }
×
141
        return Stream.empty();
×
142
    }
143

144
    public static Nanopub retrieveNanopub(ClientSession mongoSession, String nanopubId) {
145
        Nanopub np = retrieveLocalNanopub(mongoSession, nanopubId);
×
146
        int tryCount = 0;
×
147
        while (np == null) {
×
148
            if (tryCount > 10) {
×
149
                throw new RuntimeException("Could not load nanopub: " + nanopubId);
×
150
            } else if (tryCount > 0) {
×
151
                try {
152
                    Thread.sleep(100);
×
153
                } catch (InterruptedException ex) {
×
154
                    log.info("Thread was interrupted", ex);
×
155
                }
×
156
            }
157
            log.info("Loading {}", nanopubId);
×
158

159
            // TODO Reach out to other Nanopub Registries here:
160
            np = getNanopub(nanopubId);
×
161
            if (np != null) {
×
162
                RegistryDB.loadNanopub(mongoSession, np);
×
163
            } else {
164
                tryCount = tryCount + 1;
×
165
            }
166
        }
167
        return np;
×
168
    }
169

170
    public static Nanopub retrieveLocalNanopub(ClientSession mongoSession, String nanopubId) {
171
        String ac = TrustyUriUtils.getArtifactCode(nanopubId);
×
172
        MongoCursor<Document> cursor = RegistryDB.get(mongoSession, Collection.NANOPUBS.toString(), new Document("_id", ac));
×
173
        if (!cursor.hasNext()) return null;
×
174
        try {
175
            // Parse from Jelly, not TriG (it's faster)
176
            return JellyUtils.readFromDB(((Binary) cursor.next().get("jelly")).getData());
×
177
        } catch (RDF4JException | MalformedNanopubException ex) {
×
178
            log.info("Exception reading Jelly", ex);
×
179
            return null;
×
180
        }
181
    }
182

183
    // TODO Provide this method in nanopub-java (GetNanopub)
184
    private static Nanopub getNanopub(String uriOrArtifactCode) {
185
        List<String> peerUrls = new ArrayList<>(Utils.getPeerUrls());
×
186
        Collections.shuffle(peerUrls);
×
187
        String ac = GetNanopub.getArtifactCode(uriOrArtifactCode);
×
188
        if (!ac.startsWith(RdfModule.MODULE_ID)) {
×
189
            throw new IllegalArgumentException("Not a trusty URI of type RA");
×
190
        }
191
        while (!peerUrls.isEmpty()) {
×
192
            String peerUrl = peerUrls.removeFirst();
×
193
            try {
194
                Nanopub np = get(ac, peerUrl, NanopubUtils.getHttpClient());
×
195
                if (np != null) {
×
196
                    return np;
×
197
                }
198
            } catch (IOException | RDF4JException | MalformedNanopubException ex) {
×
199
                // ignore
200
            }
×
201
        }
×
202
        return null;
×
203
    }
204

205
    // TODO Provide this method in nanopub-java (GetNanopub)
206
    private static Nanopub get(String artifactCode, String registryUrl, HttpClient httpClient)
207
            throws IOException, RDF4JException, MalformedNanopubException {
208
        HttpGet get = null;
×
209
        // TODO Get in Jelly format:
210
        String getUrl = registryUrl + "np/" + artifactCode;
×
211
        try {
212
            get = new HttpGet(getUrl);
×
213
        } catch (IllegalArgumentException ex) {
×
214
            throw new IOException("invalid URL: " + getUrl);
×
215
        }
×
216
        get.setHeader("Accept", "application/trig");
×
217
        InputStream in = null;
×
218
        try {
219
            HttpResponse resp = httpClient.execute(get);
×
220
            if (!wasSuccessful(resp)) {
×
221
                EntityUtils.consumeQuietly(resp.getEntity());
×
222
                throw new IOException(resp.getStatusLine().toString());
×
223
            }
224
            in = resp.getEntity().getContent();
×
225
            Nanopub nanopub = new NanopubImpl(in, RDFFormat.TRIG);
×
226
            if (!TrustyNanopubUtils.isValidTrustyNanopub(nanopub)) {
×
227
                throw new MalformedNanopubException("Nanopub is not trusty");
×
228
            }
229
            return nanopub;
×
230
        } finally {
231
            if (in != null) in.close();
×
232
        }
233
    }
234

235
    private static boolean wasSuccessful(HttpResponse resp) {
236
        int c = resp.getStatusLine().getStatusCode();
×
237
        return c >= 200 && c < 300;
×
238
    }
239

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