build.codemodel:codemodel-foundation
build.codemodel:jdk-codemodel
diff --git a/spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/AbstractSession.java b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/AbstractSession.java
similarity index 75%
rename from spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/AbstractSession.java
rename to spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/AbstractSession.java
index b534766..28b1704 100644
--- a/spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/AbstractSession.java
+++ b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/AbstractSession.java
@@ -1,8 +1,8 @@
-package build.spawn.docker.okhttp;
+package build.spawn.docker.jdk;
/*-
* #%L
- * Spawn Docker (OkHttp Client)
+ * Spawn Docker (JDK Client)
* %%
* Copyright (C) 2026 Workday, Inc.
* %%
@@ -37,32 +37,27 @@
import build.spawn.docker.Network;
import build.spawn.docker.Networks;
import build.spawn.docker.Session;
-import build.spawn.docker.okhttp.command.Authenticate;
-import build.spawn.docker.okhttp.command.BuildImage;
-import build.spawn.docker.okhttp.command.Command;
-import build.spawn.docker.okhttp.command.CreateNetwork;
-import build.spawn.docker.okhttp.command.DeleteNetwork;
-import build.spawn.docker.okhttp.command.GetSystemEvents;
-import build.spawn.docker.okhttp.command.GetSystemInformation;
-import build.spawn.docker.okhttp.command.InspectImage;
-import build.spawn.docker.okhttp.command.InspectNetwork;
-import build.spawn.docker.okhttp.command.PullImage;
-import build.spawn.docker.okhttp.model.OkHttpBasedImage;
+import build.spawn.docker.jdk.command.Authenticate;
+import build.spawn.docker.jdk.command.BuildImage;
+import build.spawn.docker.jdk.command.Command;
+import build.spawn.docker.jdk.command.CreateNetwork;
+import build.spawn.docker.jdk.command.DeleteNetwork;
+import build.spawn.docker.jdk.command.GetSystemEvents;
+import build.spawn.docker.jdk.command.GetSystemInformation;
+import build.spawn.docker.jdk.command.InspectImage;
+import build.spawn.docker.jdk.command.InspectNetwork;
+import build.spawn.docker.jdk.command.PullImage;
+import build.spawn.docker.jdk.model.DockerImage;
import build.spawn.docker.option.DockerAPIVersion;
import build.spawn.docker.option.DockerRegistry;
import build.spawn.docker.option.IdentityToken;
import com.fasterxml.jackson.databind.ObjectMapper;
-import okhttp3.HttpUrl;
-import okhttp3.OkHttpClient;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.util.Base64;
import java.util.Objects;
import java.util.Optional;
-import java.util.function.Supplier;
-import java.util.logging.Level;
-import java.util.logging.Logger;
/**
* An abstract {@link Session} implementation.
@@ -79,9 +74,9 @@ public class AbstractSession
private final Configuration configuration;
/**
- * The {@link OkHttpClient}.
+ * The {@link HttpTransport} for communicating with the Docker Engine.
*/
- private final OkHttpClient httpClient;
+ private final HttpTransport transport;
/**
* The dependency injection {@link Context} to use for creating {@link Command}s.
@@ -115,38 +110,21 @@ public class AbstractSession
private final GetSystemEvents systemEvents;
/**
- * Constructs an {@link AbstractSession} using the specified {@link OkHttpClient} and {@link HttpUrl.Builder}
- * {@link Supplier}s and provided {@link Configuration}.
- *
- * The {@link OkHttpClient} {@link Supplier} is used to obtain {@link OkHttpClient}s when required by a
- * {@link Session} and more specifically, when {@link Command}s for the {@link Session} need to be
- * {@link Command#submit()}ted for execution. {@link Session}s are free to cache and reuse {@link OkHttpClient}s
- * for {@link Command}s, so it should not be assumed that a new {@link OkHttpClient} is created per {@link Command}
- * or a single {@link OkHttpClient} is used for all {@link Command}s.
- *
- * The {@link HttpUrl.Builder} {@link Supplier} is used to obtain base {@link HttpUrl}s from which {@link Command}s
- * may be constructed for submission with a {@link OkHttpClient}. Each {@link Command} is guaranteed to be
- * provided (usually injected) with a new {@link HttpUrl.Builder}, thus allowing per-{@link Command} customization.
+ * Constructs an {@link AbstractSession} using the specified {@link HttpTransport} and {@link Configuration}.
*
- * @param injectionFramework the {@link InjectionFramework} to use for {@link build.codemodel.injection.Dependency} injection
- * @param clientSupplier the {@link OkHttpClient} {@link Supplier}
- * @param httpUrlBuilderSupplier the {@link HttpUrl.Builder} {@link Supplier}
- * @param configuration the {@link Configuration}
+ * @param injectionFramework the {@link InjectionFramework} to use for {@link build.codemodel.injection.Dependency} injection
+ * @param transport the {@link HttpTransport} for communicating with the Docker Engine
+ * @param configuration the {@link Configuration}
*/
@SuppressWarnings("unchecked")
protected AbstractSession(final InjectionFramework injectionFramework,
- final Supplier clientSupplier,
- final Supplier httpUrlBuilderSupplier,
+ final HttpTransport transport,
final Configuration configuration) {
Objects.requireNonNull(injectionFramework, "The InjectionFramework must not be null");
- Objects.requireNonNull(clientSupplier, "The Supplier must not be null");
- Objects.requireNonNull(httpUrlBuilderSupplier, "The Supplier must not be null");
+ Objects.requireNonNull(transport, "The HttpTransport must not be null");
- Logger.getLogger(OkHttpClient.class.getName()).setLevel(Level.FINE);
-
- // establish the HttpClient from the Supplier (allowing us to connect to the Docker Engine)
- this.httpClient = clientSupplier.get();
+ this.transport = transport;
this.configuration = configuration == null
? Configuration.empty()
@@ -173,8 +151,7 @@ protected AbstractSession(final InjectionFramework injectionFramework,
this.context.addResolver(ConfigurationResolver.of(configuration));
- this.context.bind(OkHttpClient.class).to(this.httpClient);
- this.context.bind(HttpUrl.Builder.class).to(httpUrlBuilderSupplier);
+ this.context.bind(HttpTransport.class).to(this.transport);
this.context.bind(Session.class).to(this);
this.context.bind(AbstractSession.class).to(this);
this.context.bind((Class) getClass()).to(this);
@@ -249,7 +226,7 @@ protected AbstractSession(final InjectionFramework injectionFramework,
.getEncoder()
.encode(json.getBytes(StandardCharsets.UTF_8)));
- return (Authenticator) builder -> builder.addHeader("X-Registry-Auth", encoded);
+ return (Authenticator) builder -> builder.withHeader("X-Registry-Auth", encoded);
})
.orElse(Authenticator.NONE));
@@ -316,7 +293,7 @@ public Optional get(final String nameOrId, final Configuration configurat
.inject(new InspectImage(nameOrId, configuration))
.submit()
.map(info -> createContext()
- .inject(new OkHttpBasedImage(info.imageId())));
+ .inject(new DockerImage(info.imageId())));
}
@Override
@@ -351,16 +328,7 @@ public void close() {
// close the reading of System Events
this.systemEvents.close();
- // the OkHttpClient doesn't require shutdown to clean up
- // but to stop accepting requests / responses we need to stop the internal executor service
- this.httpClient
- .dispatcher()
- .executorService()
- .shutdownNow();
-
- this.httpClient
- .connectionPool()
- .evictAll();
+ // HttpTransport implementations manage their own connection lifecycle
}
class NetworksImpl
diff --git a/spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/Authenticator.java b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/Authenticator.java
similarity index 71%
rename from spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/Authenticator.java
rename to spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/Authenticator.java
index 73ebec5..b6ffba5 100644
--- a/spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/Authenticator.java
+++ b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/Authenticator.java
@@ -1,17 +1,17 @@
-package build.spawn.docker.okhttp;
+package build.spawn.docker.jdk;
/*-
* #%L
- * Spawn Docker (OkHttp Client)
+ * Spawn Docker (JDK Client)
* %%
* Copyright (C) 2026 Workday, Inc.
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
- *
+ *
* http://www.apache.org/licenses/LICENSE-2.0
- *
+ *
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -20,21 +20,19 @@
* #L%
*/
-import okhttp3.Request;
-
-import java.util.function.Function;
+import java.util.function.UnaryOperator;
/**
- * A {@link Function} to configure authentication for a {@link Request.Builder}.
+ * A {@link UnaryOperator} to configure authentication on an {@link HttpTransport.Request}.
*
* @author brian.oliver
* @since Aug-2022
*/
public interface Authenticator
- extends Function {
+ extends UnaryOperator {
/**
* An {@link Authenticator} that performs no authentication.
*/
- Authenticator NONE = builder -> builder;
+ Authenticator NONE = request -> request;
}
diff --git a/spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/CompletableFutures.java b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/CompletableFutures.java
similarity index 95%
rename from spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/CompletableFutures.java
rename to spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/CompletableFutures.java
index e48c93e..e674105 100644
--- a/spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/CompletableFutures.java
+++ b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/CompletableFutures.java
@@ -1,8 +1,8 @@
-package build.spawn.docker.okhttp;
+package build.spawn.docker.jdk;
/*-
* #%L
- * Spawn Docker (OkHttp Client)
+ * Spawn Docker (JDK Client)
* %%
* Copyright (C) 2026 Workday, Inc.
* %%
diff --git a/spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/DockerHostVariableBasedSessionFactory.java b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/DockerHostVariableBasedSessionFactory.java
similarity index 98%
rename from spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/DockerHostVariableBasedSessionFactory.java
rename to spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/DockerHostVariableBasedSessionFactory.java
index 6448287..614f521 100644
--- a/spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/DockerHostVariableBasedSessionFactory.java
+++ b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/DockerHostVariableBasedSessionFactory.java
@@ -1,8 +1,8 @@
-package build.spawn.docker.okhttp;
+package build.spawn.docker.jdk;
/*-
* #%L
- * Spawn Docker (OkHttp Client)
+ * Spawn Docker (JDK Client)
* %%
* Copyright (C) 2026 Workday, Inc.
* %%
diff --git a/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/Http11Parser.java b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/Http11Parser.java
new file mode 100644
index 0000000..9d9e8f9
--- /dev/null
+++ b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/Http11Parser.java
@@ -0,0 +1,258 @@
+package build.spawn.docker.jdk;
+
+/*-
+ * #%L
+ * Spawn Docker (JDK Client)
+ * %%
+ * Copyright (C) 2026 Workday, Inc.
+ * %%
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * #L%
+ */
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+/**
+ * Parses HTTP/1.1 responses from an {@link InputStream}, handling {@code Content-Length} and
+ * {@code Transfer-Encoding: chunked} bodies.
+ *
+ * @author brian.oliver
+ * @since Apr-2026
+ */
+class Http11Parser {
+
+ private Http11Parser() {
+ }
+
+ /**
+ * Parses an HTTP/1.1 response from the specified {@link InputStream}.
+ *
+ * @param in the {@link InputStream} to read from
+ * @param connection a {@link AutoCloseable} representing the underlying connection, closed on {@link HttpTransport.Response#close()}
+ * @return the parsed {@link HttpTransport.Response}
+ * @throws IOException when the response cannot be parsed
+ */
+ static HttpTransport.Response parse(final InputStream in,
+ final AutoCloseable connection) throws IOException {
+
+ // parse the status line: "HTTP/1.1 200 OK"
+ final var statusLine = readLine(in);
+ final var parts = statusLine.split(" ", 3);
+ final var statusCode = Integer.parseInt(parts[1]);
+
+ // parse the headers
+ final Map headers = new LinkedHashMap<>();
+ String headerLine;
+ while (!(headerLine = readLine(in)).isEmpty()) {
+ final var colon = headerLine.indexOf(':');
+ if (colon > 0) {
+ headers.put(headerLine.substring(0, colon).trim().toLowerCase(),
+ headerLine.substring(colon + 1).trim());
+ }
+ }
+
+ // determine the body stream
+ final var transferEncoding = headers.getOrDefault("transfer-encoding", "");
+ final var contentLengthStr = headers.get("content-length");
+
+ final InputStream body;
+ if ("chunked".equalsIgnoreCase(transferEncoding)) {
+ body = new ChunkedInputStream(in);
+ } else if (contentLengthStr != null) {
+ body = new BoundedInputStream(in, Long.parseLong(contentLengthStr));
+ } else {
+ body = in;
+ }
+
+ return new ParsedResponse(statusCode, headers, body, connection);
+ }
+
+ /**
+ * Reads a CRLF-terminated line from the {@link InputStream}.
+ *
+ * @param in the {@link InputStream}
+ * @return the line content without the CRLF
+ * @throws IOException when reading fails
+ */
+ static String readLine(final InputStream in) throws IOException {
+ final var sb = new StringBuilder();
+ int b;
+ while ((b = in.read()) != -1) {
+ if (b == '\r') {
+ final var next = in.read();
+ if (next == '\n') {
+ break;
+ }
+ sb.append((char) b);
+ if (next != -1) {
+ sb.append((char) next);
+ }
+ } else if (b == '\n') {
+ break;
+ } else {
+ sb.append((char) b);
+ }
+ }
+ return sb.toString();
+ }
+
+ /**
+ * An {@link HttpTransport.Response} backed by a parsed HTTP/1.1 response.
+ */
+ private static final class ParsedResponse
+ implements HttpTransport.Response {
+
+ private final int statusCode;
+ private final Map headers;
+ private final InputStream body;
+ private final AutoCloseable connection;
+
+ ParsedResponse(final int statusCode,
+ final Map headers,
+ final InputStream body,
+ final AutoCloseable connection) {
+ this.statusCode = statusCode;
+ this.headers = headers;
+ this.body = body;
+ this.connection = connection;
+ }
+
+ @Override
+ public int statusCode() {
+ return this.statusCode;
+ }
+
+ @Override
+ public String header(final String name) {
+ return this.headers.get(name.toLowerCase());
+ }
+
+ @Override
+ public InputStream bodyStream() {
+ return this.body;
+ }
+
+ @Override
+ public void cancel() {
+ close();
+ }
+
+ @Override
+ public void close() {
+ try {
+ this.connection.close();
+ } catch (final Exception ignored) {
+ // we ignore close failures
+ }
+ }
+ }
+
+ /**
+ * An {@link InputStream} that decodes HTTP chunked transfer encoding.
+ */
+ static final class ChunkedInputStream
+ extends InputStream {
+
+ private final InputStream in;
+ private int remaining;
+ private boolean done;
+
+ ChunkedInputStream(final InputStream in) {
+ this.in = in;
+ this.remaining = 0;
+ this.done = false;
+ }
+
+ @Override
+ public int read() throws IOException {
+ final var buf = new byte[1];
+ final var n = read(buf, 0, 1);
+ return n == -1 ? -1 : buf[0] & 0xff;
+ }
+
+ @Override
+ public int read(final byte[] buf, final int off, final int len) throws IOException {
+ if (this.done) {
+ return -1;
+ }
+ if (this.remaining == 0) {
+ // read the chunk size line (hex digits, possibly with extensions)
+ final var sizeLine = readLine(this.in).trim();
+ if (sizeLine.isEmpty()) {
+ return -1;
+ }
+ // strip chunk extensions (anything after ';')
+ final var semicolon = sizeLine.indexOf(';');
+ final var hexSize = semicolon >= 0 ? sizeLine.substring(0, semicolon) : sizeLine;
+ this.remaining = Integer.parseInt(hexSize.trim(), 16);
+ if (this.remaining == 0) {
+ this.done = true;
+ return -1;
+ }
+ }
+ final var toRead = Math.min(len, this.remaining);
+ final var n = this.in.read(buf, off, toRead);
+ if (n > 0) {
+ this.remaining -= n;
+ if (this.remaining == 0) {
+ // consume the trailing CRLF after the chunk data
+ readLine(this.in);
+ }
+ }
+ return n;
+ }
+ }
+
+ /**
+ * An {@link InputStream} that reads at most a bounded number of bytes.
+ */
+ static final class BoundedInputStream
+ extends InputStream {
+
+ private final InputStream in;
+ private long remaining;
+
+ BoundedInputStream(final InputStream in, final long limit) {
+ this.in = in;
+ this.remaining = limit;
+ }
+
+ @Override
+ public int read() throws IOException {
+ if (this.remaining <= 0) {
+ return -1;
+ }
+ final var b = this.in.read();
+ if (b != -1) {
+ this.remaining--;
+ }
+ return b;
+ }
+
+ @Override
+ public int read(final byte[] buf, final int off, final int len) throws IOException {
+ if (this.remaining <= 0) {
+ return -1;
+ }
+ final var toRead = (int) Math.min(len, this.remaining);
+ final var n = this.in.read(buf, off, toRead);
+ if (n > 0) {
+ this.remaining -= n;
+ }
+ return n;
+ }
+ }
+}
diff --git a/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/HttpTransport.java b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/HttpTransport.java
new file mode 100644
index 0000000..5410e4d
--- /dev/null
+++ b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/HttpTransport.java
@@ -0,0 +1,196 @@
+package build.spawn.docker.jdk;
+
+/*-
+ * #%L
+ * Spawn Docker (JDK Client)
+ * %%
+ * Copyright (C) 2026 Workday, Inc.
+ * %%
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * #L%
+ */
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+/**
+ * A transport abstraction over HTTP for communicating with the Docker Engine API.
+ *
+ * @author brian.oliver
+ * @since Apr-2026
+ */
+public interface HttpTransport {
+
+ /**
+ * Executes the specified {@link Request} and returns a {@link Response}.
+ *
+ * The caller is responsible for closing the {@link Response}.
+ *
+ * @param request the {@link Request} to execute
+ * @return the {@link Response}
+ * @throws IOException when the request fails
+ */
+ Response execute(Request request) throws IOException;
+
+ /**
+ * An HTTP request.
+ *
+ * @param method the HTTP method (GET, POST, DELETE, HEAD)
+ * @param path the request path including any query parameters (e.g. "/containers/123/start?signal=SIGKILL")
+ * @param headers the request headers
+ * @param body the request body bytes, or {@code null} for no body
+ */
+ record Request(String method, String path, Map headers, byte[] body) {
+
+ /**
+ * Constructs a {@link Request} with a defensive copy of the headers map.
+ */
+ public Request {
+ headers = new LinkedHashMap<>(headers == null ? Map.of() : headers);
+ }
+
+ /**
+ * Returns a copy of this {@link Request} with an additional header.
+ *
+ * @param name the header name
+ * @param value the header value
+ * @return a new {@link Request} with the header added
+ */
+ public Request withHeader(final String name, final String value) {
+ final var copy = new LinkedHashMap<>(headers);
+ copy.put(name, value);
+ return new Request(method, path, copy, body);
+ }
+
+ /**
+ * Returns a copy of this {@link Request} with a {@code Content-Type} header.
+ *
+ * @param contentType the content type
+ * @return a new {@link Request}
+ */
+ public Request withContentType(final String contentType) {
+ return withHeader("Content-Type", contentType);
+ }
+
+ /**
+ * Creates a GET {@link Request} for the specified path.
+ *
+ * @param path the request path
+ * @return a new GET {@link Request}
+ */
+ public static Request get(final String path) {
+ return new Request("GET", path, Map.of(), null);
+ }
+
+ /**
+ * Creates a POST {@link Request} for the specified path and body.
+ *
+ * @param path the request path
+ * @param body the request body bytes, or {@code null} for an empty body
+ * @return a new POST {@link Request}
+ */
+ public static Request post(final String path, final byte[] body) {
+ return new Request("POST", path, Map.of(), body);
+ }
+
+ /**
+ * Creates a DELETE {@link Request} for the specified path.
+ *
+ * @param path the request path
+ * @return a new DELETE {@link Request}
+ */
+ public static Request delete(final String path) {
+ return new Request("DELETE", path, Map.of(), null);
+ }
+
+ /**
+ * Creates a PUT {@link Request} for the specified path and body.
+ *
+ * @param path the request path
+ * @param body the request body bytes, or {@code null} for an empty body
+ * @return a new PUT {@link Request}
+ */
+ public static Request put(final String path, final byte[] body) {
+ return new Request("PUT", path, Map.of(), body);
+ }
+
+ /**
+ * Creates a HEAD {@link Request} for the specified path.
+ *
+ * @param path the request path
+ * @return a new HEAD {@link Request}
+ */
+ public static Request head(final String path) {
+ return new Request("HEAD", path, Map.of(), null);
+ }
+ }
+
+ /**
+ * An HTTP response.
+ */
+ interface Response extends AutoCloseable {
+
+ /**
+ * Returns the HTTP status code.
+ *
+ * @return the status code
+ */
+ int statusCode();
+
+ /**
+ * Returns {@code true} when the status code is in the 2xx range.
+ *
+ * @return {@code true} if successful
+ */
+ default boolean isSuccessful() {
+ return statusCode() >= 200 && statusCode() < 300;
+ }
+
+ /**
+ * Returns the value of the specified response header, or {@code null} if absent.
+ *
+ * @param name the header name (case-insensitive)
+ * @return the header value, or {@code null}
+ */
+ String header(String name);
+
+ /**
+ * Returns the response body as an {@link InputStream}.
+ *
+ * @return the body stream
+ * @throws IOException when the body cannot be read
+ */
+ InputStream bodyStream() throws IOException;
+
+ /**
+ * Returns the complete response body as a {@link String}.
+ *
+ * @return the body string
+ * @throws IOException when the body cannot be read
+ */
+ default String bodyString() throws IOException {
+ return new String(bodyStream().readAllBytes(), StandardCharsets.UTF_8);
+ }
+
+ /**
+ * Cancels any ongoing processing and releases resources.
+ */
+ void cancel();
+
+ @Override
+ void close();
+ }
+}
diff --git a/spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/InternalDockerHostBasedSessionFactory.java b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/InternalDockerHostBasedSessionFactory.java
similarity index 96%
rename from spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/InternalDockerHostBasedSessionFactory.java
rename to spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/InternalDockerHostBasedSessionFactory.java
index 7f290d3..53a822f 100644
--- a/spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/InternalDockerHostBasedSessionFactory.java
+++ b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/InternalDockerHostBasedSessionFactory.java
@@ -1,8 +1,8 @@
-package build.spawn.docker.okhttp;
+package build.spawn.docker.jdk;
/*-
* #%L
- * Spawn Docker (OkHttp Client)
+ * Spawn Docker (JDK Client)
* %%
* Copyright (C) 2026 Workday, Inc.
* %%
diff --git a/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/JavaHttpClientTransport.java b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/JavaHttpClientTransport.java
new file mode 100644
index 0000000..956d46f
--- /dev/null
+++ b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/JavaHttpClientTransport.java
@@ -0,0 +1,134 @@
+package build.spawn.docker.jdk;
+
+/*-
+ * #%L
+ * Spawn Docker (JDK Client)
+ * %%
+ * Copyright (C) 2026 Workday, Inc.
+ * %%
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * #L%
+ */
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URI;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.util.Objects;
+
+/**
+ * An {@link HttpTransport} implementation that uses {@link HttpClient} to connect to Docker via TCP.
+ *
+ * @author brian.oliver
+ * @since Apr-2026
+ */
+public class JavaHttpClientTransport
+ implements HttpTransport {
+
+ /**
+ * The {@link HttpClient} to use for executing requests.
+ */
+ private final HttpClient httpClient;
+
+ /**
+ * The base URL of the Docker daemon (e.g. {@code http://localhost:2375}).
+ */
+ private final String baseUrl;
+
+ /**
+ * Constructs a {@link JavaHttpClientTransport}.
+ *
+ * @param httpClient the {@link HttpClient}
+ * @param baseUrl the base URL (e.g. {@code http://localhost:2375})
+ */
+ public JavaHttpClientTransport(final HttpClient httpClient, final String baseUrl) {
+ this.httpClient = Objects.requireNonNull(httpClient, "The HttpClient must not be null");
+ this.baseUrl = Objects.requireNonNull(baseUrl, "The base URL must not be null");
+ }
+
+ @Override
+ public Response execute(final Request request) throws IOException {
+ try {
+ final var builder = HttpRequest.newBuilder()
+ .uri(URI.create(this.baseUrl + request.path()));
+
+ request.headers().forEach(builder::header);
+
+ final var body = request.body();
+ final var bodyPublisher = (body == null || body.length == 0)
+ ? HttpRequest.BodyPublishers.noBody()
+ : HttpRequest.BodyPublishers.ofByteArray(body);
+
+ switch (request.method()) {
+ case "GET" -> builder.GET();
+ case "DELETE" -> builder.DELETE();
+ case "HEAD" -> builder.method("HEAD", HttpRequest.BodyPublishers.noBody());
+ case "PUT" -> builder.PUT(bodyPublisher);
+ default -> builder.POST(bodyPublisher);
+ }
+
+ final var httpResponse = this.httpClient
+ .send(builder.build(), HttpResponse.BodyHandlers.ofInputStream());
+
+ return new JavaHttpResponse(httpResponse);
+
+ } catch (final InterruptedException e) {
+ Thread.currentThread().interrupt();
+ throw new IOException("Request interrupted", e);
+ }
+ }
+
+ /**
+ * An {@link HttpTransport.Response} backed by a {@link HttpResponse}.
+ */
+ private static final class JavaHttpResponse
+ implements Response {
+
+ private final HttpResponse response;
+
+ JavaHttpResponse(final HttpResponse response) {
+ this.response = response;
+ }
+
+ @Override
+ public int statusCode() {
+ return this.response.statusCode();
+ }
+
+ @Override
+ public String header(final String name) {
+ return this.response.headers().firstValue(name).orElse(null);
+ }
+
+ @Override
+ public InputStream bodyStream() {
+ return this.response.body();
+ }
+
+ @Override
+ public void cancel() {
+ close();
+ }
+
+ @Override
+ public void close() {
+ try {
+ this.response.body().close();
+ } catch (final IOException ignored) {
+ // we ignore close failures
+ }
+ }
+ }
+}
diff --git a/spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/LocalHostBasedSessionFactory.java b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/LocalHostBasedSessionFactory.java
similarity index 96%
rename from spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/LocalHostBasedSessionFactory.java
rename to spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/LocalHostBasedSessionFactory.java
index e3b4887..9dec51f 100644
--- a/spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/LocalHostBasedSessionFactory.java
+++ b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/LocalHostBasedSessionFactory.java
@@ -1,8 +1,8 @@
-package build.spawn.docker.okhttp;
+package build.spawn.docker.jdk;
/*-
* #%L
- * Spawn Docker (OkHttp Client)
+ * Spawn Docker (JDK Client)
* %%
* Copyright (C) 2026 Workday, Inc.
* %%
diff --git a/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/TCPSocketBasedSession.java b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/TCPSocketBasedSession.java
new file mode 100644
index 0000000..110e278
--- /dev/null
+++ b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/TCPSocketBasedSession.java
@@ -0,0 +1,58 @@
+package build.spawn.docker.jdk;
+
+/*-
+ * #%L
+ * Spawn Docker (JDK Client)
+ * %%
+ * Copyright (C) 2026 Workday, Inc.
+ * %%
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * #L%
+ */
+
+import build.base.configuration.Configuration;
+import build.codemodel.injection.InjectionFramework;
+
+import java.net.InetSocketAddress;
+import java.net.http.HttpClient;
+import java.time.Duration;
+
+/**
+ * A TCP-socket-based {@code Docker} {@link build.spawn.docker.Session}.
+ *
+ * @author brian.oliver
+ * @since Aug-2022
+ */
+public class TCPSocketBasedSession
+ extends AbstractSession {
+
+ /**
+ * Constructs a {@link TCPSocketBasedSession} for the specified {@link InetSocketAddress}.
+ *
+ * @param injectionFramework the {@link InjectionFramework}
+ * @param socketAddress the {@link InetSocketAddress}
+ * @param configuration the {@link Configuration}
+ */
+ public TCPSocketBasedSession(final InjectionFramework injectionFramework,
+ final InetSocketAddress socketAddress,
+ final Configuration configuration) {
+
+ super(injectionFramework,
+ new JavaHttpClientTransport(
+ HttpClient.newBuilder()
+ .connectTimeout(Duration.ofSeconds(10))
+ .build(),
+ "http://" + socketAddress.getHostString() + ":" + socketAddress.getPort()),
+ configuration);
+ }
+}
diff --git a/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/UnixDomainSocketBasedSession.java b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/UnixDomainSocketBasedSession.java
new file mode 100644
index 0000000..6acdc3e
--- /dev/null
+++ b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/UnixDomainSocketBasedSession.java
@@ -0,0 +1,122 @@
+package build.spawn.docker.jdk;
+
+/*-
+ * #%L
+ * Spawn Docker (JDK Client)
+ * %%
+ * Copyright (C) 2026 Workday, Inc.
+ * %%
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * #L%
+ */
+
+import build.base.configuration.Configuration;
+import build.codemodel.injection.InjectionFramework;
+import build.spawn.docker.Session;
+import jakarta.inject.Inject;
+
+import java.io.File;
+import java.net.UnixDomainSocketAddress;
+import java.nio.channels.SocketChannel;
+import java.util.ArrayList;
+import java.util.Optional;
+
+/**
+ * A Unix Domain Socket-based {@code Docker} {@link Session}.
+ *
+ * @author brian.oliver
+ * @since Jun-2021
+ */
+public class UnixDomainSocketBasedSession
+ extends AbstractSession {
+
+ /**
+ * The {@code docker.sock} {@link File}.
+ */
+ private static final File DOCKER_SOCK_FILE = resolveDockerSockFile();
+
+ /**
+ * Resolves the {@code docker.sock} {@link File} by checking known locations in order of preference.
+ *
+ * @return the {@code docker.sock} {@link File}
+ */
+ private static File resolveDockerSockFile() {
+ final var candidates = new ArrayList();
+
+ // Docker Desktop on Linux
+ candidates.add(new File(System.getProperty("user.home") + "/.docker/desktop/docker.sock"));
+
+ // standard Docker Engine location
+ candidates.add(new File("/var/run/docker.sock"));
+
+ return candidates.stream()
+ .filter(File::exists)
+ .findFirst()
+ .orElse(new File("/var/run/docker.sock"));
+ }
+
+ /**
+ * Constructs a {@link UnixDomainSocketBasedSession} using the default {@code docker.sock} file.
+ *
+ * @param injectionFramework the {@link InjectionFramework} for Dependency Injection
+ * @param configuration the {@link Configuration}
+ */
+ public UnixDomainSocketBasedSession(final InjectionFramework injectionFramework,
+ final Configuration configuration) {
+
+ this(injectionFramework, DOCKER_SOCK_FILE, configuration);
+ }
+
+ /**
+ * Constructs a {@link UnixDomainSocketBasedSession} for the specified Unix socket {@link File}.
+ *
+ * @param injectionFramework the {@link InjectionFramework} for Dependency Injection
+ * @param socketFile the Unix socket {@link File}
+ * @param configuration the {@link Configuration}
+ */
+ public UnixDomainSocketBasedSession(final InjectionFramework injectionFramework,
+ final File socketFile,
+ final Configuration configuration) {
+
+ super(injectionFramework, new UnixSocketHttpTransport(socketFile), configuration);
+ }
+
+ /**
+ * The {@link Session.Factory} for the {@link UnixDomainSocketBasedSession}s.
+ */
+ public static class Factory
+ implements Session.Factory {
+
+ /**
+ * The {@link InjectionFramework} to use for Dependency Injection.
+ */
+ @Inject
+ private InjectionFramework injectionFramework;
+
+ @Override
+ public boolean isOperational() {
+ try (var _ = SocketChannel.open(UnixDomainSocketAddress.of(DOCKER_SOCK_FILE.toPath()))) {
+ return true;
+ } catch (final Exception _) {
+ return false;
+ }
+ }
+
+ @Override
+ public Optional create(final Configuration configuration) {
+ return isOperational()
+ ? Optional.of(new UnixDomainSocketBasedSession(this.injectionFramework, configuration))
+ : Optional.empty();
+ }
+ }
+}
diff --git a/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/UnixSocketHttpTransport.java b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/UnixSocketHttpTransport.java
new file mode 100644
index 0000000..c0d42f3
--- /dev/null
+++ b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/UnixSocketHttpTransport.java
@@ -0,0 +1,108 @@
+package build.spawn.docker.jdk;
+
+/*-
+ * #%L
+ * Spawn Docker (JDK Client)
+ * %%
+ * Copyright (C) 2026 Workday, Inc.
+ * %%
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * #L%
+ */
+
+import java.io.BufferedInputStream;
+import java.io.BufferedOutputStream;
+import java.io.File;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.net.UnixDomainSocketAddress;
+import java.nio.channels.Channels;
+import java.nio.channels.SocketChannel;
+import java.nio.charset.StandardCharsets;
+import java.util.Map;
+import java.util.Objects;
+
+/**
+ * An {@link HttpTransport} implementation that communicates with the Docker daemon over a Unix domain socket.
+ *
+ * Uses {@link SocketChannel} with {@link UnixDomainSocketAddress} (standard since Java 16) to establish
+ * the socket connection and implements HTTP/1.1 framing directly.
+ *
+ * @author brian.oliver
+ * @since Apr-2026
+ */
+public class UnixSocketHttpTransport
+ implements HttpTransport {
+
+ /**
+ * The Unix domain socket {@link File}.
+ */
+ private final File socketFile;
+
+ /**
+ * Constructs a {@link UnixSocketHttpTransport}.
+ *
+ * @param socketFile the Unix domain socket {@link File}
+ */
+ public UnixSocketHttpTransport(final File socketFile) {
+ this.socketFile = Objects.requireNonNull(socketFile, "The socket file must not be null");
+ }
+
+ @Override
+ public Response execute(final Request request) throws IOException {
+ final var channel = SocketChannel.open(UnixDomainSocketAddress.of(this.socketFile.toPath()));
+
+ final var out = new BufferedOutputStream(Channels.newOutputStream(channel));
+ writeRequest(out, request);
+ out.flush();
+
+ return Http11Parser.parse(new BufferedInputStream(Channels.newInputStream(channel)), channel);
+ }
+
+ /**
+ * Writes an HTTP/1.1 request to the specified {@link OutputStream}.
+ *
+ * @param out the {@link OutputStream}
+ * @param request the {@link Request}
+ * @throws IOException when writing fails
+ */
+ private void writeRequest(final OutputStream out, final Request request) throws IOException {
+ writeLine(out, request.method() + " " + request.path() + " HTTP/1.1");
+ writeLine(out, "Host: docker");
+ writeLine(out, "Connection: close");
+
+ final var body = request.body();
+ writeLine(out, "Content-Length: " + (body != null ? body.length : 0));
+
+ for (final Map.Entry header : request.headers().entrySet()) {
+ writeLine(out, header.getKey() + ": " + header.getValue());
+ }
+
+ writeLine(out, "");
+
+ if (body != null && body.length > 0) {
+ out.write(body);
+ }
+ }
+
+ /**
+ * Writes a CRLF-terminated line to the {@link OutputStream}.
+ *
+ * @param out the {@link OutputStream}
+ * @param line the line content
+ * @throws IOException when writing fails
+ */
+ private void writeLine(final OutputStream out, final String line) throws IOException {
+ out.write((line + "\r\n").getBytes(StandardCharsets.UTF_8));
+ }
+}
diff --git a/spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/command/AbstractBlockingCommand.java b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/AbstractBlockingCommand.java
similarity index 55%
rename from spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/command/AbstractBlockingCommand.java
rename to spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/AbstractBlockingCommand.java
index a06eeba..96d98ea 100644
--- a/spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/command/AbstractBlockingCommand.java
+++ b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/AbstractBlockingCommand.java
@@ -1,17 +1,17 @@
-package build.spawn.docker.okhttp.command;
+package build.spawn.docker.jdk.command;
/*-
* #%L
- * Spawn Docker (OkHttp Client)
+ * Spawn Docker (JDK Client)
* %%
* Copyright (C) 2026 Workday, Inc.
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
- *
+ *
* http://www.apache.org/licenses/LICENSE-2.0
- *
+ *
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -20,12 +20,7 @@
* #L%
*/
-import okhttp3.Call;
-import okhttp3.OkHttpClient;
-import okhttp3.Request;
-import okhttp3.Response;
-
-import java.util.concurrent.TimeUnit;
+import build.spawn.docker.jdk.HttpTransport;
/**
* An abstract {@link Command} that blocks until the {@link Command} has been executed and a response has been
@@ -38,35 +33,19 @@
public abstract class AbstractBlockingCommand extends AbstractCommand {
@Override
- protected OkHttpClient httpClient() {
- // ensure the reading from the response never times out (so we can continuously wait for responses)
- return super.httpClient().newBuilder()
- .connectTimeout(0, TimeUnit.MINUTES)
- .readTimeout(0, TimeUnit.MINUTES)
- .writeTimeout(0, TimeUnit.MINUTES)
- .build();
- }
-
- @Override
- protected void onRequestCreated(final Request request) {
+ protected void onRequestCreated(final HttpTransport.Request request) {
// by default, we don't do anything when the Request is created
}
@Override
- protected void onCallCreated(final Call call) {
- // by default, we don't do anything when the Call is created
- }
-
- @Override
- protected void onResponseReceived(final Response response) {
+ protected void onResponseReceived(final HttpTransport.Response response) {
// by default, we don't do anything with the Response when it is received
}
@Override
- protected void onSuccessfulRequest(final Request request,
- final Response response,
+ protected void onSuccessfulRequest(final HttpTransport.Request request,
+ final HttpTransport.Response response,
final T result) {
-
response.close();
}
}
diff --git a/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/AbstractCommand.java b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/AbstractCommand.java
new file mode 100644
index 0000000..fc7958f
--- /dev/null
+++ b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/AbstractCommand.java
@@ -0,0 +1,187 @@
+package build.spawn.docker.jdk.command;
+
+/*-
+ * #%L
+ * Spawn Docker (JDK Client)
+ * %%
+ * Copyright (C) 2026 Workday, Inc.
+ * %%
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * #L%
+ */
+
+import build.codemodel.injection.Context;
+import build.spawn.docker.jdk.HttpTransport;
+import jakarta.inject.Inject;
+
+import java.io.IOException;
+
+/**
+ * An abstract {@link Command} that uses an {@link HttpTransport} to submit request(s) and receive responses,
+ * that of which may be processed to produce a result.
+ *
+ * @param the type of result produced by the {@link Command}
+ * @author brian.oliver
+ * @since Jun-2021
+ */
+public abstract class AbstractCommand
+ implements Command {
+
+ /**
+ * The {@link HttpTransport} to use for executing the {@link Command}.
+ */
+ @Inject
+ private HttpTransport transport;
+
+ /**
+ * The dependency injection {@link Context} for the {@link Command}.
+ */
+ @Inject
+ private Context context;
+
+ /**
+ * Obtains the {@link HttpTransport} to use for executing {@link Command}s.
+ *
+ * @return the {@link HttpTransport}
+ */
+ protected HttpTransport transport() {
+ return this.transport;
+ }
+
+ /**
+ * Creates a new dependency injection {@link Context}, based on the {@link Context} used to create the
+ * {@link Command}.
+ *
+ * @return a new {@link Context}
+ */
+ protected Context createContext() {
+ return this.context.newContext();
+ }
+
+ /**
+ * Obtains the {@link HttpTransport.Request} to execute for the {@link Command}.
+ *
+ * @return the {@link HttpTransport.Request}
+ */
+ abstract protected HttpTransport.Request createRequest();
+
+ /**
+ * Creates the result for the {@link Command} based on the successful {@link HttpTransport.Response}.
+ *
+ * @param response the {@link HttpTransport.Response}
+ * @return the result
+ * @throws IOException should processing the response fail
+ */
+ abstract protected T createResult(HttpTransport.Response response)
+ throws IOException;
+
+ /**
+ * Handle when the {@link HttpTransport.Request} has been created for the {@link Command}, but not yet sent.
+ *
+ * @param request the {@link HttpTransport.Request}
+ */
+ abstract protected void onRequestCreated(HttpTransport.Request request);
+
+ /**
+ * Handle when the {@link HttpTransport.Response} has been received for the {@link Command}.
+ *
+ * @param response the {@link HttpTransport.Response}
+ */
+ abstract protected void onResponseReceived(HttpTransport.Response response);
+
+ /**
+ * Handle when the execution of a {@link HttpTransport.Request} was successful.
+ *
+ * @param request the {@link HttpTransport.Request}
+ * @param response the {@link HttpTransport.Response}
+ * @param result the result
+ */
+ abstract protected void onSuccessfulRequest(HttpTransport.Request request,
+ HttpTransport.Response response,
+ T result);
+
+ /**
+ * Handle when the execution of a {@link HttpTransport.Request} was unsuccessful.
+ *
+ * @param request the {@link HttpTransport.Request}
+ * @param response the {@link HttpTransport.Response}
+ * @throws IOException should the request be unsuccessful
+ */
+ protected void onUnsuccessfulRequest(final HttpTransport.Request request,
+ final HttpTransport.Response response)
+ throws IOException {
+
+ throw new IOException("Request failed with " + response.statusCode()
+ + ". Failed to execute " + request.method() + " " + request.path());
+ }
+
+ /**
+ * Handle when the execution of a {@link HttpTransport.Request} failed with the specified {@link Throwable}.
+ *
+ * @param request the {@link HttpTransport.Request}
+ * @param throwable the {@link Throwable}
+ * @return the result if the request was recoverable
+ */
+ protected T onRequestFailed(final HttpTransport.Request request,
+ final Throwable throwable) {
+
+ throw (throwable instanceof RuntimeException)
+ ? (RuntimeException) throwable
+ : new RuntimeException("Request Failed", throwable);
+ }
+
+ /**
+ * Handle when attempting to process the {@link HttpTransport.Response} failed.
+ *
+ * @param request the {@link HttpTransport.Request}
+ * @param response the {@link HttpTransport.Response}
+ * @param throwable the {@link Throwable}
+ */
+ public void onUnprocessableResponse(final HttpTransport.Request request,
+ final HttpTransport.Response response,
+ final Throwable throwable) {
+ response.close();
+ }
+
+ @Override
+ public T submit() {
+ final var request = createRequest();
+
+ try {
+ onRequestCreated(request);
+
+ final var response = transport().execute(request);
+
+ onResponseReceived(response);
+
+ try {
+ if (!response.isSuccessful()) {
+ onUnsuccessfulRequest(request, response);
+ }
+
+ final var result = createResult(response);
+
+ onSuccessfulRequest(request, response, result);
+
+ return result;
+
+ } catch (final Throwable throwable) {
+ onUnprocessableResponse(request, response, throwable);
+ throw throwable;
+ }
+
+ } catch (final Throwable recoverable) {
+ return onRequestFailed(request, recoverable);
+ }
+ }
+}
diff --git a/spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/command/AbstractEventBasedBlockingCommand.java b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/AbstractEventBasedBlockingCommand.java
similarity index 80%
rename from spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/command/AbstractEventBasedBlockingCommand.java
rename to spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/AbstractEventBasedBlockingCommand.java
index 0def6e2..e1b7231 100644
--- a/spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/command/AbstractEventBasedBlockingCommand.java
+++ b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/AbstractEventBasedBlockingCommand.java
@@ -1,17 +1,17 @@
-package build.spawn.docker.okhttp.command;
+package build.spawn.docker.jdk.command;
/*-
* #%L
- * Spawn Docker (OkHttp Client)
+ * Spawn Docker (JDK Client)
* %%
* Copyright (C) 2026 Workday, Inc.
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
- *
+ *
* http://www.apache.org/licenses/LICENSE-2.0
- *
+ *
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -23,9 +23,8 @@
import build.base.flow.Publisher;
import build.base.foundation.Lazy;
import build.spawn.docker.Event;
+import build.spawn.docker.jdk.HttpTransport;
import jakarta.inject.Inject;
-import okhttp3.Request;
-import okhttp3.Response;
import java.util.concurrent.CompletableFuture;
@@ -46,7 +45,7 @@ public abstract class AbstractEventBasedBlockingCommand extends AbstractBlock
private Publisher publisher;
/**
- * The {@link CompletableFuture} indicating when the required {@link Event}s have been received or has failed
+ * The {@link CompletableFuture} indicating when the required {@link Event}s have been received.
*/
private final Lazy> processing;
@@ -65,24 +64,17 @@ protected AbstractEventBasedBlockingCommand() {
*/
abstract protected CompletableFuture> subscribe(Publisher publisher);
-
@Override
- protected void onRequestCreated(final Request request) {
- // subscribe to the publisher for the required Events
+ protected void onRequestCreated(final HttpTransport.Request request) {
this.processing.set(subscribe(this.publisher));
-
super.onRequestCreated(request);
}
@Override
- protected void onSuccessfulRequest(final Request request,
- final Response response,
+ protected void onSuccessfulRequest(final HttpTransport.Request request,
+ final HttpTransport.Response response,
final T result) {
-
super.onSuccessfulRequest(request, response, result);
-
- // now wait for the required processing to complete
- this.processing.orElseThrow()
- .join();
+ this.processing.orElseThrow().join();
}
}
diff --git a/spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/command/AbstractNonBlockingCommand.java b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/AbstractNonBlockingCommand.java
similarity index 60%
rename from spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/command/AbstractNonBlockingCommand.java
rename to spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/AbstractNonBlockingCommand.java
index b8631cf..e46d22a 100644
--- a/spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/command/AbstractNonBlockingCommand.java
+++ b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/AbstractNonBlockingCommand.java
@@ -1,17 +1,17 @@
-package build.spawn.docker.okhttp.command;
+package build.spawn.docker.jdk.command;
/*-
* #%L
- * Spawn Docker (OkHttp Client)
+ * Spawn Docker (JDK Client)
* %%
* Copyright (C) 2026 Workday, Inc.
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
- *
+ *
* http://www.apache.org/licenses/LICENSE-2.0
- *
+ *
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -22,17 +22,13 @@
import build.base.foundation.AtomicEnum;
import build.base.foundation.Lazy;
-import okhttp3.Call;
-import okhttp3.OkHttpClient;
-import okhttp3.Request;
-import okhttp3.Response;
+import build.spawn.docker.jdk.HttpTransport;
import java.io.Closeable;
import java.util.concurrent.CompletableFuture;
-import java.util.concurrent.TimeUnit;
/**
- * An abstract {@link Command} that continues to process the {@link Response} after execution, until the
+ * An abstract {@link Command} that continues to process the response after execution, until the
* {@link Command} is closed or a processing failure occurs.
*
* @param the type of result produced by the {@link Command}
@@ -43,14 +39,9 @@ public abstract class AbstractNonBlockingCommand extends AbstractCommand
implements Closeable {
/**
- * The {@link Lazy} {@link Call} for the {@link Command}.
- */
- protected final Lazy call;
-
- /**
- * The {@link Lazy} {@link Response} to process.
+ * The active {@link HttpTransport.Response} being processed.
*/
- protected final Lazy response;
+ protected final Lazy activeResponse;
/**
* The {@link Lazy} result of the {@link Command}.
@@ -58,8 +49,7 @@ public abstract class AbstractNonBlockingCommand extends AbstractCommand
protected final Lazy result;
/**
- * A {@link CompletableFuture} indicating when the {@link Response} processing has completed, either exceptionally
- * or otherwise.
+ * A {@link CompletableFuture} indicating when processing has completed.
*/
private final CompletableFuture processing;
@@ -72,73 +62,47 @@ public abstract class AbstractNonBlockingCommand extends AbstractCommand
* Constructs an {@link AbstractNonBlockingCommand}.
*/
protected AbstractNonBlockingCommand() {
- this.call = Lazy.empty();
- this.response = Lazy.empty();
+ this.activeResponse = Lazy.empty();
this.result = Lazy.empty();
this.processing = new CompletableFuture<>();
this.state = AtomicEnum.of(State.INITIALIZING);
}
@Override
- protected OkHttpClient httpClient() {
- // ensure the reading from the response never times out (so we can continuously wait for responses)
- return super.httpClient().newBuilder()
- .connectTimeout(0, TimeUnit.MILLISECONDS)
- .readTimeout(0, TimeUnit.MILLISECONDS)
- .writeTimeout(0, TimeUnit.MILLISECONDS)
- .build();
- }
-
- @Override
- protected void onRequestCreated(final Request request) {
+ protected void onRequestCreated(final HttpTransport.Request request) {
// by default, we don't do anything when the Request is created
}
@Override
- protected void onCallCreated(final Call call) {
- this.call.set(call);
- }
-
- @Override
- protected void onResponseReceived(final Response response) {
- this.response.set(response);
+ protected void onResponseReceived(final HttpTransport.Response response) {
+ this.activeResponse.set(response);
this.state.compareAndSet(State.INITIALIZING, State.PROCESSING);
}
@Override
- protected void onSuccessfulRequest(final Request request,
- final Response response,
+ protected void onSuccessfulRequest(final HttpTransport.Request request,
+ final HttpTransport.Response response,
final T result) {
-
- // capture the result so we can later complete the processing with it
this.result.set(result);
-
// NOTE: we don't close the Response, so that we can continue to consume it
}
protected void onProcessingError(final Throwable throwable) {
if (this.state.compareAndSet(State.PROCESSING, State.COMPLETED)) {
- // complete the processing exceptionally
this.processing.completeExceptionally(throwable);
-
- // cancel the Call
- this.call.ifPresent(Call::cancel);
+ this.activeResponse.ifPresent(HttpTransport.Response::cancel);
}
}
protected void onProcessingCompletion() {
if (this.state.compareAndSet(State.PROCESSING, State.COMPLETED)) {
- // complete the processing with the result
this.processing.complete(this.result.orElse(null));
-
- // cancel the Call
- this.call.ifPresent(Call::cancel);
+ this.activeResponse.ifPresent(HttpTransport.Response::cancel);
}
}
/**
- * Obtains a {@link CompletableFuture} that will be completed when the {@link Response} processing has completed,
- * either exceptionally or otherwise.
+ * Obtains a {@link CompletableFuture} that will be completed when processing has completed.
*
* @return the {@link CompletableFuture}
*/
@@ -156,14 +120,9 @@ protected void onClosing() {
@Override
public void close() {
if (this.state.compareAndSet(State.PROCESSING, State.COMPLETED)) {
- // notify that the Command is being closed
onClosing();
-
- // complete the processing with the result
this.processing.complete(this.result.orElse(null));
-
- // cancel the Call
- this.call.ifPresent(Call::cancel);
+ this.activeResponse.ifPresent(HttpTransport.Response::cancel);
}
}
diff --git a/spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/command/AttachContainer.java b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/AttachContainer.java
similarity index 77%
rename from spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/command/AttachContainer.java
rename to spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/AttachContainer.java
index 453efd6..e9a884e 100644
--- a/spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/command/AttachContainer.java
+++ b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/AttachContainer.java
@@ -1,8 +1,8 @@
-package build.spawn.docker.okhttp.command;
+package build.spawn.docker.jdk.command;
/*-
* #%L
- * Spawn Docker (OkHttp Client)
+ * Spawn Docker (JDK Client)
* %%
* Copyright (C) 2026 Workday, Inc.
* %%
@@ -24,11 +24,8 @@
import build.base.io.NullWriter;
import build.base.io.Terminal;
import build.spawn.docker.Container;
+import build.spawn.docker.jdk.HttpTransport;
import jakarta.inject.Inject;
-import okhttp3.FormBody;
-import okhttp3.HttpUrl;
-import okhttp3.Request;
-import okhttp3.Response;
import java.io.IOException;
import java.io.PipedReader;
@@ -61,25 +58,14 @@ public AttachContainer(final Configuration configuration) {
}
@Override
- protected Request createRequest(final HttpUrl.Builder httpUrlBuilder) {
- return new Request.Builder()
- .url(httpUrlBuilder
- .addPathSegment("containers")
- .addPathSegment(this.container.id())
- .addPathSegment("attach")
- .build())
- .post(new FormBody.Builder()
- .add("logs", "true")
- .add("stream", "true")
- .add("stdin", "false")
- .add("stdout", "true")
- .add("stderr", "true")
- .build())
- .build();
+ protected HttpTransport.Request createRequest() {
+ return HttpTransport.Request.post(
+ "/containers/" + this.container.id() + "/attach?logs=true&stream=true&stdin=false&stdout=true&stderr=true",
+ null);
}
@Override
- protected Terminal createResult(final Response response)
+ protected Terminal createResult(final HttpTransport.Response response)
throws IOException {
// establish PipedReaders for stdout and stderr
@@ -117,7 +103,7 @@ public CompletableFuture> onClosed() {
};
// establish a FrameProcessor to redirect the I/O frames to the Terminal
- final var processor = new FrameProcessor(response, outputReader, errorReader);
+ final var processor = new FrameProcessor(response.bodyStream(), outputReader, errorReader);
processor.processing()
.whenComplete((_, error) -> {
diff --git a/spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/command/Authenticate.java b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/Authenticate.java
similarity index 74%
rename from spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/command/Authenticate.java
rename to spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/Authenticate.java
index 02cc76e..b9f1635 100644
--- a/spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/command/Authenticate.java
+++ b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/Authenticate.java
@@ -1,8 +1,8 @@
-package build.spawn.docker.okhttp.command;
+package build.spawn.docker.jdk.command;
/*-
* #%L
- * Spawn Docker (OkHttp Client)
+ * Spawn Docker (JDK Client)
* %%
* Copyright (C) 2026 Workday, Inc.
* %%
@@ -26,18 +26,14 @@
import build.base.option.Password;
import build.base.option.Username;
import build.spawn.docker.Session;
+import build.spawn.docker.jdk.HttpTransport;
import build.spawn.docker.option.DockerRegistry;
import build.spawn.docker.option.IdentityToken;
import com.fasterxml.jackson.databind.ObjectMapper;
-import com.fasterxml.jackson.databind.node.ArrayNode;
-import com.fasterxml.jackson.databind.node.ObjectNode;
import jakarta.inject.Inject;
-import okhttp3.HttpUrl;
-import okhttp3.Request;
-import okhttp3.RequestBody;
-import okhttp3.Response;
import java.io.IOException;
+import java.nio.charset.StandardCharsets;
/**
* The {@code Docker Daemon} {@link Command} to authenticate and validate credentials with a Docker Registry using the
@@ -80,7 +76,7 @@ public class Authenticate
private Configuration configuration;
@Override
- protected Request createRequest(final HttpUrl.Builder httpUrlBuilder) {
+ protected HttpTransport.Request createRequest() {
// establish an ObjectNode containing the auth json
final var node = this.objectMapper.createObjectNode();
@@ -91,27 +87,16 @@ protected Request createRequest(final HttpUrl.Builder httpUrlBuilder) {
.ifPresent(email -> node.put("email", email));
node.put("serveraddress", this.dockerRegistry.get().toString());
- return new Request.Builder()
- .url(httpUrlBuilder
- .addPathSegment("auth")
- .build())
- .post(RequestBody.create(node.toString(), MEDIA_TYPE_JSON))
- .build();
- }
-
- private ArrayNode getOrCreateArray(final ObjectNode node, final String key) {
- final var child = node.get(key);
- if (child instanceof ArrayNode) {
- return (ArrayNode) child;
- }
- return node.putArray(key);
+ return HttpTransport.Request
+ .post("/auth", node.toString().getBytes(StandardCharsets.UTF_8))
+ .withContentType("application/json");
}
@Override
- protected IdentityToken createResult(final Response response)
+ protected IdentityToken createResult(final HttpTransport.Response response)
throws IOException {
- final var body = response.body().string();
+ final var body = response.bodyString();
final var json = this.objectMapper.readTree(body);
final var identityToken = json.get("IdentityToken").asText();
diff --git a/spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/command/BuildImage.java b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/BuildImage.java
similarity index 76%
rename from spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/command/BuildImage.java
rename to spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/BuildImage.java
index 1cea244..00ebd9b 100644
--- a/spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/command/BuildImage.java
+++ b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/BuildImage.java
@@ -1,8 +1,8 @@
-package build.spawn.docker.okhttp.command;
+package build.spawn.docker.jdk.command;
/*-
* #%L
- * Spawn Docker (OkHttp Client)
+ * Spawn Docker (JDK Client)
* %%
* Copyright (C) 2026 Workday, Inc.
* %%
@@ -23,17 +23,14 @@
import build.base.configuration.Configuration;
import build.base.flow.CompletingSubscriber;
import build.spawn.docker.Image;
+import build.spawn.docker.jdk.HttpTransport;
import build.spawn.docker.option.ImageName;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.inject.Inject;
-import okhttp3.HttpUrl;
-import okhttp3.MediaType;
-import okhttp3.Request;
-import okhttp3.RequestBody;
-import okhttp3.Response;
import java.io.IOException;
+import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Objects;
import java.util.Optional;
@@ -49,11 +46,6 @@
public class BuildImage
extends AbstractBlockingCommand> {
- /**
- * The {@link MediaType} for {@code tar} files.
- */
- private final static MediaType MEDIATYPE = MediaType.parse("application/x-tar");
-
/**
* The {@link Path} to the {@code Docker Context} for building the {@link Image}.
*/
@@ -86,25 +78,21 @@ public BuildImage(final Path contextPath,
}
@Override
- protected Request createRequest(final HttpUrl.Builder httpUrlBuilder) {
-
- // configure the HttpUrl.Builder for the request
- httpUrlBuilder
- .addPathSegment("build")
- .addQueryParameter("q", "false");
-
- // include the optionally provided ImageNames
+ protected HttpTransport.Request createRequest() {
+ final var tagParams = new StringBuilder("?q=false");
this.configuration.stream(ImageName.class)
- .forEach(name -> httpUrlBuilder.addQueryParameter("t", name.get()));
-
- return new Request.Builder()
- .url(httpUrlBuilder.build())
- .post(RequestBody.create(this.contextPath.toFile(), MEDIATYPE))
- .build();
+ .forEach(name -> tagParams.append("&t=").append(name.get()));
+ try {
+ return HttpTransport.Request
+ .post("/build" + tagParams, Files.readAllBytes(this.contextPath))
+ .withContentType("application/x-tar");
+ } catch (final IOException e) {
+ throw new RuntimeException("Failed to read build context", e);
+ }
}
@Override
- protected Optional createResult(final Response response)
+ protected Optional createResult(final HttpTransport.Response response)
throws IOException {
// establish the CompletingObserver observe when image ID has been generated
@@ -120,7 +108,7 @@ protected Optional createResult(final Response response)
// process the entire InputStream from the Response to essentially wait for the image to be created
final JsonNodeInputStreamProcessor processor = new JsonNodeInputStreamProcessor(this.objectMapper);
- processor.process(response.body().byteStream(), completingSubscriber);
+ processor.process(response.bodyStream(), completingSubscriber);
// we've completed building when the ImageId is available and "Successful" has been observed
onSuccess.join();
diff --git a/spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/command/Command.java b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/Command.java
similarity index 92%
rename from spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/command/Command.java
rename to spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/Command.java
index 6288162..be6eb1e 100644
--- a/spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/command/Command.java
+++ b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/Command.java
@@ -1,8 +1,8 @@
-package build.spawn.docker.okhttp.command;
+package build.spawn.docker.jdk.command;
/*-
* #%L
- * Spawn Docker (OkHttp Client)
+ * Spawn Docker (JDK Client)
* %%
* Copyright (C) 2026 Workday, Inc.
* %%
diff --git a/spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/command/CopyFiles.java b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/CopyFiles.java
similarity index 73%
rename from spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/command/CopyFiles.java
rename to spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/CopyFiles.java
index 6b5ff0e..cb40a5a 100644
--- a/spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/command/CopyFiles.java
+++ b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/CopyFiles.java
@@ -1,8 +1,8 @@
-package build.spawn.docker.okhttp.command;
+package build.spawn.docker.jdk.command;
/*-
* #%L
- * Spawn Docker (OkHttp Client)
+ * Spawn Docker (JDK Client)
* %%
* Copyright (C) 2026 Workday, Inc.
* %%
@@ -22,14 +22,11 @@
import build.base.archiving.TarBuilder;
import build.spawn.docker.Container;
+import build.spawn.docker.jdk.HttpTransport;
import jakarta.inject.Inject;
-import okhttp3.HttpUrl;
-import okhttp3.MediaType;
-import okhttp3.Request;
-import okhttp3.RequestBody;
-import okhttp3.Response;
import java.io.IOException;
+import java.nio.file.Files;
import java.nio.file.Path;
/**
@@ -90,29 +87,26 @@ public CopyFiles(final Path archivePath, final String destinationDirectory, fina
}
@Override
- protected Request createRequest(final HttpUrl.Builder httpUrlBuilder) {
- final MediaType mType = MediaType.parse("application/octet-stream; charset=utf-8");
- final RequestBody requestBody = RequestBody.create(this.archivePath.toFile(), mType);
- return new Request.Builder()
- .url(httpUrlBuilder
- .addPathSegment("containers")
- .addPathSegment(this.container.id())
- .addPathSegment("archive")
- .addQueryParameter("path", this.destinationDirectory)
- .build())
- .put(requestBody)
- .build();
+ protected HttpTransport.Request createRequest() {
+ try {
+ return HttpTransport.Request
+ .put("/containers/" + this.container.id() + "/archive?path=" + this.destinationDirectory,
+ Files.readAllBytes(this.archivePath))
+ .withContentType("application/octet-stream");
+ } catch (final IOException e) {
+ throw new RuntimeException("Failed to read archive", e);
+ }
}
@Override
- protected Container createResult(final Response response)
+ protected Container createResult(final HttpTransport.Response response)
throws IOException {
// 400 Bad Parameter
// 403 permission denied (read only)
// 404 no such destination path found
// 500 server error
- if (response.code() != 200) {
- throw new RuntimeException(response.message());
+ if (response.statusCode() != 200) {
+ throw new RuntimeException("CopyFiles failed with HTTP " + response.statusCode());
}
return this.container;
}
diff --git a/spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/command/CreateContainer.java b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/CreateContainer.java
similarity index 87%
rename from spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/command/CreateContainer.java
rename to spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/CreateContainer.java
index 909d148..e3a2eda 100644
--- a/spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/command/CreateContainer.java
+++ b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/CreateContainer.java
@@ -1,8 +1,8 @@
-package build.spawn.docker.okhttp.command;
+package build.spawn.docker.jdk.command;
/*-
* #%L
- * Spawn Docker (OkHttp Client)
+ * Spawn Docker (JDK Client)
* %%
* Copyright (C) 2026 Workday, Inc.
* %%
@@ -26,7 +26,8 @@
import build.codemodel.injection.Context;
import build.spawn.docker.Container;
import build.spawn.docker.Image;
-import build.spawn.docker.okhttp.model.OkHttpBasedContainer;
+import build.spawn.docker.jdk.HttpTransport;
+import build.spawn.docker.jdk.model.DockerContainer;
import build.spawn.docker.option.ContainerName;
import build.spawn.docker.option.DockerOption;
import build.spawn.docker.option.ExposedPort;
@@ -39,12 +40,9 @@
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import jakarta.inject.Inject;
-import okhttp3.HttpUrl;
-import okhttp3.Request;
-import okhttp3.RequestBody;
-import okhttp3.Response;
import java.io.IOException;
+import java.nio.charset.StandardCharsets;
import java.util.Objects;
/**
@@ -100,7 +98,7 @@ public CreateContainer(final Image image,
}
@Override
- protected Request createRequest(final HttpUrl.Builder httpUrlBuilder) {
+ protected HttpTransport.Request createRequest() {
// establish an ObjectNode containing the containers/create json
final var node = this.objectMapper.createObjectNode();
@@ -138,14 +136,10 @@ protected Request createRequest(final HttpUrl.Builder httpUrlBuilder) {
final var name = this.configuration.getOptionalValue(ContainerName.class)
.orElse(new UniqueNameGenerator(".").next());
- return new Request.Builder()
- .url(httpUrlBuilder
- .addPathSegment("containers")
- .addPathSegment("create")
- .addQueryParameter("name", name)
- .build())
- .post(RequestBody.create(node.toString(), MEDIA_TYPE_JSON))
- .build();
+ return HttpTransport.Request
+ .post("/containers/create?name=" + name,
+ node.toString().getBytes(StandardCharsets.UTF_8))
+ .withContentType("application/json");
}
private ArrayNode getOrCreateArray(final ObjectNode node, final String key) {
@@ -157,7 +151,7 @@ private ArrayNode getOrCreateArray(final ObjectNode node, final String key) {
}
@Override
- protected Container createResult(final Response response)
+ protected Container createResult(final HttpTransport.Response response)
throws IOException {
final var context = createContext();
@@ -166,10 +160,10 @@ protected Container createResult(final Response response)
context.bind(Configuration.class).to(this.configuration);
// bind the JsonNode representation of the response
- final var json = response.body().string();
+ final var json = response.bodyString();
context.bind(JsonNode.class).to(this.objectMapper.readTree(json));
// create the Container to return
- return context.create(OkHttpBasedContainer.class);
+ return context.create(DockerContainer.class);
}
}
diff --git a/spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/command/CreateExecution.java b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/CreateExecution.java
similarity index 83%
rename from spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/command/CreateExecution.java
rename to spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/CreateExecution.java
index bc452ce..a928798 100644
--- a/spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/command/CreateExecution.java
+++ b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/CreateExecution.java
@@ -1,8 +1,8 @@
-package build.spawn.docker.okhttp.command;
+package build.spawn.docker.jdk.command;
/*-
* #%L
- * Spawn Docker (OkHttp Client)
+ * Spawn Docker (JDK Client)
* %%
* Copyright (C) 2026 Workday, Inc.
* %%
@@ -24,17 +24,15 @@
import build.base.io.Terminal;
import build.spawn.docker.Container;
import build.spawn.docker.Execution;
+import build.spawn.docker.jdk.HttpTransport;
import build.spawn.docker.option.DockerOption;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import jakarta.inject.Inject;
-import okhttp3.HttpUrl;
-import okhttp3.Request;
-import okhttp3.RequestBody;
-import okhttp3.Response;
import java.io.IOException;
+import java.nio.charset.StandardCharsets;
import java.util.Objects;
/**
@@ -85,7 +83,7 @@ public CreateExecution(final Container container,
}
@Override
- protected Request createRequest(final HttpUrl.Builder httpUrlBuilder) {
+ protected HttpTransport.Request createRequest() {
// establish an ObjectNode containing the containers/create json
final ObjectNode node = this.objectMapper.createObjectNode();
@@ -98,22 +96,18 @@ protected Request createRequest(final HttpUrl.Builder httpUrlBuilder) {
this.configuration.stream(DockerOption.class)
.forEach(option -> option.configure(node, this.objectMapper));
- return new Request.Builder()
- .url(httpUrlBuilder
- .addPathSegment("containers")
- .addPathSegment(this.container.id())
- .addPathSegment("exec")
- .build())
- .post(RequestBody.create(node.toString(), MEDIA_TYPE_JSON))
- .build();
+ return HttpTransport.Request
+ .post("/containers/" + this.container.id() + "/exec",
+ node.toString().getBytes(StandardCharsets.UTF_8))
+ .withContentType("application/json");
}
@Override
- protected String createResult(final Response response)
+ protected String createResult(final HttpTransport.Response response)
throws IOException {
// bind the JsonNode representation of the response
- final String json = response.body().string();
+ final String json = response.bodyString();
final JsonNode node = this.objectMapper.readTree(json);
// obtain the Execution identity to return
diff --git a/spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/command/CreateNetwork.java b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/CreateNetwork.java
similarity index 77%
rename from spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/command/CreateNetwork.java
rename to spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/CreateNetwork.java
index 690f896..ddae7eb 100644
--- a/spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/command/CreateNetwork.java
+++ b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/CreateNetwork.java
@@ -1,8 +1,8 @@
-package build.spawn.docker.okhttp.command;
+package build.spawn.docker.jdk.command;
/*-
* #%L
- * Spawn Docker (OkHttp Client)
+ * Spawn Docker (JDK Client)
* %%
* Copyright (C) 2026 Workday, Inc.
* %%
@@ -21,15 +21,13 @@
*/
import build.base.configuration.Configuration;
+import build.spawn.docker.jdk.HttpTransport;
import build.spawn.docker.option.DockerOption;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.inject.Inject;
-import okhttp3.HttpUrl;
-import okhttp3.Request;
-import okhttp3.RequestBody;
-import okhttp3.Response;
import java.io.IOException;
+import java.nio.charset.StandardCharsets;
import java.util.Optional;
/**
@@ -71,7 +69,7 @@ public CreateNetwork(final String name, final Configuration configuration) {
}
@Override
- protected Request createRequest(final HttpUrl.Builder httpUrlBuilder) {
+ protected HttpTransport.Request createRequest() {
final var node = this.objectMapper.createObjectNode();
node.put("Name", this.name);
node.put("CheckDuplicate", true);
@@ -79,20 +77,16 @@ protected Request createRequest(final HttpUrl.Builder httpUrlBuilder) {
this.configuration.stream(DockerOption.class)
.forEach(option -> option.configure(node, this.objectMapper));
- return new Request.Builder()
- .url(httpUrlBuilder
- .addPathSegment("networks")
- .addPathSegment("create")
- .build())
- .post(RequestBody.create(node.toString(), MEDIA_TYPE_JSON))
- .build();
+ return HttpTransport.Request
+ .post("/networks/create", node.toString().getBytes(StandardCharsets.UTF_8))
+ .withContentType("application/json");
}
@Override
- protected Optional createResult(final Response response)
+ protected Optional createResult(final HttpTransport.Response response)
throws IOException {
- if (response.code() == 201) {
+ if (response.statusCode() == 201) {
return Optional.of(this.name);
}
return Optional.empty();
diff --git a/spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/command/DeleteContainer.java b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/DeleteContainer.java
similarity index 72%
rename from spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/command/DeleteContainer.java
rename to spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/DeleteContainer.java
index 67b715e..978b691 100644
--- a/spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/command/DeleteContainer.java
+++ b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/DeleteContainer.java
@@ -1,8 +1,8 @@
-package build.spawn.docker.okhttp.command;
+package build.spawn.docker.jdk.command;
/*-
* #%L
- * Spawn Docker (OkHttp Client)
+ * Spawn Docker (JDK Client)
* %%
* Copyright (C) 2026 Workday, Inc.
* %%
@@ -22,11 +22,9 @@
import build.base.configuration.Configuration;
import build.spawn.docker.Container;
+import build.spawn.docker.jdk.HttpTransport;
import build.spawn.docker.option.RemoveVolumes;
import jakarta.inject.Inject;
-import okhttp3.HttpUrl;
-import okhttp3.Request;
-import okhttp3.Response;
import java.io.IOException;
@@ -64,20 +62,14 @@ public DeleteContainer(final Configuration configuration) {
}
@Override
- protected Request createRequest(final HttpUrl.Builder httpUrlBuilder) {
- return new Request.Builder()
- .url(httpUrlBuilder
- .addPathSegment("containers")
- .addPathSegment(this.container.id())
- .addQueryParameter("v", String.valueOf(this.configuration.getValue(RemoveVolumes.class)))
- .addQueryParameter("force", "true")
- .build())
- .delete()
- .build();
+ protected HttpTransport.Request createRequest() {
+ return HttpTransport.Request.delete(
+ "/containers/" + this.container.id()
+ + "?v=" + this.configuration.getValue(RemoveVolumes.class) + "&force=true");
}
@Override
- protected Void createResult(final Response response)
+ protected Void createResult(final HttpTransport.Response response)
throws IOException {
return null;
diff --git a/spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/command/DeleteImage.java b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/DeleteImage.java
similarity index 70%
rename from spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/command/DeleteImage.java
rename to spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/DeleteImage.java
index dda5f54..a05bae3 100644
--- a/spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/command/DeleteImage.java
+++ b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/DeleteImage.java
@@ -1,8 +1,8 @@
-package build.spawn.docker.okhttp.command;
+package build.spawn.docker.jdk.command;
/*-
* #%L
- * Spawn Docker (OkHttp Client)
+ * Spawn Docker (JDK Client)
* %%
* Copyright (C) 2026 Workday, Inc.
* %%
@@ -22,10 +22,8 @@
import build.base.configuration.Configuration;
import build.spawn.docker.Image;
+import build.spawn.docker.jdk.HttpTransport;
import jakarta.inject.Inject;
-import okhttp3.HttpUrl;
-import okhttp3.Request;
-import okhttp3.Response;
import java.io.IOException;
@@ -55,29 +53,22 @@ public DeleteImage(final Configuration configuration) {
}
@Override
- protected Request createRequest(final HttpUrl.Builder httpUrlBuilder) {
- return new Request.Builder()
- .url(httpUrlBuilder
- .addPathSegment("images")
- .addPathSegment(this.image.id())
- .addQueryParameter("force", "true")
- .build())
- .delete()
- .build();
+ protected HttpTransport.Request createRequest() {
+ return HttpTransport.Request.delete("/images/" + this.image.id() + "?force=true");
}
@Override
- protected void onUnsuccessfulRequest(final Request request, final Response response)
+ protected void onUnsuccessfulRequest(final HttpTransport.Request request, final HttpTransport.Response response)
throws IOException {
// 404 (missing image) is not considered a failure
- if (response.code() != 404) {
+ if (response.statusCode() != 404) {
super.onUnsuccessfulRequest(request, response);
}
}
@Override
- protected Void createResult(final Response response)
+ protected Void createResult(final HttpTransport.Response response)
throws IOException {
return null;
diff --git a/spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/command/DeleteNetwork.java b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/DeleteNetwork.java
similarity index 71%
rename from spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/command/DeleteNetwork.java
rename to spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/DeleteNetwork.java
index 09e247d..b99d485 100644
--- a/spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/command/DeleteNetwork.java
+++ b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/DeleteNetwork.java
@@ -1,8 +1,8 @@
-package build.spawn.docker.okhttp.command;
+package build.spawn.docker.jdk.command;
/*-
* #%L
- * Spawn Docker (OkHttp Client)
+ * Spawn Docker (JDK Client)
* %%
* Copyright (C) 2026 Workday, Inc.
* %%
@@ -20,11 +20,9 @@
* #L%
*/
+import build.spawn.docker.jdk.HttpTransport;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.inject.Inject;
-import okhttp3.HttpUrl;
-import okhttp3.Request;
-import okhttp3.Response;
import java.io.IOException;
@@ -58,19 +56,13 @@ public DeleteNetwork(final String nameOrId) {
}
@Override
- protected Request createRequest(final HttpUrl.Builder httpUrlBuilder) {
- return new Request.Builder()
- .url(httpUrlBuilder
- .addPathSegment("networks")
- .addPathSegment(this.nameOrId)
- .build())
- .delete()
- .build();
+ protected HttpTransport.Request createRequest() {
+ return HttpTransport.Request.delete("/networks/" + this.nameOrId);
}
@Override
- protected Boolean createResult(final Response response)
+ protected Boolean createResult(final HttpTransport.Response response)
throws IOException {
- return response.code() == 204;
+ return response.statusCode() == 204;
}
}
diff --git a/spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/command/FileInformation.java b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/FileInformation.java
similarity index 74%
rename from spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/command/FileInformation.java
rename to spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/FileInformation.java
index d020464..ab30485 100644
--- a/spawn-docker-okhttp/src/main/java/build/spawn/docker/okhttp/command/FileInformation.java
+++ b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/FileInformation.java
@@ -1,8 +1,8 @@
-package build.spawn.docker.okhttp.command;
+package build.spawn.docker.jdk.command;
/*-
* #%L
- * Spawn Docker (OkHttp Client)
+ * Spawn Docker (JDK Client)
* %%
* Copyright (C) 2026 Workday, Inc.
* %%
@@ -21,11 +21,9 @@
*/
import build.spawn.docker.Container;
+import build.spawn.docker.jdk.HttpTransport;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.inject.Inject;
-import okhttp3.HttpUrl;
-import okhttp3.Request;
-import okhttp3.Response;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
@@ -64,23 +62,17 @@ public FileInformation(final Path filePath) {
}
@Override
- protected Request createRequest(final HttpUrl.Builder httpUrlBuilder) {
- return new Request.Builder()
- .url(httpUrlBuilder
- .addPathSegment("containers")
- .addPathSegment(this.container.id())
- .addPathSegment("archive")
- .addQueryParameter("path", this.filePath.toFile().getAbsolutePath())
- .build())
- .head()
- .build();
+ protected HttpTransport.Request createRequest() {
+ return HttpTransport.Request.head(
+ "/containers/" + this.container.id()
+ + "/archive?path=" + this.filePath.toFile().getAbsolutePath());
}
@SuppressWarnings("unchecked")
@Override
- protected Optional