🚧 Make HTTP query multi-query-able
authorDavid ‘Bombe’ Roden <bombe@pterodactylus.net>
Thu, 23 Apr 2026 17:42:03 +0000 (19:42 +0200)
committerDavid ‘Bombe’ Roden <bombe@pterodactylus.net>
Thu, 23 Apr 2026 17:42:03 +0000 (19:42 +0200)
src/main/java/net/pterodactylus/rhynodge/queries/HttpQuery.java
src/test/kotlin/net/pterodactylus/rhynodge/queries/HttpQueryTest.kt [new file with mode: 0644]

index c148eb0..1d8d899 100644 (file)
 package net.pterodactylus.rhynodge.queries;
 
 import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
 import java.util.concurrent.TimeUnit;
 import net.pterodactylus.rhynodge.Query;
 import net.pterodactylus.rhynodge.State;
+import net.pterodactylus.rhynodge.states.AbstractState;
 import net.pterodactylus.rhynodge.states.FailedState;
 import net.pterodactylus.rhynodge.states.HttpState;
 import org.apache.hc.client5.http.classic.methods.HttpGet;
@@ -37,6 +40,9 @@ import org.apache.hc.core5.http.protocol.ResponseContent;
 import org.apache.hc.core5.ssl.SSLContexts;
 import org.jspecify.annotations.Nullable;
 
+import static java.util.Arrays.asList;
+import static java.util.Arrays.stream;
+
 /**
  * {@link Query} that performs an HTTP GET request to a fixed uri.
  *
@@ -44,18 +50,27 @@ import org.jspecify.annotations.Nullable;
  */
 public class HttpQuery implements Query {
 
-       private final String uri;
        private final @Nullable String proxyHost;
        private final int proxyPort;
+       private List<String> uris = new ArrayList<>();
 
        public HttpQuery(String uri) {
                this(uri, null, -1);
        }
 
+       public HttpQuery(String uri, String... additionalUris) {
+               this(uri, null, -1, additionalUris);
+       }
+
        public HttpQuery(String uri, @Nullable String proxyHost, int proxyPort) {
-               this.uri = uri;
+               this(uri, proxyHost, proxyPort, new String[0]);
+       }
+
+       public HttpQuery(String uri, @Nullable String proxyHost, int proxyPort, String... additionalUris) {
                this.proxyHost = proxyHost;
                this.proxyPort = proxyPort;
+               this.uris.add(uri);
+               this.uris.addAll(asList(additionalUris));
        }
 
        //
@@ -81,22 +96,32 @@ public class HttpQuery implements Query {
                if ((proxyHost != null) && (proxyPort != -1)) {
                        httpClientBuilder.setProxy(new HttpHost(proxyHost, proxyPort));
                }
-               var get = new HttpGet(uri);
 
+               List<AbstractState> states = new ArrayList<>();
                try (var httpClient = httpClientBuilder.build()) {
-                       /* make request. */
-                       get.addHeader("User-Agent", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/536.11 (KHTML, like Gecko) Ubuntu/12.04 Chromium/20.0.1132.47 Chrome/20.0.1132.47 Safari/536.11");
-                       var response = httpClient.execute(get);
-                       if (response.getCode() != HttpStatus.SC_OK) {
-                               return new FailedState(new IllegalStateException(String.format("Invalid HTTP Status: %d", response.getCode())));
-                       }
-                       var entity = response.getEntity();
+                       for (var uri : uris) {
+                               var get = new HttpGet(uri);
+                               /* make request. */
+                               get.addHeader("User-Agent", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/536.11 (KHTML, like Gecko) Ubuntu/12.04 Chromium/20.0.1132.47 Chrome/20.0.1132.47 Safari/536.11");
+                               var response = httpClient.execute(get);
+                               if (response.getCode() != HttpStatus.SC_OK) {
+                                       states.add(new FailedState(new IllegalStateException(String.format("Invalid HTTP Status: %d", response.getCode()))));
+                                       continue;
+                               }
+                               var entity = response.getEntity();
+
+                               states.add(new HttpState(uri, response.getCode(), entity.getContentType(), EntityUtils.toByteArray(entity)));
 
-                       /* yay, done! */
-                       return new HttpState(uri, response.getCode(), entity.getContentType(), EntityUtils.toByteArray(entity));
+                       }
                } catch (IOException ioe1) {
-                       return new FailedState(ioe1);
+                       states.add(new FailedState(ioe1));
                }
+
+               /* collapse states */
+               return states.stream().reduce((state, newState) -> {
+                       state.addState(newState);
+                       return state;
+               }).get();
        }
 
 }
diff --git a/src/test/kotlin/net/pterodactylus/rhynodge/queries/HttpQueryTest.kt b/src/test/kotlin/net/pterodactylus/rhynodge/queries/HttpQueryTest.kt
new file mode 100644 (file)
index 0000000..30c80bb
--- /dev/null
@@ -0,0 +1,59 @@
+package net.pterodactylus.rhynodge.queries
+
+import com.sun.net.httpserver.HttpExchange
+import com.sun.net.httpserver.HttpServer
+import java.net.InetSocketAddress
+import net.pterodactylus.rhynodge.states.HttpState
+import org.hamcrest.MatcherAssert.assertThat
+import org.hamcrest.Matchers.equalTo
+import org.junit.jupiter.api.AfterEach
+import org.junit.jupiter.api.Test
+
+class HttpQueryTest {
+
+       @Test
+       fun `http query returns content in state`() {
+               val query = HttpQuery("http://localhost:${httpServer.address.port}/url.test1")
+               val httpState = query.state() as HttpState
+               assertThat(httpState.protocolCode(), equalTo(200))
+               assertThat(httpState.contentType(), equalTo("text/test1"))
+               assertThat(httpState.content(), equalTo("first-response"))
+       }
+
+       @Test
+       fun `http query returns multiple states when multiple urls are given`() {
+               val query = HttpQuery("http://localhost:${httpServer.address.port}/url.test1", "http://localhost:${httpServer.address.port}/url.test2")
+               val httpState = query.state() as HttpState
+               assertThat(httpState.protocolCode(), equalTo(200))
+               assertThat(httpState.contentType(), equalTo("text/test1"))
+               assertThat(httpState.content(), equalTo("first-response"))
+               val secondHttpState = httpState.additionalStates.single() as HttpState
+               assertThat(secondHttpState.protocolCode(), equalTo(200))
+               assertThat(secondHttpState.contentType(), equalTo("text/test2"))
+               assertThat(secondHttpState.content(), equalTo("second-response"))
+       }
+
+       private val httpServer = HttpServer.create(InetSocketAddress("localhost", 0), 0)
+
+       init {
+               httpServer.createContext("/url.test1", sendResponse("text/test1", "first-response"))
+               httpServer.createContext("/url.test2", sendResponse("text/test2", "second-response"))
+               httpServer.start()
+       }
+
+       @AfterEach
+       fun shutdownHttpServer() {
+               httpServer.stop(0)
+       }
+
+}
+
+val sendResponse = { contentType: String, response: String ->
+       { httpExchange: HttpExchange ->
+               httpExchange.responseHeaders.add("content-type", contentType)
+               httpExchange.sendResponseHeaders(200, response.toByteArray().size.toLong())
+               httpExchange.responseBody.use {
+                       it.write(response.toByteArray())
+               }
+       }
+}