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

Nanopublication / nanopub-java / 25657329499

11 May 2026 07:49AM UTC coverage: 53.252% (+1.1%) from 52.184%
25657329499

push

github

web-flow
Merge pull request #78 from Nanopublication/feature/setting-driven-query-instances

feat(services): discover query API instances from nanopub setting

1206 of 3196 branches covered (37.73%)

Branch coverage included in aggregate %.

5573 of 9534 relevant lines covered (58.45%)

8.19 hits per line

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

82.84
src/main/java/org/nanopub/extra/services/QueryCall.java
1
package org.nanopub.extra.services;
2

3
import org.apache.http.HttpResponse;
4
import org.apache.http.client.methods.HttpGet;
5
import org.apache.http.util.EntityUtils;
6
import org.nanopub.NanopubUtils;
7
import org.nanopub.vocabulary.NPS;
8
import org.slf4j.Logger;
9
import org.slf4j.LoggerFactory;
10

11
import java.io.IOException;
12
import java.util.ArrayList;
13
import java.util.LinkedList;
14
import java.util.List;
15

16
/**
17
 * Second-generation query API call.
18
 */
19
public class QueryCall {
20

21
    private static int parallelCallCount = 2;
6✔
22
    private static int maxRetryCount = 3;
6✔
23
    private static final Logger logger = LoggerFactory.getLogger(QueryCall.class);
12✔
24

25
    /**
26
     * Run a query call with the given query ID and parameters.
27
     *
28
     * @param queryRef the reference to the query to run
29
     * @return the HTTP response from the query API
30
     * @throws APINotReachableException       if the API is not reachable after retries
31
     * @throws NotEnoughAPIInstancesException if there are not enough API instances available
32
     */
33
    public static HttpResponse run(QueryRef queryRef) throws APINotReachableException, NotEnoughAPIInstancesException {
34
        int retryCount = 0;
6✔
35
        while (retryCount < maxRetryCount) {
9!
36
            QueryCall apiCall = new QueryCall(queryRef);
15✔
37
            apiCall.run();
6✔
38
            while (!apiCall.calls.isEmpty() && apiCall.resp == null) {
21!
39
                try {
40
                    Thread.sleep(200);
6✔
41
                } catch (InterruptedException ex) {
×
42
                    Thread.currentThread().interrupt();
×
43
                }
3✔
44
            }
45
            if (apiCall.resp != null) {
9!
46
                return apiCall.resp;
9✔
47
            }
48
            retryCount = retryCount + 1;
×
49
        }
×
50
        throw new APINotReachableException("Giving up contacting API: " + queryRef.getQueryId());
×
51
    }
52

53
    /**
54
     * System property naming a whitespace-separated list of query API instance URLs.
55
     * When set, this overrides discovery via the nanopub setting (env var
56
     * {@code NANOPUB_QUERY_INSTANCES} also accepted).
57
     */
58
    public static final String QUERY_INSTANCES_PROPERTY = "nanopub.query.instances";
59

60
    /**
61
     * Environment variable equivalent of {@link #QUERY_INSTANCES_PROPERTY}.
62
     */
63
    public static final String QUERY_INSTANCES_ENV = "NANOPUB_QUERY_INSTANCES";
64

65
    private static List<String> checkedApiInstances;
66

67
    /**
68
     * Returns the list of available query API instances that are currently accessible.
69
     * <p>
70
     * Sources, in order of priority:
71
     * <ol>
72
     *   <li>{@code nanopub.query.instances} system property / {@code NANOPUB_QUERY_INSTANCES} env var
73
     *       (whitespace-separated URLs).</li>
74
     *   <li>The active {@link org.nanopub.extra.setting.NanopubSetting}'s service intro collection,
75
     *       filtered to services of type {@link NPS#NANOPUB_QUERY_1_1}.</li>
76
     * </ol>
77
     * Each candidate is liveness-checked via an HTTP GET to its root URL.
78
     *
79
     * @return a list of accessible query API instances
80
     */
81
    public static List<String> getApiInstances() throws NotEnoughAPIInstancesException {
82
        if (checkedApiInstances != null) return checkedApiInstances;
12✔
83
        List<String> candidates = resolveCandidateInstances();
6✔
84
        if (candidates.isEmpty()) {
9!
85
            throw new NotEnoughAPIInstancesException("No query API instances configured or discoverable");
×
86
        }
87
        checkedApiInstances = new ArrayList<>();
12✔
88
        for (String a : candidates) {
30✔
89
            try {
90
                logger.info("Checking API instance: {}", a);
12✔
91
                HttpResponse resp = NanopubUtils.getHttpClient().execute(new HttpGet(a));
21✔
92
                EntityUtils.consumeQuietly(resp.getEntity());
9✔
93
                if (wasSuccessful(resp)) {
9✔
94
                    logger.info("SUCCESS: Nanopub Query instance is accessible: {}", a);
12✔
95
                    checkedApiInstances.add(a);
15✔
96
                } else {
97
                    logger.error("FAILURE: Nanopub Query instance isn't accessible: {}", a);
12✔
98
                }
99
            } catch (IOException ex) {
×
100
                logger.error("FAILURE: Nanopub Query instance isn't accessible: {}", a);
×
101
            }
3✔
102
        }
3✔
103
        logger.info("{} accessible Nanopub Query instances", checkedApiInstances.size());
18✔
104
        if (checkedApiInstances.size() < 2) {
12✔
105
            checkedApiInstances = null;
6✔
106
            throw new NotEnoughAPIInstancesException("Not enough healthy Nanopub Query instances available");
15✔
107
        }
108
        return checkedApiInstances;
6✔
109
    }
110

111
    private static List<String> resolveCandidateInstances() {
112
        String override = System.getProperty(QUERY_INSTANCES_PROPERTY);
9✔
113
        if (override == null || override.isEmpty()) override = System.getenv(QUERY_INSTANCES_ENV);
24!
114
        if (override != null && !override.trim().isEmpty()) {
18!
115
            List<String> list = new ArrayList<>();
12✔
116
            for (String url : override.trim().split("\\s+")) list.add(url);
69✔
117
            logger.info("Using {} query API instance(s) from override", list.size());
18✔
118
            return list;
6✔
119
        }
120
        List<String> fromSetting = ServiceLookup.getServices(NPS.NANOPUB_QUERY_1_1);
9✔
121
        logger.info("Discovered {} query API instance(s) from setting", fromSetting.size());
18✔
122
        return new ArrayList<>(fromSetting);
15✔
123
    }
124

125
    private QueryRef queryRef;
126
    private List<String> apisToCall = new ArrayList<>();
15✔
127
    private List<Call> calls = new ArrayList<>();
15✔
128

129
    private HttpResponse resp;
130

131
    private QueryCall(QueryRef queryRef) {
6✔
132
        this.queryRef = queryRef;
9✔
133
        logger.info("Invoking API operation {}", queryRef);
12✔
134
    }
3✔
135

136
    private void run() throws NotEnoughAPIInstancesException {
137
        List<String> apiInstancesToTry = new LinkedList<>(getApiInstances());
15✔
138
        while (!apiInstancesToTry.isEmpty() && apisToCall.size() < parallelCallCount) {
24!
139
            int randomIndex = (int) ((Math.random() * apiInstancesToTry.size()));
21✔
140
            String apiUrl = apiInstancesToTry.get(randomIndex);
15✔
141
            apisToCall.add(apiUrl);
15✔
142
            logger.info("Trying API ({}) {}", apisToCall.size(), apiUrl);
24✔
143
            apiInstancesToTry.remove(randomIndex);
12✔
144
        }
3✔
145
        for (String api : apisToCall) {
33✔
146
            Call call = new Call(api);
18✔
147
            calls.add(call);
15✔
148
            new Thread(call).start();
15✔
149
        }
3✔
150
    }
3✔
151

152
    private synchronized void finished(Call call, HttpResponse resp, String apiUrl) {
153
        if (this.resp != null) { // result already in
9!
154
            EntityUtils.consumeQuietly(resp.getEntity());
×
155
            return;
×
156
        }
157
        logger.info("Result in from {}:", apiUrl);
12✔
158
        logger.info("- Request: {}", queryRef);
15✔
159
        logger.info("- Response size: {}", resp.getEntity().getContentLength());
21✔
160
        this.resp = resp;
9✔
161

162
        for (Call c : calls) {
33✔
163
            if (c != call) c.abort();
15✔
164
        }
3✔
165
    }
3✔
166

167
    private static boolean wasSuccessful(HttpResponse resp) {
168
        if (resp == null || resp.getEntity() == null) return false;
15!
169
        int c = resp.getStatusLine().getStatusCode();
12✔
170
        if (c < 200 || c >= 300) return false;
24!
171
        return true;
6✔
172
    }
173

174
    private static boolean wasSuccessfulNonempty(HttpResponse resp) {
175
        if (!wasSuccessful(resp)) return false;
9!
176
        // TODO Make sure we always return proper error codes, and then this shouldn't be necessary:
177
        if (resp.getHeaders("Content-Length").length > 0 && resp.getEntity().getContentLength() < 0) return false;
33!
178
        return true;
6✔
179
    }
180

181

182
    private class Call implements Runnable {
183

184
        private String apiUrl;
185
        private HttpGet get;
186

187
        public Call(String apiUrl) {
15✔
188
            this.apiUrl = apiUrl;
9✔
189
        }
3✔
190

191
        public void run() {
192
            get = new HttpGet(apiUrl + "api/" + queryRef.getAsUrlString());
36✔
193
            get.setHeader("Accept", "text/csv, text/turtle;q=0.9, application/ld+json;q=0.8");
15✔
194
            HttpResponse resp = null;
6✔
195
            try {
196
                resp = NanopubUtils.getHttpClient().execute(get);
15✔
197
                if (!wasSuccessfulNonempty(resp)) {
9!
198
                    throw new IOException(resp.getStatusLine().toString());
×
199
                }
200
                finished(this, resp, apiUrl);
21✔
201
            } catch (Exception ex) {
3✔
202
                if (resp != null) EntityUtils.consumeQuietly(resp.getEntity());
6!
203
                logger.error("Request to {} was not successful: {}", apiUrl, ex.getMessage());
21✔
204
            }
3✔
205
            calls.remove(this);
18✔
206
        }
3✔
207

208
        private void abort() {
209
            if (get == null) return;
9!
210
            if (get.isAborted()) return;
12!
211
            get.abort();
9✔
212
        }
3✔
213

214
    }
215

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