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

knowledgepixels / nanopub-query / 32485572661

21 Aug 2026 01:11PM UTC coverage: 76.49% (+0.08%) from 76.413%
32485572661

push

github

web-flow
Merge pull request #191 from knowledgepixels/rdf4j-query-timeout

Inject server-side query timeout into proxied rdf4j requests

845 of 1248 branches covered (67.71%)

Branch coverage included in aggregate %.

2467 of 3082 relevant lines covered (80.05%)

12.51 hits per line

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

96.26
src/main/java/com/knowledgepixels/query/Utils.java
1
package com.knowledgepixels.query;
2

3
import java.io.IOException;
4
import java.io.InputStream;
5
import java.nio.charset.StandardCharsets;
6
import java.util.ArrayList;
7
import java.util.HashMap;
8
import java.util.List;
9
import java.util.Map;
10
import java.util.Properties;
11

12
import org.apache.commons.lang3.StringUtils;
13
import org.apache.http.client.config.CookieSpecs;
14
import org.apache.http.client.config.RequestConfig;
15
import org.eclipse.rdf4j.model.IRI;
16
import org.eclipse.rdf4j.model.Value;
17
import org.eclipse.rdf4j.model.ValueFactory;
18
import org.eclipse.rdf4j.model.impl.SimpleValueFactory;
19
import org.eclipse.rdf4j.query.BindingSet;
20
import org.eclipse.rdf4j.query.QueryLanguage;
21
import org.eclipse.rdf4j.query.TupleQuery;
22
import org.eclipse.rdf4j.query.TupleQueryResult;
23
import org.eclipse.rdf4j.repository.RepositoryConnection;
24
import org.nanopub.vocabulary.NPA;
25
import org.slf4j.Logger;
26
import org.slf4j.LoggerFactory;
27

28
import com.google.common.hash.Hashing;
29

30
/**
31
 * Utils class for Nanopub Registry.
32
 */
33
public class Utils {
34

35
    private Utils() {
36
    }  // no instances allowed
37

38
    private static final ValueFactory vf = SimpleValueFactory.getInstance();
6✔
39

40
    private static final Logger logger = LoggerFactory.getLogger(Utils.class);
12✔
41

42
    private static Map<String, Value> hashToObjMap;
43

44
    /**
45
     * Returns reverse hashing map.
46
     *
47
     * @return Map from hashes to their original objects
48
     */
49
    static Map<String, Value> getHashToObjectMap() {
50
        if (hashToObjMap == null) {
6✔
51
            hashToObjMap = new HashMap<>();
12✔
52
            try (RepositoryConnection conn = TripleStore.get().getAdminRepoConnection()) {
9✔
53
                TupleQuery query = conn.prepareTupleQuery(QueryLanguage.SPARQL, "SELECT * { graph ?g { ?s ?p ?o } }");
15✔
54
                query.setBinding("g", NPA.GRAPH);
12✔
55
                query.setBinding("p", NPA.IS_HASH_OF);
12✔
56
                try (TupleQueryResult r = query.evaluate()) {
9✔
57
                    while (r.hasNext()) {
9✔
58
                        BindingSet b = r.next();
12✔
59
                        String hash = b.getBinding("s").getValue().stringValue();
18✔
60
                        hash = StringUtils.replace(hash, NPA.HASH.toString(), "");
18✔
61
                        hashToObjMap.put(hash, b.getBinding("o").getValue());
24✔
62
                    }
3✔
63
                }
64
            }
65
        }
66
        return hashToObjMap;
6✔
67
    }
68

69
    /**
70
     * Returns the original object for the given hash value.
71
     *
72
     * @param hash The hash value
73
     * @return The original object
74
     */
75
    public static Value getObjectForHash(String hash) {
76
        return getHashToObjectMap().get(hash);
15✔
77
    }
78

79
    /**
80
     * Creates a hash value for the object and remembers it.
81
     *
82
     * @param obj Object to be hashed
83
     * @return hash value
84
     */
85
    public static String createHash(Object obj) {
86
        String hash = Hashing.sha256().hashString(obj.toString(), StandardCharsets.UTF_8).toString();
21✔
87

88
        if (!getHashToObjectMap().containsKey(hash)) {
12✔
89
            Value objV = getValue(obj);
9✔
90
            try (RepositoryConnection conn = TripleStore.get().getAdminRepoConnection()) {
9✔
91
                conn.add(vf.createStatement(vf.createIRI(NPA.HASH + hash), NPA.IS_HASH_OF, objV, NPA.GRAPH));
45✔
92
            }
93
            getHashToObjectMap().put(hash, objV);
15✔
94
        }
95
        return hash;
6✔
96
    }
97

98
    /**
99
     * Returns the object as a Value object.
100
     *
101
     * @param obj Input object
102
     * @return A Value object with the string content of the input object
103
     */
104
    static Value getValue(Object obj) {
105
        if (obj instanceof Value) {
9✔
106
            return (Value) obj;
9✔
107
        } else {
108
            return vf.createLiteral(obj.toString());
15✔
109
        }
110
    }
111

112
    /**
113
     * Returns a short "name" for the given public key
114
     *
115
     * @param pubkey Public key string
116
     * @return Short "name"
117
     */
118
    public static String getShortPubkeyName(String pubkey) {
119
        return pubkey.replaceFirst("^(.).{39}(.{5}).*$", "$1..$2..");
15✔
120
    }
121

122
    /**
123
     * Executes a query on the given connection to return the first objects matching the input.
124
     *
125
     * @param conn  The repository connection
126
     * @param graph Graph (=context) IRI
127
     * @param subj  Subject IRI
128
     * @param pred  Predicate IRI
129
     * @return the first object to match the pattern
130
     */
131
    public static Value getObjectForPattern(RepositoryConnection conn, IRI graph, IRI subj, IRI pred) {
132
        TupleQueryResult r = conn.prepareTupleQuery(QueryLanguage.SPARQL, "SELECT * { graph <" + graph.stringValue() + "> { <" + subj.stringValue() + "> <" + pred.stringValue() + "> ?o } }").evaluate();
36✔
133
        try (r) {
6✔
134
            if (!r.hasNext()) return null;
21✔
135
            return r.next().getBinding("o").getValue();
27✔
136
        }
12!
137
    }
138

139
    /**
140
     * Executes a query on the given connection to return all objects matching the input.
141
     *
142
     * @param conn  The repository connection
143
     * @param graph Graph (=context) IRI
144
     * @param subj  Subject IRI
145
     * @param pred  Predicate IRI
146
     * @return a list of all objects matching the pattern
147
     */
148
    public static List<Value> getObjectsForPattern(RepositoryConnection conn, IRI graph, IRI subj, IRI pred) {
149
        List<Value> values = new ArrayList<>();
12✔
150
        TupleQueryResult r = conn.prepareTupleQuery(QueryLanguage.SPARQL, "SELECT * { graph <" + graph.stringValue() + "> { <" + subj.stringValue() + "> <" + pred.stringValue() + "> ?o } }").evaluate();
36✔
151
        try (r) {
6✔
152
            while (r.hasNext()) {
9✔
153
                values.add(r.next().getBinding("o").getValue());
30✔
154
            }
155
            return values;
12✔
156
        }
157
    }
158

159
    /**
160
     * Returns the system environment variable content for the given environment variable name.
161
     *
162
     * @param envVarName   environment variable name
163
     * @param defaultValue default value if not found
164
     * @return environment variable value
165
     */
166
    public static String getEnvString(String envVarName, String defaultValue) {
167
        String s = getRawEnv(envVarName);
9✔
168
        if (s != null && !s.isEmpty()) {
15✔
169
            return s;
6✔
170
        }
171
        return defaultValue;
6✔
172
    }
173

174
    /**
175
     * Reads a single environment variable from the JVM's in-memory environment
176
     * block. The previous implementation used Apache Commons Exec's
177
     * {@code EnvironmentUtils.getProcEnvironment()}, which forks a subprocess
178
     * ({@code env}/{@code sh}) and parses its stdout on every call. That is neither
179
     * thread-safe nor robust under load, and since {@link #getEnvString} is on the
180
     * per-nanopub hot path ({@link FeatureFlags#fullRepoEnabled()} and friends,
181
     * called from the 4-thread loading pool) an intermittently mangled read made
182
     * {@code fullRepoEnabled()} evaluate to {@code false} and silently skip the
183
     * full-repo write for individual nanopubs — leaving {@code full} behind
184
     * {@code meta} undetectably (issue #117).
185
     *
186
     * <p>Extracted as a package-private seam because {@link System} static methods
187
     * cannot be mocked directly with Mockito; tests stub this instead.
188
     *
189
     * @param envVarName environment variable name
190
     * @return the raw value, or {@code null} if unset
191
     */
192
    static String getRawEnv(String envVarName) {
193
        return System.getenv(envVarName);
9✔
194
    }
195

196
    /**
197
     * Returns the system environment variable content as an integer for the given environment variable name.
198
     *
199
     * @param envVarName   environment variable name
200
     * @param defaultValue default value if not found
201
     * @return environment variable value interpreted as an integer
202
     */
203
    public static int getEnvInt(String envVarName, int defaultValue) {
204
        try {
205
            String s = getEnvString(envVarName, null);
12✔
206
            if (s != null) {
6✔
207
                return Integer.parseInt(s);
9✔
208
            }
209
        } catch (Exception ex) {
3✔
210
            logger.info("Could not get environment variable", ex);
12✔
211
        }
3✔
212
        return defaultValue;
6✔
213
    }
214

215
    /**
216
     * Appends the RDF4J protocol {@code timeout} parameter (in seconds) to a request URI,
217
     * making the server abort query evaluation after the given time. Without this, a client
218
     * disconnect (e.g. the public proxy's 10s cut) leaves the server evaluating to completion;
219
     * enough of those zombie evaluations pinning store snapshots sent the changeset overlay
220
     * into a self-reinforcing pile-up that saturated every worker thread (incident 2026-08-21).
221
     * <p>
222
     * A URI that already carries a {@code timeout} parameter is returned unchanged, so
223
     * clients can request a shorter (or longer) limit explicitly. A non-positive
224
     * {@code timeoutSeconds} disables injection.
225
     *
226
     * @param uri            the request URI, with or without a query string
227
     * @param timeoutSeconds server-side evaluation limit in seconds; non-positive = no-op
228
     * @return the URI with the timeout parameter appended, or unchanged
229
     */
230
    public static String appendQueryTimeout(String uri, int timeoutSeconds) {
231
        if (timeoutSeconds <= 0 || uri == null) return uri;
18✔
232
        int q = uri.indexOf('?');
12✔
233
        if (q >= 0 && uri.substring(q).matches(".*[?&]timeout=.*")) return uri;
30✔
234
        return uri + (q >= 0 ? "&" : "?") + "timeout=" + timeoutSeconds;
27✔
235
    }
236

237
    private static String version;
238

239
    /**
240
     * Returns the application version, read from the filtered version.properties resource.
241
     *
242
     * @return the project version, or "unknown" if it cannot be read
243
     */
244
    public static String getVersion() {
245
        String v = version;
6✔
246
        if (v != null) {
6✔
247
            return v;
6✔
248
        }
249
        Properties p = new Properties();
12✔
250
        try (InputStream in = Utils.class.getResourceAsStream("/version.properties")) {
12✔
251
            if (in != null) {
6!
252
                p.load(in);
9✔
253
            }
254
        } catch (IOException ex) {
×
255
            logger.warn("Could not read version.properties", ex);
×
256
        }
3✔
257
        v = p.getProperty("version", "unknown");
15✔
258
        version = v;
6✔
259
        return v;
6✔
260
    }
261

262
    /**
263
     * Default query to be shown in YASGUI client.
264
     */
265
    public static final String defaultQuery = """
266
            prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#>
267
            prefix dct: <http://purl.org/dc/terms/>
268
            prefix np: <http://www.nanopub.org/nschema#>
269
            prefix npa: <http://purl.org/nanopub/admin/>
270
            prefix npx: <http://purl.org/nanopub/x/>
271
            
272
            select * where {
273
            ## Info about this repo:
274
              npa:thisRepo ?pred ?obj .
275
            ## Search for nanopublications:
276
            # graph npa:graph {
277
            #   ?np npa:hasValidSignatureForPublicKey ?pubkey .
278
            #   filter not exists { ?npx npx:invalidates ?np ; npa:hasValidSignatureForPublicKey ?pubkey . }
279
            #   ?np dct:created ?date .
280
            #   ?np np:hasAssertion ?a .
281
            #   optional { ?np rdfs:label ?label }
282
            # }
283
            } limit 10""";
284

285
    /**
286
     * Get the HTTP request config for fetching nanopublications.
287
     *
288
     * @return the HTTP client
289
     */
290
    static RequestConfig getHttpRequestConfig() {
291
        return RequestConfig.custom()
12✔
292
                .setConnectTimeout(getEnvInt("NANOPUB_QUERY_FETCHING_CONNECT_TIMEOUT", 10000))
12✔
293
                .setConnectionRequestTimeout(getEnvInt("NANOPUB_QUERY_FETCHING_CONNECTION_REQUEST_TIMEOUT", 1000))
12✔
294
                .setSocketTimeout(getEnvInt("NANOPUB_QUERY_FETCHING_SOCKET_TIMEOUT", 10000))
9✔
295
                .setCookieSpec(CookieSpecs.IGNORE_COOKIES).build();
6✔
296
    }
297

298
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc