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

Nanopublication / nanopub-java / 20413618139

21 Dec 2025 05:52PM UTC coverage: 51.692% (+0.01%) from 51.679%
20413618139

push

github

ashleycaselli
docs: update/fix Javadoc annotations

1013 of 2942 branches covered (34.43%)

Branch coverage included in aggregate %.

5221 of 9118 relevant lines covered (57.26%)

8.05 hits per line

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

83.92
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.slf4j.Logger;
8
import org.slf4j.LoggerFactory;
9

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

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

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

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

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

62
    private static List<String> checkedApiInstances;
63

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

94
    private QueryRef queryRef;
95
    private List<String> apisToCall = new ArrayList<>();
15✔
96
    private List<Call> calls = new ArrayList<>();
15✔
97

98
    private HttpResponse resp;
99

100
    private QueryCall(QueryRef queryRef) {
6✔
101
        this.queryRef = queryRef;
9✔
102
        logger.info("Invoking API operation {}", queryRef);
12✔
103
    }
3✔
104

105
    private void run() throws NotEnoughAPIInstancesException {
106
        List<String> apiInstancesToTry = new LinkedList<>(getApiInstances());
15✔
107
        while (!apiInstancesToTry.isEmpty() && apisToCall.size() < parallelCallCount) {
24!
108
            int randomIndex = (int) ((Math.random() * apiInstancesToTry.size()));
21✔
109
            String apiUrl = apiInstancesToTry.get(randomIndex);
15✔
110
            apisToCall.add(apiUrl);
15✔
111
            logger.info("Trying API ({}) {}", apisToCall.size(), apiUrl);
24✔
112
            apiInstancesToTry.remove(randomIndex);
12✔
113
        }
3✔
114
        for (String api : apisToCall) {
33✔
115
            Call call = new Call(api);
18✔
116
            calls.add(call);
15✔
117
            new Thread(call).start();
15✔
118
        }
3✔
119
    }
3✔
120

121
    private synchronized void finished(Call call, HttpResponse resp, String apiUrl) {
122
        if (this.resp != null) { // result already in
9!
123
            EntityUtils.consumeQuietly(resp.getEntity());
×
124
            return;
×
125
        }
126
        logger.info("Result in from {}:", apiUrl);
12✔
127
        logger.info("- Request: {}", queryRef);
15✔
128
        logger.info("- Response size: {}", resp.getEntity().getContentLength());
21✔
129
        this.resp = resp;
9✔
130

131
        for (Call c : calls) {
33✔
132
            if (c != call) c.abort();
15✔
133
        }
3✔
134
    }
3✔
135

136
    private static boolean wasSuccessful(HttpResponse resp) {
137
        if (resp == null || resp.getEntity() == null) return false;
15!
138
        int c = resp.getStatusLine().getStatusCode();
12✔
139
        if (c < 200 || c >= 300) return false;
24!
140
        return true;
6✔
141
    }
142

143
    private static boolean wasSuccessfulNonempty(HttpResponse resp) {
144
        if (!wasSuccessful(resp)) return false;
9!
145
        // TODO Make sure we always return proper error codes, and then this shouldn't be necessary:
146
        if (resp.getHeaders("Content-Length").length > 0 && resp.getEntity().getContentLength() < 0) return false;
33!
147
        return true;
6✔
148
    }
149

150

151
    private class Call implements Runnable {
152

153
        private String apiUrl;
154
        private HttpGet get;
155

156
        public Call(String apiUrl) {
15✔
157
            this.apiUrl = apiUrl;
9✔
158
        }
3✔
159

160
        public void run() {
161
            get = new HttpGet(apiUrl + "api/" + queryRef.getAsUrlString());
36✔
162
            get.setHeader("Accept", "text/csv");
15✔
163
            HttpResponse resp = null;
6✔
164
            try {
165
                resp = NanopubUtils.getHttpClient().execute(get);
15✔
166
                if (!wasSuccessfulNonempty(resp)) {
9!
167
                    throw new IOException(resp.getStatusLine().toString());
×
168
                }
169
                finished(this, resp, apiUrl);
21✔
170
            } catch (Exception ex) {
3✔
171
                if (resp != null) EntityUtils.consumeQuietly(resp.getEntity());
6!
172
                logger.error("Request to {} was not successful: {}", apiUrl, ex.getMessage());
21✔
173
            }
3✔
174
            calls.remove(this);
18✔
175
        }
3✔
176

177
        private void abort() {
178
            if (get == null) return;
9!
179
            if (get.isAborted()) return;
12!
180
            get.abort();
9✔
181
        }
3✔
182

183
    }
184

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