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

Nanopublication / nanopub-java / 17376074091

01 Sep 2025 11:19AM UTC coverage: 45.497% (+0.02%) from 45.474%
17376074091

push

github

ashleycaselli
fix: add APINotReachable and NotEnoughAPIInstances exceptions and relative tests

825 of 2878 branches covered (28.67%)

Branch coverage included in aggregate %.

4606 of 9059 relevant lines covered (50.84%)

2.43 hits per line

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

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

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

11
import java.io.IOException;
12
import java.net.URLEncoder;
13
import java.util.ArrayList;
14
import java.util.LinkedList;
15
import java.util.List;
16
import java.util.Map;
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
        if (resp.getEntity().getContentLength() < 0) return false;
×
158
        return true;
×
159
    }
160

161

162
    private class Call implements Runnable {
163

164
        private String apiUrl;
165
        private HttpGet get;
166

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

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

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

194
    }
195

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

© 2025 Coveralls, Inc