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

Nanopublication / nanopub-java / 17800476443

17 Sep 2025 02:11PM UTC coverage: 50.885% (-0.009%) from 50.894%
17800476443

push

github

tkuhn
fix: Improve failing HTTP response detection

960 of 2868 branches covered (33.47%)

Branch coverage included in aggregate %.

5077 of 8996 relevant lines covered (56.44%)

2.65 hits per line

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

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

3
import java.io.IOException;
4
import java.net.URLEncoder;
5
import java.util.ArrayList;
6
import java.util.LinkedList;
7
import java.util.List;
8
import java.util.Map;
9

10
import org.apache.commons.codec.Charsets;
11
import org.apache.http.HttpResponse;
12
import org.apache.http.client.methods.HttpGet;
13
import org.apache.http.util.EntityUtils;
14
import org.nanopub.NanopubUtils;
15
import org.slf4j.Logger;
16
import org.slf4j.LoggerFactory;
17

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

23
    private static int parallelCallCount = 2;
2✔
24
    private static int maxRetryCount = 3;
2✔
25
    private static final Logger logger = LoggerFactory.getLogger(QueryCall.class);
3✔
26

27
    /**
28
     * Run a query call with the given query ID and parameters.
29
     *
30
     * @param queryId the ID of the query to run
31
     * @param params  the parameters to pass to the query
32
     * @return the HTTP response from the query API
33
     */
34
    public static HttpResponse run(String queryId, Map<String, String> params) throws APINotReachableException, NotEnoughAPIInstancesException {
35
        int retryCount = 0;
2✔
36
        while (retryCount < maxRetryCount) {
3!
37
            QueryCall apiCall = new QueryCall(queryId, params);
×
38
            apiCall.run();
×
39
            while (!apiCall.calls.isEmpty() && apiCall.resp == null) {
×
40
                try {
41
                    Thread.sleep(200);
×
42
                } catch (InterruptedException ex) {
×
43
                    Thread.currentThread().interrupt();
×
44
                }
×
45
            }
46
            if (apiCall.resp != null) {
×
47
                return apiCall.resp;
×
48
            }
49
            retryCount = retryCount + 1;
×
50
        }
×
51
        throw new APINotReachableException("Giving up contacting API: " + queryId);
×
52
    }
53

54
    /**
55
     * List of available query API instances.
56
     */
57
    // TODO Available services should be retrieved from a setting, not hard-coded:
58
    public static String[] queryApiInstances = new String[]{
16✔
59
            "https://query.knowledgepixels.com/",
60
            "https://query.petapico.org/",
61
            "https://query.np.trustyuri.net/"
62
    };
63

64
    private static List<String> checkedApiInstances;
65

66
    /**
67
     * Returns the list of available query API instances that are currently accessible.
68
     *
69
     * @return a list of accessible query API instance
70
     */
71
    public static List<String> getApiInstances() throws NotEnoughAPIInstancesException {
72
        if (checkedApiInstances != null) return checkedApiInstances;
4✔
73
        checkedApiInstances = new ArrayList<>();
4✔
74
        for (String a : queryApiInstances) {
16✔
75
            try {
76
                logger.info("Checking API instance: {}", a);
4✔
77
                HttpResponse resp = NanopubUtils.getHttpClient().execute(new HttpGet(a));
7✔
78
                if (wasSuccessful(resp)) {
3✔
79
                    logger.info("SUCCESS: Nanopub Query instance is accessible: {}", a);
4✔
80
                    checkedApiInstances.add(a);
5✔
81
                } else {
82
                    logger.error("FAILURE: Nanopub Query instance isn't accessible: {}", a);
4✔
83
                }
84
            } catch (IOException ex) {
×
85
                logger.error("FAILURE: Nanopub Query instance isn't accessible: {}", a);
×
86
            }
1✔
87
        }
88
        logger.info("{} accessible Nanopub Query instances", checkedApiInstances.size());
6✔
89
        if (checkedApiInstances.size() < 2) {
4✔
90
            checkedApiInstances = null;
2✔
91
            throw new NotEnoughAPIInstancesException("Not enough healthy Nanopub Query instances available");
5✔
92
        }
93
        return checkedApiInstances;
2✔
94
    }
95

96
    private String queryId;
97
    private String paramString;
98
    private List<String> apisToCall = new ArrayList<>();
5✔
99
    private List<Call> calls = new ArrayList<>();
5✔
100

101
    private HttpResponse resp;
102

103
    private QueryCall(String queryId, Map<String, String> params) {
2✔
104
        this.queryId = queryId;
3✔
105
        paramString = "";
3✔
106
        if (params != null) {
2!
107
            paramString = "?";
3✔
108
            for (String k : params.keySet()) {
11!
109
                if (paramString.length() > 1) paramString += "&";
5!
110
                paramString += k + "=";
6✔
111
                paramString += URLEncoder.encode(params.get(k), Charsets.UTF_8);
×
112
            }
×
113
        }
114
        logger.info("Invoking API operation {} {}", queryId, paramString);
×
115
    }
×
116

117
    private void run() throws NotEnoughAPIInstancesException {
118
        List<String> apiInstancesToTry = new LinkedList<>(getApiInstances());
×
119
        while (!apiInstancesToTry.isEmpty() && apisToCall.size() < parallelCallCount) {
×
120
            int randomIndex = (int) ((Math.random() * apiInstancesToTry.size()));
×
121
            String apiUrl = apiInstancesToTry.get(randomIndex);
×
122
            apisToCall.add(apiUrl);
×
123
            logger.info("Trying API ({}) {}", apisToCall.size(), apiUrl);
×
124
            apiInstancesToTry.remove(randomIndex);
×
125
        }
×
126
        for (String api : apisToCall) {
×
127
            Call call = new Call(api);
×
128
            calls.add(call);
×
129
            new Thread(call).start();
×
130
        }
×
131
    }
×
132

133
    private synchronized void finished(Call call, HttpResponse resp, String apiUrl) {
134
        if (this.resp != null) { // result already in
×
135
            EntityUtils.consumeQuietly(resp.getEntity());
×
136
            return;
×
137
        }
138
        logger.info("Result in from {}:", apiUrl);
×
139
        logger.info("- Request: {} {}", queryId, paramString);
×
140
        logger.info("- Response size: {}", resp.getEntity().getContentLength());
×
141
        this.resp = resp;
×
142

143
        for (Call c : calls) {
×
144
            if (c != call) c.abort();
×
145
        }
×
146
    }
×
147

148
    private static boolean wasSuccessful(HttpResponse resp) {
149
        if (resp == null || resp.getEntity() == null) return false;
5!
150
        int c = resp.getStatusLine().getStatusCode();
4✔
151
        if (c < 200 || c >= 300) return false;
8!
152
        return true;
2✔
153
    }
154

155
    private static boolean wasSuccessfulNonempty(HttpResponse resp) {
156
        if (!wasSuccessful(resp)) return false;
×
157
        // TODO Make sure we always return proper error codes, and then this shouldn't be necessary:
158
        if (resp.getHeaders("Content-Length").length > 0 && resp.getEntity().getContentLength() < 0) return false;
×
159
        return true;
×
160
    }
161

162

163
    private class Call implements Runnable {
164

165
        private String apiUrl;
166
        private HttpGet get;
167

168
        public Call(String apiUrl) {
×
169
            this.apiUrl = apiUrl;
×
170
        }
×
171

172
        public void run() {
173
            get = new HttpGet(apiUrl + "api/" + queryId + paramString);
×
174
            get.setHeader("Accept", "text/csv");
×
175
            HttpResponse resp = null;
×
176
            try {
177
                resp = NanopubUtils.getHttpClient().execute(get);
×
178
                if (!wasSuccessfulNonempty(resp)) {
×
179
                    throw new IOException(resp.getStatusLine().toString());
×
180
                }
181
                finished(this, resp, apiUrl);
×
182
            } catch (Exception ex) {
×
183
                if (resp != null) EntityUtils.consumeQuietly(resp.getEntity());
×
184
                logger.error("Request to {} was not successful: {}", apiUrl, ex.getMessage());
×
185
            }
×
186
            calls.remove(this);
×
187
        }
×
188

189
        private void abort() {
190
            if (get == null) return;
×
191
            if (get.isAborted()) return;
×
192
            get.abort();
×
193
        }
×
194

195
    }
196

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