diff --git a/pom.xml b/pom.xml index 69cc3d9..6f02552 100644 --- a/pom.xml +++ b/pom.xml @@ -68,10 +68,9 @@ 3.27.7 - 0.23.1 + 0.24.0 1.18.4 - 0.21.1 - 2.21.2 + 0.21.2 6.0.3 2.0.1 5.23.0 @@ -236,22 +235,19 @@ ${codemodel.version} + + + build.base + base-json + ${base.version} + + net.bytebuddy byte-buddy ${byte-buddy.version} - - com.fasterxml.jackson.core - jackson-core - ${jackson-core.version} - - - com.fasterxml.jackson.core - jackson-databind - ${jackson-core.version} - jakarta.inject jakarta.inject-api diff --git a/spawn-docker-jdk/pom.xml b/spawn-docker-jdk/pom.xml index deaba71..0e91b15 100644 --- a/spawn-docker-jdk/pom.xml +++ b/spawn-docker-jdk/pom.xml @@ -73,13 +73,8 @@ - com.fasterxml.jackson.core - jackson-core - - - - com.fasterxml.jackson.core - jackson-databind + build.base + base-json diff --git a/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/AbstractSession.java b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/AbstractSession.java index 28b1704..6617c78 100644 --- a/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/AbstractSession.java +++ b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/AbstractSession.java @@ -25,6 +25,7 @@ import build.base.flow.Publicist; import build.base.flow.Publisher; import build.base.flow.SubscriberRegistry; +import build.base.json.JsonObject; import build.base.option.Email; import build.base.option.Password; import build.base.option.Username; @@ -51,7 +52,6 @@ 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 java.nio.charset.StandardCharsets; import java.nio.file.Path; @@ -142,9 +142,6 @@ protected AbstractSession(final InjectionFramework injectionFramework, this.eventSubscriber = new CompletingSubscriber<>(); this.publicist.subscribe(this.eventSubscriber); - // establish an ObjectMapper for working with JSON - final ObjectMapper objectMapper = new ObjectMapper(); - // establish the dependency injection context this.context = injectionFramework .newContext(); @@ -155,7 +152,6 @@ protected AbstractSession(final InjectionFramework injectionFramework, this.context.bind(Session.class).to(this); this.context.bind(AbstractSession.class).to(this); this.context.bind((Class) getClass()).to(this); - this.context.bind(ObjectMapper.class).to(objectMapper); this.context.bind(Configuration.class).to(this.configuration); this.context.bind(Publicist.class).to(this.publicist); this.context.bind(Publisher.class).to(this.publicist); @@ -178,23 +174,23 @@ protected AbstractSession(final InjectionFramework injectionFramework, .to(identityToken); // establish the Authentication JSON - final var json = objectMapper.createObjectNode(); + final var jsonBuilder = JsonObject.builder(); if (identityToken.isEmpty()) { this.configuration.getOptionalValue(Username.class) - .ifPresent(username -> json.put("username", username)); + .ifPresent(username -> jsonBuilder.put("username", username)); this.configuration.getOptionalValue(Password.class) - .ifPresent(password -> json.put("password", password)); + .ifPresent(password -> jsonBuilder.put("password", password)); this.configuration.getOptionalValue(Email.class) - .ifPresent(email -> json.put("email", email)); + .ifPresent(email -> jsonBuilder.put("email", email)); this.configuration.getOptionalValue(DockerRegistry.class) - .ifPresent(url -> json.put("serveraddress", url.getHost())); + .ifPresent(url -> jsonBuilder.put("serveraddress", url.getHost())); } else { - json.put("identitytoken", identityToken.get()); + jsonBuilder.put("identitytoken", identityToken.get()); } - xRegistryAuth = Optional.of(json.toString()); + xRegistryAuth = Optional.of(jsonBuilder.build().toJsonString()); } else { // determine the Configuration-provided IdentityToken @@ -211,9 +207,11 @@ protected AbstractSession(final InjectionFramework injectionFramework, xRegistryAuth = Optional.empty(); } else { - final var json = objectMapper.createObjectNode(); - json.put("identitytoken", identityToken.get()); - xRegistryAuth = Optional.of(json.toString()); + xRegistryAuth = Optional.of( + JsonObject.builder() + .put("identitytoken", identityToken.get()) + .build() + .toJsonString()); } } diff --git a/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/Authenticate.java b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/Authenticate.java index b9f1635..9f0dbb3 100644 --- a/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/Authenticate.java +++ b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/Authenticate.java @@ -9,9 +9,9 @@ * 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,6 +22,8 @@ import build.base.configuration.Configuration; import build.base.foundation.Strings; +import build.base.json.Json; +import build.base.json.JsonObject; import build.base.option.Email; import build.base.option.Password; import build.base.option.Username; @@ -29,7 +31,6 @@ 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 jakarta.inject.Inject; import java.io.IOException; @@ -45,12 +46,6 @@ public class Authenticate extends AbstractBlockingCommand { - /** - * The {@link ObjectMapper} for parsing json. - */ - @Inject - private ObjectMapper objectMapper; - /** * The {@link Username} for authentication. */ @@ -79,16 +74,16 @@ public class Authenticate protected HttpTransport.Request createRequest() { // establish an ObjectNode containing the auth json - final var node = this.objectMapper.createObjectNode(); + final var builder = JsonObject.builder() + .put("username", this.username.get()) + .put("password", this.password.get()) + .put("serveraddress", this.dockerRegistry.get().toString()); - node.put("username", this.username.get()); - node.put("password", this.password.get()); this.configuration.getOptionalValue(Email.class) - .ifPresent(email -> node.put("email", email)); - node.put("serveraddress", this.dockerRegistry.get().toString()); + .ifPresent(email -> builder.put("email", email)); return HttpTransport.Request - .post("/auth", node.toString().getBytes(StandardCharsets.UTF_8)) + .post("/auth", builder.build().toJsonString().getBytes(StandardCharsets.UTF_8)) .withContentType("application/json"); } @@ -96,10 +91,10 @@ protected HttpTransport.Request createRequest() { protected IdentityToken createResult(final HttpTransport.Response response) throws IOException { - final var body = response.bodyString(); - final var json = this.objectMapper.readTree(body); - - final var identityToken = json.get("IdentityToken").asText(); + final var json = Json.parse(response.bodyString()).asObject(); + final var identityToken = json.has("IdentityToken") + ? json.getString("IdentityToken") + : ""; return Strings.isEmpty(identityToken) ? IdentityToken.of(this.password.get()) diff --git a/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/BuildImage.java b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/BuildImage.java index 00ebd9b..8d69826 100644 --- a/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/BuildImage.java +++ b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/BuildImage.java @@ -9,9 +9,9 @@ * 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,12 +22,10 @@ import build.base.configuration.Configuration; import build.base.flow.CompletingSubscriber; +import build.base.json.JsonValue; 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 java.io.IOException; import java.nio.file.Files; @@ -56,12 +54,6 @@ public class BuildImage */ private final Configuration configuration; - /** - * The {@link ObjectMapper} for parsing json. - */ - @Inject - private ObjectMapper objectMapper; - /** * Constructs a {@link BuildImage} {@link Command}. * @@ -97,17 +89,17 @@ protected Optional createResult(final HttpTransport.Response response) // establish the CompletingObserver observe when image ID has been generated // (we want to capture {"aux":{"ID":"sha256:f65c628a75fe8b3e982165d1a4ceaf521fadab8da2136702c59e184d7be3e243"}) - final var completingSubscriber = new CompletingSubscriber(); + final var completingSubscriber = new CompletingSubscriber(); final var onImageBuilt = completingSubscriber.when( - json -> json.get("aux") != null, - json -> json.get("aux").get("ID").asText()); + json -> json.asObject().has("aux"), + json -> json.get("aux").getString("ID")); final CompletableFuture onSuccess = completingSubscriber.when( - json -> json.get("stream") != null, - json -> json.get("stream").asText().contains("Successful")); + json -> json.asObject().has("stream"), + json -> json.getString("stream").contains("Successful")); // process the entire InputStream from the Response to essentially wait for the image to be created - final JsonNodeInputStreamProcessor processor = new JsonNodeInputStreamProcessor(this.objectMapper); + final var processor = new JsonNodeInputStreamProcessor(); processor.process(response.bodyStream(), completingSubscriber); // we've completed building when the ImageId is available and "Successful" has been observed diff --git a/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/CreateContainer.java b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/CreateContainer.java index e3a2eda..c4f87fc 100644 --- a/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/CreateContainer.java +++ b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/CreateContainer.java @@ -9,9 +9,9 @@ * 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. @@ -21,6 +21,10 @@ */ import build.base.configuration.Configuration; +import build.base.json.Json; +import build.base.json.JsonArray; +import build.base.json.JsonObject; +import build.base.json.JsonValue; import build.base.naming.UniqueNameGenerator; import build.base.option.HostName; import build.codemodel.injection.Context; @@ -28,21 +32,22 @@ import build.spawn.docker.Image; import build.spawn.docker.jdk.HttpTransport; import build.spawn.docker.jdk.model.DockerContainer; +import build.spawn.docker.option.Bind; import build.spawn.docker.option.ContainerName; import build.spawn.docker.option.DockerOption; import build.spawn.docker.option.ExposedPort; +import build.spawn.docker.option.ExtraHost; +import build.spawn.docker.option.Link; import build.spawn.docker.option.NetworkName; import build.spawn.docker.option.PublishAllPorts; import build.spawn.docker.option.PublishPort; import build.spawn.option.EnvironmentVariable; -import com.fasterxml.jackson.databind.JsonNode; -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 java.io.IOException; +import java.net.InetSocketAddress; import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.LinkedHashMap; import java.util.Objects; /** @@ -55,12 +60,6 @@ public class CreateContainer extends AbstractBlockingCommand { - /** - * The {@link ObjectMapper} for parsing json. - */ - @Inject - private ObjectMapper objectMapper; - /** * The {@link Image} for which to create the {@link Container}. */ @@ -100,56 +99,118 @@ public CreateContainer(final Image image, @Override protected HttpTransport.Request createRequest() { - // establish an ObjectNode containing the containers/create json - final var node = this.objectMapper.createObjectNode(); - node.put("Image", this.image.id()); + final var nodeBuilder = JsonObject.builder(); + nodeBuilder.put("Image", this.image.id()); + + this.configuration.getOptionalValue(HostName.class) + .ifPresent(h -> nodeBuilder.put("Hostname", h)); + + final var envList = this.configuration.stream(EnvironmentVariable.class) + .map(e -> e.key() + "=" + e.value().orElse("")) + .toList(); + if (!envList.isEmpty()) { + nodeBuilder.put("Env", JsonArray.builder().addAll(envList).build()); + } + + // Accumulate each DockerOption type; the exhaustive switch enforces that new + // permitted subtypes are handled here at compile time. + final var cmdList = new ArrayList(); + final var exposedPortList = new ArrayList(); + final var bindList = new ArrayList(); + final var linkList = new ArrayList(); + final var extraHostList = new ArrayList(); + final var publishPortList = new ArrayList(); + PublishAllPorts publishAllPorts = null; + + for (final DockerOption option : this.configuration.stream(DockerOption.class).toList()) { + var _ = switch (option) { + case build.spawn.docker.option.Command c -> cmdList.addAll(c.values()); + case ExposedPort ep -> exposedPortList.add(ep); + case Bind b -> bindList.add(b); + case Link l -> linkList.add(l); + case ExtraHost h -> extraHostList.add(h); + case PublishPort pp -> publishPortList.add(pp); + case PublishAllPorts p -> { + publishAllPorts = p; + yield true; + } + }; + } + + if (!cmdList.isEmpty()) { + nodeBuilder.put("Cmd", JsonArray.builder().addAll(cmdList).build()); + } - // allow the DockerOptions to configure the ObjectNode - this.configuration.stream(DockerOption.class) - .forEach(option -> option.configure(node, this.objectMapper)); + if (!exposedPortList.isEmpty()) { + final var obj = JsonObject.builder(); + exposedPortList.forEach(ep -> + obj.put(ep.port() + "/" + ep.type().toString().toLowerCase(), JsonObject.builder().build())); + nodeBuilder.put("ExposedPorts", obj.build()); + } + + final var hostConfigBuilder = JsonObject.builder(); + + if (!bindList.isEmpty()) { + hostConfigBuilder.put("Binds", JsonArray.builder().addAll( + bindList.stream().map(b -> b.externalPath() + ":" + b.internalPath()).toList()).build()); + } - // configure docker environment variables node - this.configuration.stream(EnvironmentVariable.class) - .forEach(envVar -> { - final ArrayNode arrayNode = getOrCreateArray(node, "Env"); - arrayNode.add(envVar.key() + "=" + envVar.value().orElse("")); + if (!linkList.isEmpty()) { + hostConfigBuilder.put("Links", JsonArray.builder().addAll( + linkList.stream().map(l -> l.existingNameOrId() + ":" + l.nameToLink()).toList()).build()); + } + + if (!extraHostList.isEmpty()) { + hostConfigBuilder.put("ExtraHosts", JsonArray.builder().addAll( + extraHostList.stream().map(ExtraHost::get).toList()).build()); + } + + if (!publishPortList.isEmpty()) { + final var grouped = new LinkedHashMap>(); + publishPortList.forEach(pb -> { + final String key = pb.port() + "/" + pb.type().toString().toLowerCase(); + grouped.computeIfAbsent(key, k -> new ArrayList<>()).add(pb); }); + final var portBindingsObj = JsonObject.builder(); + grouped.forEach((key, pbs) -> { + final var arr = JsonArray.builder(); + for (final var pb : pbs) { + final var addr = pb.getSocketAddress() + .orElse(new InetSocketAddress("localhost", pb.port())); + final var pbObj = JsonObject.builder() + .put("HostPort", Integer.toString(addr.getPort())); + if (!addr.getHostName().equals("localhost")) { + pbObj.put("HostIp", addr.getHostName()); + } + arr.add(pbObj.build()); + } + portBindingsObj.put(key, arr.build()); + }); + hostConfigBuilder.put("PortBindings", portBindingsObj.build()); + } + + if (publishAllPorts != null) { + hostConfigBuilder.put("PublishAllPorts", publishAllPorts.isEnabled()); + } - // configure network this.configuration.stream(NetworkName.class) .findFirst() - .ifPresent(networkName -> { - ObjectNode hostConfig = (ObjectNode) node.get("HostConfig"); - if (hostConfig == null) { - hostConfig = this.objectMapper.createObjectNode(); - node.set("HostConfig", hostConfig); - } - hostConfig.put("NetworkMode", networkName.get()); - }); + .ifPresent(n -> hostConfigBuilder.put("NetworkMode", n.get())); - // perform custom configuration (for non-DockerOptions) - this.configuration.getOptionalValue(HostName.class) - .ifPresent(hostName -> { - node.put("Hostname", hostName); - }); + final var hostConfig = hostConfigBuilder.build(); + if (!hostConfig.members().isEmpty()) { + nodeBuilder.put("HostConfig", hostConfig); + } final var name = this.configuration.getOptionalValue(ContainerName.class) .orElse(new UniqueNameGenerator(".").next()); return HttpTransport.Request .post("/containers/create?name=" + name, - node.toString().getBytes(StandardCharsets.UTF_8)) + nodeBuilder.build().toJsonString().getBytes(StandardCharsets.UTF_8)) .withContentType("application/json"); } - 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); - } - @Override protected Container createResult(final HttpTransport.Response response) throws IOException { @@ -159,9 +220,8 @@ protected Container createResult(final HttpTransport.Response response) context.bind(Context.class).to(context); context.bind(Configuration.class).to(this.configuration); - // bind the JsonNode representation of the response - final var json = response.bodyString(); - context.bind(JsonNode.class).to(this.objectMapper.readTree(json)); + // bind the JsonValue representation of the response + context.bind(JsonValue.class).to(Json.parse(response.bodyString())); // create the Container to return return context.create(DockerContainer.class); diff --git a/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/CreateExecution.java b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/CreateExecution.java index a928798..75e2e94 100644 --- a/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/CreateExecution.java +++ b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/CreateExecution.java @@ -9,9 +9,9 @@ * 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,14 +22,12 @@ import build.base.configuration.Configuration; import build.base.io.Terminal; +import build.base.json.Json; +import build.base.json.JsonArray; +import build.base.json.JsonObject; 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 java.io.IOException; import java.nio.charset.StandardCharsets; @@ -45,12 +43,6 @@ public class CreateExecution extends AbstractBlockingCommand { - /** - * The {@link ObjectMapper} for parsing json. - */ - @Inject - private ObjectMapper objectMapper; - /** * The {@link Container} in which the {@code exec} will be created. */ @@ -86,19 +78,24 @@ public CreateExecution(final Container container, protected HttpTransport.Request createRequest() { // establish an ObjectNode containing the containers/create json - final ObjectNode node = this.objectMapper.createObjectNode(); - node.put("AttachStdin", false); - node.put("AttachStdout", this.terminalRequired); - node.put("AttachStderr", this.terminalRequired); - node.put("Tty", false); - - // allow the DockerOptions to configure the ObjectNode - this.configuration.stream(DockerOption.class) - .forEach(option -> option.configure(node, this.objectMapper)); + final var nodeBuilder = JsonObject.builder() + .put("AttachStdin", false) + .put("AttachStdout", this.terminalRequired) + .put("AttachStderr", this.terminalRequired) + .put("Tty", false); + + // Cmd — only Command options apply here + this.configuration.stream(build.spawn.docker.option.Command.class) + .findFirst() + .ifPresent(cmd -> { + final var arr = JsonArray.builder(); + cmd.values().forEach(arr::add); + nodeBuilder.put("Cmd", arr.build()); + }); return HttpTransport.Request .post("/containers/" + this.container.id() + "/exec", - node.toString().getBytes(StandardCharsets.UTF_8)) + nodeBuilder.build().toJsonString().getBytes(StandardCharsets.UTF_8)) .withContentType("application/json"); } @@ -106,11 +103,7 @@ protected HttpTransport.Request createRequest() { protected String createResult(final HttpTransport.Response response) throws IOException { - // bind the JsonNode representation of the response - final String json = response.bodyString(); - final JsonNode node = this.objectMapper.readTree(json); - // obtain the Execution identity to return - return node.get("Id").asText(); + return Json.parse(response.bodyString()).getString("Id"); } } diff --git a/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/CreateNetwork.java b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/CreateNetwork.java index ddae7eb..432fd0f 100644 --- a/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/CreateNetwork.java +++ b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/CreateNetwork.java @@ -9,9 +9,9 @@ * 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. @@ -21,10 +21,8 @@ */ import build.base.configuration.Configuration; +import build.base.json.JsonObject; import build.spawn.docker.jdk.HttpTransport; -import build.spawn.docker.option.DockerOption; -import com.fasterxml.jackson.databind.ObjectMapper; -import jakarta.inject.Inject; import java.io.IOException; import java.nio.charset.StandardCharsets; @@ -39,12 +37,6 @@ public class CreateNetwork extends AbstractBlockingCommand> { - /** - * The object mapper. - */ - @Inject - private ObjectMapper objectMapper; - /** * Options to configure the network. */ @@ -70,15 +62,13 @@ public CreateNetwork(final String name, final Configuration configuration) { @Override protected HttpTransport.Request createRequest() { - final var node = this.objectMapper.createObjectNode(); - node.put("Name", this.name); - node.put("CheckDuplicate", true); - - this.configuration.stream(DockerOption.class) - .forEach(option -> option.configure(node, this.objectMapper)); + final var node = JsonObject.builder() + .put("Name", this.name) + .put("CheckDuplicate", true) + .build(); return HttpTransport.Request - .post("/networks/create", node.toString().getBytes(StandardCharsets.UTF_8)) + .post("/networks/create", node.toJsonString().getBytes(StandardCharsets.UTF_8)) .withContentType("application/json"); } diff --git a/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/DeleteNetwork.java b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/DeleteNetwork.java index b99d485..2e1e45d 100644 --- a/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/DeleteNetwork.java +++ b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/DeleteNetwork.java @@ -9,9 +9,9 @@ * 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. @@ -21,8 +21,6 @@ */ import build.spawn.docker.jdk.HttpTransport; -import com.fasterxml.jackson.databind.ObjectMapper; -import jakarta.inject.Inject; import java.io.IOException; @@ -40,12 +38,6 @@ public class DeleteNetwork */ private final String nameOrId; - /** - * The {@link ObjectMapper} for parsing json. - */ - @Inject - private ObjectMapper objectMapper; - /** * Constructor. * diff --git a/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/FileInformation.java b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/FileInformation.java index ab30485..b2a31e9 100644 --- a/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/FileInformation.java +++ b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/FileInformation.java @@ -9,9 +9,9 @@ * 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,15 +20,19 @@ * #L% */ +import build.base.json.Json; +import build.base.json.JsonBoolean; +import build.base.json.JsonNumber; +import build.base.json.JsonString; import build.spawn.docker.Container; import build.spawn.docker.jdk.HttpTransport; -import com.fasterxml.jackson.databind.ObjectMapper; import jakarta.inject.Inject; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.nio.file.Path; import java.util.Base64; +import java.util.LinkedHashMap; import java.util.Map; import java.util.Optional; @@ -68,18 +72,30 @@ protected HttpTransport.Request createRequest() { + "/archive?path=" + this.filePath.toFile().getAbsolutePath()); } - @SuppressWarnings("unchecked") @Override protected Optional> createResult(final HttpTransport.Response response) throws IOException { + if (response.statusCode() != 200) { return Optional.empty(); } + final String base64Encoded = response.header("x-docker-container-path-stat"); final byte[] decoded = Base64.getDecoder().decode(base64Encoded); final String json = new String(decoded, StandardCharsets.UTF_8); - final ObjectMapper mapper = new ObjectMapper(); - final Map map = mapper.readValue(json, Map.class); - return Optional.of(map); + + final var obj = Json.parse(json).asObject(); + final var map = new LinkedHashMap(); + obj.members().forEach((k, v) -> { + final String str = switch (v) { + case JsonString s -> s.value(); + case JsonNumber n -> n.toNumber().toString(); + case JsonBoolean b -> String.valueOf(b.value()); + default -> v.toJsonString(); + }; + map.put(k, str); + }); + + return Optional.of(Map.copyOf(map)); } } diff --git a/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/GetSystemEvents.java b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/GetSystemEvents.java index 942f390..7ef3029 100644 --- a/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/GetSystemEvents.java +++ b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/GetSystemEvents.java @@ -22,12 +22,12 @@ import build.base.flow.Publicist; import build.base.flow.Subscriber; +import build.base.json.JsonFormat; +import build.base.json.JsonValue; import build.base.naming.UniqueNameGenerator; import build.spawn.docker.Event; import build.spawn.docker.jdk.HttpTransport; import build.spawn.docker.jdk.event.ActionEvent; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; import jakarta.inject.Inject; import java.io.IOException; @@ -42,12 +42,6 @@ public class GetSystemEvents extends AbstractNonBlockingCommand { - /** - * The {@link ObjectMapper} for parsing json. - */ - @Inject - private ObjectMapper objectMapper; - /** * The {@link Publicist} for {@code Docker Engine} {@link Event}s. */ @@ -69,17 +63,17 @@ protected Void createResult(final HttpTransport.Response response) { // create a Thread to commence reading the event stream from the response final Runnable runnable = () -> { // establish the Json-based Subscriber for System Events - final var jsonSubscriber = new Subscriber() { + final var jsonSubscriber = new Subscriber() { @Override - public void onNext(final JsonNode item) { + public void onNext(final JsonValue item) { // establish a Context to use for creating Events final var context = createContext(); - context.bind(JsonNode.class).to(item); + context.bind(JsonValue.class).to(item); - System.out.println("Raw Event: [" + name + "] " + item.toPrettyString()); + System.out.println("Raw Event: [" + name + "] " + item.toJsonString(JsonFormat.PRETTY)); // publish "Action" events as ActionEvents - if (item.get("Action") != null) { + if (item.asObject().has("Action")) { final var event = context.create(ActionEvent.class); GetSystemEvents.this.publisher.publish(event); } @@ -97,7 +91,7 @@ public void onComplete() { }; // process the entire InputStream from the Response to essentially wait for the image to be created - final var processor = new JsonNodeInputStreamProcessor(this.objectMapper); + final var processor = new JsonNodeInputStreamProcessor(); try { processor.process(response.bodyStream(), jsonSubscriber); } catch (final IOException e) { diff --git a/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/GetSystemInformation.java b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/GetSystemInformation.java index de1a1fd..0cc3aa1 100644 --- a/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/GetSystemInformation.java +++ b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/GetSystemInformation.java @@ -9,9 +9,9 @@ * 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,11 @@ * #L% */ +import build.base.json.Json; +import build.base.json.JsonValue; import build.spawn.docker.Session; import build.spawn.docker.jdk.HttpTransport; import build.spawn.docker.jdk.model.AbstractJsonBasedResult; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import jakarta.inject.Inject; import java.io.IOException; @@ -39,12 +38,6 @@ public class GetSystemInformation extends AbstractBlockingCommand { - /** - * The {@link ObjectMapper} for parsing json. - */ - @Inject - private ObjectMapper objectMapper; - @Override protected HttpTransport.Request createRequest() { return HttpTransport.Request.get("/info"); @@ -57,11 +50,8 @@ protected Session.Information createResult(final HttpTransport.Response response // establish a new Context to create the Result final var context = createContext(); - // bind the JsonNode representation of the response - final var json = response.bodyString(); - - final var jsonNode = this.objectMapper.readTree(json); - context.bind(JsonNode.class).to(jsonNode); + // bind the JsonValue representation of the response + context.bind(JsonValue.class).to(Json.parse(response.bodyString())); return context.create(Result.class); } @@ -75,7 +65,7 @@ private static class Result @Override public String getServerVersion() { - return jsonNode().get("ServerVersion").asText(); + return text("ServerVersion"); } } } diff --git a/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/InspectContainer.java b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/InspectContainer.java index 0e02c10..c0366fd 100644 --- a/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/InspectContainer.java +++ b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/InspectContainer.java @@ -9,9 +9,9 @@ * 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,11 @@ * #L% */ +import build.base.json.Json; +import build.base.json.JsonValue; import build.codemodel.injection.Context; import build.spawn.docker.Container; import build.spawn.docker.jdk.HttpTransport; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import jakarta.inject.Inject; import java.io.IOException; import java.util.Objects; @@ -41,12 +40,6 @@ public class InspectContainer extends AbstractBlockingCommand> { - /** - * The {@link ObjectMapper} for parsing json. - */ - @Inject - private ObjectMapper objectMapper; - /** * The {@link Container} to inspect. */ @@ -88,9 +81,8 @@ protected Optional createResult(final HttpTransport.Respo context.bind(Container.class).to(this.container); context.bind(Context.class).to(context); - // bind the JsonNode representation of the response - final var json = response.bodyString(); - context.bind(JsonNode.class).to(this.objectMapper.readTree(json)); + // bind the JsonValue representation of the response + context.bind(JsonValue.class).to(Json.parse(response.bodyString())); // establish the Container.Information based on the Response return Optional.of(context.create(build.spawn.docker.jdk.model.ContainerInformation.class)); diff --git a/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/InspectExecution.java b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/InspectExecution.java index e6b86e4..e3c3202 100644 --- a/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/InspectExecution.java +++ b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/InspectExecution.java @@ -9,9 +9,9 @@ * 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,11 @@ * #L% */ +import build.base.json.Json; +import build.base.json.JsonValue; import build.spawn.docker.Execution; import build.spawn.docker.jdk.HttpTransport; import build.spawn.docker.jdk.model.ExecutionInformation; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import jakarta.inject.Inject; import java.io.IOException; import java.util.Objects; @@ -41,12 +40,6 @@ public class InspectExecution extends AbstractBlockingCommand> { - /** - * The {@link ObjectMapper} for parsing json. - */ - @Inject - private ObjectMapper objectMapper; - /** * The id of the {@link Execution} to inspect */ @@ -86,9 +79,8 @@ protected Optional createResult(final HttpTransport.Respo // establish a new Context to create the Result final var context = createContext(); - // bind the JsonNode representation of the response - final var json = response.bodyString(); - context.bind(JsonNode.class).to(this.objectMapper.readTree(json)); + // bind the JsonValue representation of the response + context.bind(JsonValue.class).to(Json.parse(response.bodyString())); return Optional.of(context.create(ExecutionInformation.class)); } diff --git a/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/InspectImage.java b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/InspectImage.java index b695049..c607faa 100644 --- a/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/InspectImage.java +++ b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/InspectImage.java @@ -9,9 +9,9 @@ * 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. @@ -21,13 +21,12 @@ */ import build.base.configuration.Configuration; +import build.base.json.Json; +import build.base.json.JsonValue; import build.spawn.docker.Image; import build.spawn.docker.jdk.HttpTransport; import build.spawn.docker.jdk.model.ImageInformation; import build.spawn.docker.option.ImageName; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import jakarta.inject.Inject; import java.io.IOException; import java.util.Objects; @@ -43,12 +42,6 @@ public class InspectImage extends AbstractBlockingCommand> { - /** - * The {@link ObjectMapper} for parsing json. - */ - @Inject - private ObjectMapper objectMapper; - /** * The name or id of the {@link Image} to inspect. */ @@ -100,6 +93,7 @@ protected void onUnsuccessfulRequest(final HttpTransport.Request request, final @Override protected Optional createResult(final HttpTransport.Response response) throws IOException { + if (response.statusCode() == 404) { return Optional.empty(); } @@ -107,9 +101,8 @@ protected Optional createResult(final HttpTransport.Response // establish a new Context to create the Result final var context = createContext(); - // bind the JsonNode representation of the response - final String json = response.bodyString(); - context.bind(JsonNode.class).to(this.objectMapper.readTree(json)); + // bind the JsonValue representation of the response + context.bind(JsonValue.class).to(Json.parse(response.bodyString())); return Optional.of(context.create(ImageInformation.class)); } diff --git a/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/InspectNetwork.java b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/InspectNetwork.java index cc69815..5558e89 100644 --- a/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/InspectNetwork.java +++ b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/InspectNetwork.java @@ -9,9 +9,9 @@ * 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,11 @@ * #L% */ +import build.base.json.Json; +import build.base.json.JsonValue; import build.spawn.docker.Network; import build.spawn.docker.jdk.HttpTransport; import build.spawn.docker.jdk.model.NetworkInformation; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import jakarta.inject.Inject; import java.io.IOException; import java.util.Optional; @@ -50,12 +49,6 @@ public InspectNetwork(final String nameOrId) { this.nameOrId = nameOrId; } - /** - * The {@link ObjectMapper} for parsing json. - */ - @Inject - private ObjectMapper objectMapper; - @Override protected HttpTransport.Request createRequest() { return HttpTransport.Request.get("/networks/" + this.nameOrId); @@ -64,6 +57,7 @@ protected HttpTransport.Request createRequest() { @Override protected Optional createResult(final HttpTransport.Response response) throws IOException { + if (response.statusCode() == 404) { return Optional.empty(); } @@ -71,9 +65,8 @@ protected Optional createResult(final HttpTransport.Respons // establish a new Context to create the Result final var context = createContext(); - // bind the JsonNode representation of the response - final var json = response.bodyString(); - context.bind(JsonNode.class).to(this.objectMapper.readTree(json)); + // bind the JsonValue representation of the response + context.bind(JsonValue.class).to(Json.parse(response.bodyString())); return Optional.of(context.create(NetworkInformation.class)); } diff --git a/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/JsonNodeInputStreamProcessor.java b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/JsonNodeInputStreamProcessor.java index 34dde1c..33b4981 100644 --- a/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/JsonNodeInputStreamProcessor.java +++ b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/JsonNodeInputStreamProcessor.java @@ -9,9 +9,9 @@ * 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,93 +22,63 @@ import build.base.flow.Subscriber; import build.base.flow.Subscription; -import com.fasterxml.jackson.core.JsonFactory; -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.core.JsonToken; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; +import build.base.json.Json; +import build.base.json.JsonValue; +import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; -import java.util.Objects; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; import java.util.concurrent.atomic.AtomicBoolean; import java.util.logging.Level; import java.util.logging.Logger; /** - * An {@link InputStreamProcessor} for {@link JsonNode}s. + * An {@link InputStreamProcessor} that reads NDJSON (newline-delimited JSON) and emits one + * {@link JsonValue} per line. * * @author brian.oliver * @since Jun-2021 */ public class JsonNodeInputStreamProcessor - implements InputStreamProcessor { + implements InputStreamProcessor { /** * The {@link Logger}. */ private static final Logger LOG = Logger.getLogger(JsonNodeInputStreamProcessor.class.getName()); - /** - * The {@link JsonFactory} to produce {@link JsonParser}s. - */ - private static final JsonFactory JSON_FACTORY = new JsonFactory(); - - /** - * The {@link ObjectMapper}. - */ - private final ObjectMapper objectMapper; - - /** - * Construct a {@link JsonNodeInputStreamProcessor}. - * - * @param objectMapper the {@link ObjectMapper} - */ - public JsonNodeInputStreamProcessor(final ObjectMapper objectMapper) { - this.objectMapper = Objects.requireNonNull(objectMapper, "The ObjectMapper must not be null"); - } - @Override public void process(final InputStream inputStream, - final Subscriber subscriber) { + final Subscriber subscriber) { - Objects.requireNonNull(inputStream, "The InputStream must not be null"); - Objects.requireNonNull(subscriber, "The Observer must not be null"); + final var cancelled = new AtomicBoolean(false); - boolean failed = false; - try { - // establish the JsonParser for the InputStream - final var parser = JSON_FACTORY.createParser(inputStream); + final var subscription = new Subscription() { + @Override + public void request(final long number) {} - // notify the subscriber that is has been subscribed (with a no-op subscription) - final var cancelled = new AtomicBoolean(false); + @Override + public void cancel() { + cancelled.set(true); + } + }; - final var subscription = new Subscription() { - @Override - public void request(final long number) { - // ignore the requests for a number of items - } + subscriber.onSubscribe(subscription); - @Override - public void cancel() { - cancelled.set(true); + boolean failed = false; + try (var reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8))) { + String line; + while (!cancelled.get() && !failed && (line = reader.readLine()) != null) { + final String trimmed = line.trim(); + if (trimmed.isEmpty()) { + continue; } - }; - - subscriber.onSubscribe(subscription); - - JsonToken token; - while (!cancelled.get() - && !failed - && !parser.isClosed() - && (token = parser.nextToken()) != null - && token != JsonToken.END_OBJECT) { - try { - final JsonNode jsonNode = this.objectMapper.readTree(parser); - subscriber.onNext(jsonNode); + subscriber.onNext(Json.parse(trimmed)); } catch (final Throwable throwable) { - LOG.log(Level.FINE, "Failed to read or deliver a JSON node from the stream", throwable); + LOG.log(Level.FINE, "Failed to parse or deliver a JSON line from the stream", throwable); failed = true; subscriber.onError(throwable); } @@ -123,7 +93,6 @@ public void cancel() { if (!failed) { subscriber.onComplete(); } - inputStream.close(); } catch (final IOException e) { LOG.log(Level.FINE, "Failed to close the JSON input stream", e); diff --git a/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/PullImage.java b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/PullImage.java index 26b0b0b..cf159f6 100644 --- a/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/PullImage.java +++ b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/PullImage.java @@ -9,9 +9,9 @@ * 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. @@ -28,7 +28,6 @@ import build.spawn.docker.jdk.HttpTransport; import build.spawn.docker.jdk.event.ActionEvent; import build.spawn.docker.option.ImageName; -import com.fasterxml.jackson.databind.ObjectMapper; import jakarta.inject.Inject; import java.nio.charset.StandardCharsets; @@ -45,12 +44,6 @@ public class PullImage extends AbstractEventBasedBlockingCommand> { - /** - * The {@link ObjectMapper} for parsing json. - */ - @Inject - private ObjectMapper objectMapper; - /** * The {@link Authenticator} for the {@link Request}. */ @@ -107,6 +100,6 @@ protected CompletableFuture subscribe(final Publisher publisher) { return this.eventSubscriber.when(event -> event instanceof ActionEvent actionEvent && actionEvent.action().equals("pull") - && actionEvent.jsonNode().get("id").asText().equals(this.nameOrId)); + && actionEvent.jsonValue().getString("id").equals(this.nameOrId)); } } diff --git a/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/StartExecution.java b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/StartExecution.java index 8dda4f1..13652fa 100644 --- a/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/StartExecution.java +++ b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/command/StartExecution.java @@ -9,9 +9,9 @@ * 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,11 +23,10 @@ import build.base.configuration.Configuration; import build.base.io.NullWriter; import build.base.io.Terminal; +import build.base.json.JsonObject; import build.spawn.docker.Container; import build.spawn.docker.Execution; import build.spawn.docker.jdk.HttpTransport; -import com.fasterxml.jackson.databind.ObjectMapper; -import jakarta.inject.Inject; import java.io.IOException; import java.io.PipedReader; @@ -48,12 +47,6 @@ public class StartExecution extends AbstractNonBlockingCommand { - /** - * The {@link ObjectMapper} for parsing json. - */ - @Inject - private ObjectMapper objectMapper; - /** * The {@link Container} to start. */ @@ -90,7 +83,6 @@ public StartExecution(final Container container, this.container = Objects.requireNonNull(container, "The Container must not be null"); this.id = Objects.requireNonNull(id, "The identity of the Execution must not be null"); this.configuration = Objects.requireNonNull(configuration, "The OptionsByType must not be null"); - this.terminalRequired = terminalRequired; } @@ -98,12 +90,13 @@ public StartExecution(final Container container, protected HttpTransport.Request createRequest() { // establish an ObjectNode containing the containers/create json - final var node = this.objectMapper.createObjectNode(); - node.put("Detach", !this.terminalRequired); - node.put("Tty", false); + final var node = JsonObject.builder() + .put("Detach", !this.terminalRequired) + .put("Tty", false) + .build(); return HttpTransport.Request - .post("/exec/" + this.id + "/start", node.toString().getBytes(StandardCharsets.UTF_8)) + .post("/exec/" + this.id + "/start", node.toJsonString().getBytes(StandardCharsets.UTF_8)) .withContentType("application/json"); } diff --git a/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/event/AbstractEvent.java b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/event/AbstractEvent.java index 1d3c1b2..e25f025 100644 --- a/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/event/AbstractEvent.java +++ b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/event/AbstractEvent.java @@ -9,9 +9,9 @@ * 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,9 +20,9 @@ * #L% */ +import build.base.json.JsonValue; import build.spawn.docker.Event; import build.spawn.docker.jdk.model.AbstractJsonBasedResult; -import com.fasterxml.jackson.databind.JsonNode; import java.util.Optional; @@ -40,8 +40,8 @@ public abstract class AbstractEvent @SuppressWarnings("unchecked") public Optional get(final Class requiredClass) { return requiredClass != null - && requiredClass.isAssignableFrom(JsonNode.class) - ? Optional.of((T) jsonNode()) + && requiredClass.isAssignableFrom(JsonValue.class) + ? Optional.of((T) jsonValue()) : Event.super.get(requiredClass); } } diff --git a/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/event/ActionEvent.java b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/event/ActionEvent.java index fca992f..4b8aa23 100644 --- a/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/event/ActionEvent.java +++ b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/event/ActionEvent.java @@ -20,7 +20,7 @@ * #L% */ -import com.fasterxml.jackson.databind.JsonNode; +import build.base.json.JsonValue; /** * Represents an immutable {@code Docker Engine} Action Event. @@ -37,7 +37,7 @@ public class ActionEvent * @return the type of the event */ public String type() { - return jsonNode().get("Type").asText(); + return jsonValue().getString("Type"); } /** @@ -46,15 +46,15 @@ public String type() { * @return the action */ public String action() { - return jsonNode().get("Action").asText(); + return jsonValue().getString("Action"); } /** - * Obtains the Actor {@link JsonNode}. + * Obtains the Actor {@link JsonValue}. * - * @return the Actor {@link JsonNode} + * @return the Actor {@link JsonValue} */ - public JsonNode actor() { - return jsonNode().get("Actor"); + public JsonValue actor() { + return jsonValue().get("Actor"); } } diff --git a/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/model/AbstractJsonBasedResult.java b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/model/AbstractJsonBasedResult.java index cfe2c9c..27aaaa7 100644 --- a/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/model/AbstractJsonBasedResult.java +++ b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/model/AbstractJsonBasedResult.java @@ -9,9 +9,9 @@ * 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,14 +20,18 @@ * #L% */ +import build.base.json.JsonBoolean; +import build.base.json.JsonNull; +import build.base.json.JsonNumber; +import build.base.json.JsonObject; +import build.base.json.JsonString; +import build.base.json.JsonValue; import build.spawn.docker.Session; import build.spawn.docker.jdk.command.Command; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; import jakarta.inject.Inject; /** - * An abstract {@link JsonNode}-based result from produced by a {@link Command}. + * An abstract {@link JsonValue}-based result produced by a {@link Command}. * * @author brian.oliver * @since Jun-2021 @@ -40,17 +44,8 @@ public abstract class AbstractJsonBasedResult { @Inject private Session session; - /** - * The {@link JsonNode} representation of the {@code Docker Daemon} response. - */ @Inject - private JsonNode jsonNode; - - /** - * The {@link ObjectMapper} that produced the {@link JsonNode}. - */ - @Inject - private ObjectMapper objectMapper; + private JsonValue jsonValue; /** * Obtains the {@link Session} that executed the {@link Command}. @@ -61,21 +56,60 @@ protected Session session() { return this.session; } + public JsonValue jsonValue() { + return this.jsonValue; + } + /** - * Obtains the {@link JsonNode} representing the raw result returned by a {@link Command}. - * - * @return the {@link JsonNode} + * Navigate a path of keys through nested JSON objects. + * Returns {@code null} if any key is absent (rather than throwing). */ - public JsonNode jsonNode() { - return this.jsonNode; + protected JsonValue at(final String... keys) { + JsonValue current = this.jsonValue; + for (final String key : keys) { + if (!(current instanceof JsonObject obj)) { + return null; + } + current = obj.members().get(key); + if (current == null) { + return null; + } + } + return current; + } + + /** Navigate path and return the string value, or {@code ""} if absent or JSON null. */ + protected String text(final String... keys) { + final JsonValue v = at(keys); + return switch (v) { + case JsonString s -> s.value(); + case JsonNumber n -> n.toNumber().toString(); + case JsonNull ignored -> ""; + case null -> ""; + default -> v.toJsonString(); + }; + } + + /** Navigate path and return the int value, or {@code defaultValue} if absent. */ + protected int intAt(final int defaultValue, final String... keys) { + final JsonValue v = at(keys); + return v instanceof JsonNumber n ? n.toNumber().intValue() : defaultValue; + } + + /** Navigate path and return the long value, or {@code defaultValue} if absent. */ + protected long longAt(final long defaultValue, final String... keys) { + final JsonValue v = at(keys); + return v instanceof JsonNumber n ? n.toNumber().longValue() : defaultValue; + } + + /** Navigate path and return the boolean value, or {@code false} if absent. */ + protected boolean boolAt(final String... keys) { + final JsonValue v = at(keys); + return v instanceof JsonBoolean b && b.value(); } @Override public String toString() { - try { - return this.objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(this.jsonNode); - } catch (final Exception e) { - return this.jsonNode.toString(); - } + return this.jsonValue.toJsonString(); } } diff --git a/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/model/ContainerInformation.java b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/model/ContainerInformation.java index 0c698a2..9d2155d 100644 --- a/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/model/ContainerInformation.java +++ b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/model/ContainerInformation.java @@ -9,9 +9,9 @@ * 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,24 +20,22 @@ * #L% */ +import build.base.json.JsonArray; +import build.base.json.JsonString; +import build.base.json.JsonValue; import build.spawn.docker.Container; import build.spawn.docker.Image; import build.spawn.docker.PublishedPort; import build.spawn.docker.option.ExposedPort; import build.spawn.docker.option.Link; -import com.fasterxml.jackson.databind.JsonNode; import jakarta.inject.Inject; import java.net.InetSocketAddress; import java.util.ArrayList; -import java.util.Iterator; import java.util.List; import java.util.Objects; import java.util.Optional; -import java.util.Spliterator; -import java.util.Spliterators; import java.util.stream.Stream; -import java.util.stream.StreamSupport; /** * An internal implementation of {@link Container.Information}. @@ -59,7 +57,7 @@ public String containerId() { @Override public String name() { - return jsonNode().get("Name").asText(); + return text("Name"); } @Override @@ -72,33 +70,31 @@ public Stream exposedPorts() { @Override public Stream publishedPorts() { - return StreamSupport - .stream( - Spliterators.spliteratorUnknownSize( - jsonNode().at("/NetworkSettings/Ports").fields(), - Spliterator.IMMUTABLE), - false) + final var portsNode = at("NetworkSettings", "Ports"); + if (!(portsNode instanceof build.base.json.JsonObject portsObj)) { + return Stream.empty(); + } + return portsObj.members().entrySet().stream() + .filter(entry -> entry.getValue() instanceof JsonArray) .map(entry -> ExposedPort.of(entry.getKey()) .map(exposedPort -> PublishedPort.of(exposedPort, - StreamSupport.stream( - Spliterators.spliteratorUnknownSize(entry.getValue().iterator(), Spliterator.IMMUTABLE) - , false) - .map(address -> new InetSocketAddress(address.get("HostIp").asText(), - address.get("HostPort").asInt())))) + ((JsonArray) entry.getValue()).values().stream() + .map(address -> new InetSocketAddress( + address.getString("HostIp"), + Integer.parseInt(address.getString("HostPort")))))) .orElse(null)) .filter(Objects::nonNull); } @Override public String ipAddress() { - final JsonNode node = jsonNode().at("/NetworkSettings"); - return node.get("IPAddress").asText(); + return text("NetworkSettings", "IPAddress"); } @Override public long pid() { - return jsonNode().at("/State/Pid").asInt(-1); + return longAt(-1L, "State", "Pid"); } @Override @@ -106,7 +102,7 @@ public Optional state() { try { return Optional.of(Enum.valueOf( Container.State.class, - jsonNode().at("/State/Status").asText().toUpperCase())); + text("State", "Status").toUpperCase())); } catch (final Exception e) { return Optional.empty(); @@ -115,12 +111,13 @@ public Optional state() { @Override public Stream links() { - final JsonNode links = jsonNode().at("/HostConfig/Links"); - final Iterator iterator = links.iterator(); + final var linksNode = at("HostConfig", "Links"); + if (!(linksNode instanceof JsonArray linksArray)) { + return Stream.empty(); + } final List linkList = new ArrayList<>(); - while (iterator.hasNext()) { - final JsonNode entry = iterator.next(); - final String text = entry.asText(); + for (final JsonValue entry : linksArray.values()) { + final String text = entry instanceof JsonString s ? s.value() : entry.toJsonString(); final String[] split = text.split(":"); linkList.add(Link.of(split[0], split[1])); } diff --git a/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/model/DockerContainer.java b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/model/DockerContainer.java index ee58f36..f2fbdf1 100644 --- a/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/model/DockerContainer.java +++ b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/model/DockerContainer.java @@ -28,6 +28,8 @@ import build.base.flow.MappingSubscriber; import build.base.flow.Publisher; import build.base.io.Terminal; +import build.base.json.JsonNumber; +import build.base.json.JsonString; import build.codemodel.injection.Context; import build.codemodel.injection.PostInject; import build.spawn.docker.Container; @@ -127,8 +129,8 @@ private void onPostInject() { // (so that it may be injected into anything, like Commands, that use this Context) this.context.bind(Container.class).to(this); - // obtain the identity for the Container from the JsonNode - this.id = jsonNode().get("Id").asText(); + // obtain the identity for the Container from the JsonValue + this.id = jsonValue().getString("Id"); System.out.println("Created Container: " + this.id.substring(this.id.length() - 8)); @@ -139,7 +141,7 @@ private void onPostInject() { FilteringSubscriber.of(ActionEvent.class::isInstance, MappingSubscriber.of(ActionEvent.class::cast, FilteringSubscriber.of( - event -> event.actor().get("ID").asText().equals(this.id), + event -> event.actor().getString("ID").equals(this.id), this.completingSubscriber)))); // establish the CompletableFuture to identify when the Container has started @@ -152,14 +154,17 @@ private void onPostInject() { // establish the CompletableFuture to identify when the Container has terminated (died) this.onExit = this.completingSubscriber.when(event -> { - final var jsonNode = event.jsonNode(); final var action = event.action(); if ("die".equals(action)) { - // extract the exitCode as the exitValue for the Container - final var exitCode = event.actor().get("Attributes").get("exitCode"); - this.exitValue = exitCode.isMissingNode() ? null : exitCode.asInt(); - + final var attributes = event.actor().get("Attributes").asObject().members(); + final var exitCode = attributes.get("exitCode"); + this.exitValue = switch (exitCode) { + case JsonNumber n -> n.toNumber().intValue(); + case JsonString s -> Integer.parseInt(s.value()); + case null -> null; + default -> null; + }; return true; } @@ -167,19 +172,12 @@ private void onPostInject() { }, _ -> { System.out.println("Exited Container: " + this.id.substring(this.id.length() - 8)); - return this; }); this.exitValue = null; } - /** - * Creates a new dependency injection {@link Context}, based on the {@link Context} used to - * create the {@link Container}. - * - * @return a new {@link Context} - */ protected Context createContext() { return this.context.newContext(); } @@ -224,7 +222,6 @@ public Terminal attach(final Configuration configuration) { @Override public Executable createExecutable(final Command command) { - // establish the ConfigurationBuilder for the Execution of our command final var options = ConfigurationBuilder.create() .add(command); @@ -306,7 +303,7 @@ public void kill(final Configuration configuration) { public CompletableFuture pause() { // establish a CompletableFuture to notify when the Container has been paused final CompletableFuture future = this.completingSubscriber - .when(event -> "pause".equals(event.jsonNode().get("Action").asText()), __ -> this); + .when(event -> "pause".equals(event.jsonValue().getString("Action")), __ -> this); createContext() .create(PauseContainer.class) @@ -319,7 +316,7 @@ public CompletableFuture pause() { public CompletableFuture unpause() { // establish a CompletableFuture to notify when the Container has been unpaused final CompletableFuture future = this.completingSubscriber - .when(event -> "unpause".equals(event.jsonNode().get("Action").asText()), __ -> this); + .when(event -> "unpause".equals(event.jsonValue().getString("Action")), __ -> this); createContext() .create(UnpauseContainer.class) diff --git a/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/model/DockerImage.java b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/model/DockerImage.java index aa2b841..3eb1548 100644 --- a/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/model/DockerImage.java +++ b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/model/DockerImage.java @@ -9,9 +9,9 @@ * 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. @@ -21,6 +21,7 @@ */ import build.base.configuration.Configuration; +import build.base.json.JsonString; import build.codemodel.injection.Context; import build.spawn.docker.Container; import build.spawn.docker.Image; @@ -29,15 +30,11 @@ import build.spawn.docker.jdk.command.InspectImage; import build.spawn.docker.jdk.command.StartContainer; import build.spawn.docker.option.ImageName; -import com.fasterxml.jackson.databind.JsonNode; import jakarta.inject.Inject; import java.util.Objects; import java.util.Optional; -import java.util.Spliterator; -import java.util.Spliterators; import java.util.stream.Stream; -import java.util.stream.StreamSupport; /** * An internal implementation of an {@link Image}. @@ -120,14 +117,8 @@ public Stream names() { .submit() .map(ImageInformation.class::cast) .map(info -> - StreamSupport.stream( - Spliterators.spliteratorUnknownSize( - info.jsonNode() - .get("RepoTags") - .iterator(), - Spliterator.ORDERED), - false) - .map(JsonNode::asText) + info.jsonValue().get("RepoTags").asArray().values().stream() + .map(v -> v instanceof JsonString s ? s.value() : v.toJsonString()) .map(ImageName::of)) .orElse(Stream.empty()); } diff --git a/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/model/ExecutionInformation.java b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/model/ExecutionInformation.java index a1cd466..8f2a5f8 100644 --- a/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/model/ExecutionInformation.java +++ b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/model/ExecutionInformation.java @@ -9,9 +9,9 @@ * 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. @@ -37,29 +37,29 @@ public class ExecutionInformation @Override public String getContainerId() { - return jsonNode().get("ContainerId").asText(); + return text("ContainerId"); } @Override public String id() { - return jsonNode().get("ID").asText(); + return text("ID"); } @Override public OptionalLong pid() { - final var pid = jsonNode().get("Pid"); - - return pid == null || pid.isMissingNode() || pid.longValue() == 0 - ? OptionalLong.empty() - : OptionalLong.of(pid.longValue()); + final var pidNode = at("Pid"); + if (pidNode == null) { + return OptionalLong.empty(); + } + final long value = longAt(0L, "Pid"); + return value == 0 ? OptionalLong.empty() : OptionalLong.of(value); } @Override public OptionalInt exitValue() { - final var exitValue = jsonNode().get("ExitCode"); - - return exitValue == null || exitValue.isMissingNode() + final var exitCodeNode = at("ExitCode"); + return exitCodeNode == null ? OptionalInt.empty() - : OptionalInt.of(exitValue.intValue()); + : OptionalInt.of(intAt(0, "ExitCode")); } } diff --git a/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/model/ImageInformation.java b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/model/ImageInformation.java index 676966c..916a91e 100644 --- a/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/model/ImageInformation.java +++ b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/model/ImageInformation.java @@ -9,9 +9,9 @@ * 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. @@ -24,10 +24,7 @@ import build.spawn.docker.option.ExposedPort; import java.util.Optional; -import java.util.Spliterator; -import java.util.Spliterators; import java.util.stream.Stream; -import java.util.stream.StreamSupport; /** * An internal implementation of {@link Image.Information}. @@ -41,17 +38,16 @@ public class ImageInformation @Override public String imageId() { - return jsonNode().get("Id").asText(); + return text("Id"); } @Override public Stream exposedPorts() { - return StreamSupport - .stream( - Spliterators.spliteratorUnknownSize( - jsonNode().at("/Config/ExposedPorts").fieldNames(), - Spliterator.IMMUTABLE), - false) + final var portsNode = at("Config", "ExposedPorts"); + if (portsNode == null) { + return Stream.empty(); + } + return portsNode.asObject().members().keySet().stream() .map(ExposedPort::of) .filter(Optional::isPresent) .map(Optional::get); diff --git a/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/model/NetworkInformation.java b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/model/NetworkInformation.java index 7fab59d..ebc4f6a 100644 --- a/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/model/NetworkInformation.java +++ b/spawn-docker-jdk/src/main/java/build/spawn/docker/jdk/model/NetworkInformation.java @@ -9,9 +9,9 @@ * 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. @@ -34,21 +34,21 @@ public class NetworkInformation @Override public String driver() { - return jsonNode().get("driver").asText(); + return text("driver"); } @Override public String id() { - return jsonNode().get("Id").asText(); + return text("Id"); } @Override public String name() { - return jsonNode().get("Name").asText(); + return text("Name"); } @Override public boolean enableIPv6() { - return jsonNode().get("EnableIPv6").asBoolean(); + return boolAt("EnableIPv6"); } } diff --git a/spawn-docker-jdk/src/main/java/module-info.java b/spawn-docker-jdk/src/main/java/module-info.java index a83309f..a5b48bd 100644 --- a/spawn-docker-jdk/src/main/java/module-info.java +++ b/spawn-docker-jdk/src/main/java/module-info.java @@ -37,7 +37,7 @@ requires jdk.httpserver; requires jakarta.inject; requires build.base.io; - requires com.fasterxml.jackson.databind; + requires build.base.json; requires build.base.archiving; requires build.base.option; requires build.base.flow; diff --git a/spawn-docker-jdk/src/test/java/build/spawn/docker/jdk/SessionTests.java b/spawn-docker-jdk/src/test/java/build/spawn/docker/jdk/SessionTests.java index 46aab41..ff43654 100644 --- a/spawn-docker-jdk/src/test/java/build/spawn/docker/jdk/SessionTests.java +++ b/spawn-docker-jdk/src/test/java/build/spawn/docker/jdk/SessionTests.java @@ -406,7 +406,7 @@ void shouldStartKillAndRemoveRabbitMQ() { .get(RABBITMQ_IMAGE) .orElseThrow(() -> new AssertionError("Failed to get the required image")); - try (var container = image.start()) { + try (var container = image.start(PublishAllPorts.ENABLED)) { Eventually.assertThat(container.onStart()) .isCompleted(); diff --git a/spawn-docker-jdk/src/test/java/build/spawn/docker/jdk/model/ContainerInformationTests.java b/spawn-docker-jdk/src/test/java/build/spawn/docker/jdk/model/ContainerInformationTests.java index f928cc1..56715ab 100644 --- a/spawn-docker-jdk/src/test/java/build/spawn/docker/jdk/model/ContainerInformationTests.java +++ b/spawn-docker-jdk/src/test/java/build/spawn/docker/jdk/model/ContainerInformationTests.java @@ -1,6 +1,8 @@ package build.spawn.docker.jdk.model; -import com.fasterxml.jackson.databind.ObjectMapper; +import build.base.json.Json; +import build.base.json.JsonObject; +import build.base.json.JsonValue; import org.junit.jupiter.api.Test; import java.lang.reflect.Field; @@ -23,18 +25,68 @@ class ContainerInformationTests { */ @Test void nameShouldReturnUnquotedContainerName() throws Exception { - final var objectMapper = new ObjectMapper(); - final var jsonNode = objectMapper.createObjectNode(); - jsonNode.put("Name", "/my-container"); + final JsonValue jsonValue = JsonObject.builder() + .put("Name", "/my-container") + .build(); final var info = new ContainerInformation(); - // inject the JsonNode via reflection — AbstractJsonBasedResult is an open module - final Field field = AbstractJsonBasedResult.class.getDeclaredField("jsonNode"); + final Field field = AbstractJsonBasedResult.class.getDeclaredField("jsonValue"); field.setAccessible(true); - field.set(info, jsonNode); + field.set(info, jsonValue); - // name() must return the plain text, not the JSON-serialized form including quotes assertThat(info.name()).isEqualTo("/my-container"); } + + @Test + void ipAddressShouldReturnEmptyStringWhenJsonNull() throws Exception { + final JsonValue jsonValue = Json.parse("{\"NetworkSettings\":{\"IPAddress\":null}}"); + + final var info = new ContainerInformation(); + + final Field field = AbstractJsonBasedResult.class.getDeclaredField("jsonValue"); + field.setAccessible(true); + field.set(info, jsonValue); + + assertThat(info.ipAddress()).isEmpty(); + } + + @Test + void publishedPortsShouldReturnEmptyStreamWhenPortsIsJsonNull() throws Exception { + final JsonValue jsonValue = Json.parse("{\"NetworkSettings\":{\"Ports\":null}}"); + + final var info = new ContainerInformation(); + + final Field field = AbstractJsonBasedResult.class.getDeclaredField("jsonValue"); + field.setAccessible(true); + field.set(info, jsonValue); + + assertThat(info.publishedPorts()).isEmpty(); + } + + @Test + void linksShouldReturnEmptyStreamWhenLinksIsJsonNull() throws Exception { + final JsonValue jsonValue = Json.parse("{\"HostConfig\":{\"Links\":null}}"); + + final var info = new ContainerInformation(); + + final Field field = AbstractJsonBasedResult.class.getDeclaredField("jsonValue"); + field.setAccessible(true); + field.set(info, jsonValue); + + assertThat(info.links()).isEmpty(); + } + + @Test + void pidShouldReturnNegativeOneWhenAbsent() throws Exception { + final JsonValue jsonValue = JsonObject.builder().build(); + + final var info = new ContainerInformation(); + + final Field field = AbstractJsonBasedResult.class.getDeclaredField("jsonValue"); + field.setAccessible(true); + field.set(info, jsonValue); + + assertThat(info.pid()).isEqualTo(-1L); + } } diff --git a/spawn-docker/pom.xml b/spawn-docker/pom.xml index 355edae..d9648d7 100644 --- a/spawn-docker/pom.xml +++ b/spawn-docker/pom.xml @@ -46,11 +46,6 @@ dependency-injection - - com.fasterxml.jackson.core - jackson-databind - - org.junit.jupiter diff --git a/spawn-docker/src/main/java/build/spawn/docker/option/Bind.java b/spawn-docker/src/main/java/build/spawn/docker/option/Bind.java index ee7d9f8..4254379 100644 --- a/spawn-docker/src/main/java/build/spawn/docker/option/Bind.java +++ b/spawn-docker/src/main/java/build/spawn/docker/option/Bind.java @@ -9,9 +9,9 @@ * 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,9 +22,6 @@ import build.base.configuration.CollectedOption; import build.spawn.docker.Container; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.node.ArrayNode; -import com.fasterxml.jackson.databind.node.ObjectNode; import java.nio.file.Path; import java.util.LinkedHashSet; @@ -36,7 +33,7 @@ * @author brian.oliver * @since Aug-2021 */ -public class Bind +public final class Bind implements DockerOption, CollectedOption { /** @@ -53,37 +50,24 @@ public class Bind * Constructs a {@link Bind}. * * @param externalPath the external {@link Path} - * @param internalPath the external {@link Path} + * @param internalPath the internal {@link Path} */ private Bind(final Path externalPath, final Path internalPath) { Objects.requireNonNull(externalPath, "The external Path must not be null"); - Objects.requireNonNull(externalPath, "The internal Path must not be null"); + Objects.requireNonNull(internalPath, "The internal Path must not be null"); this.externalPath = externalPath; this.internalPath = internalPath; } - @Override - public void configure(final ObjectNode objectNode, final ObjectMapper objectMapper) { - - // ensure the "HostConfig" exists an ObjectNode - final ObjectNode hostConfig = objectNode.get("HostConfig") == null - || !(objectNode.get("HostConfig") instanceof ObjectNode) - ? objectMapper.createObjectNode() - : (ObjectNode) objectNode.get("HostConfig"); - - // ensure the "Binds" exists as an ObjectNode - final ArrayNode binds = hostConfig.get("Binds") == null - || !(hostConfig.get("Binds") instanceof ArrayNode) - ? objectMapper.createArrayNode() - : (ArrayNode) hostConfig.get("Binds"); - - binds.add(this.externalPath.toString() + ":" + this.internalPath.toString()); + public Path externalPath() { + return this.externalPath; + } - hostConfig.set("Binds", binds); - objectNode.set("HostConfig", hostConfig); + public Path internalPath() { + return this.internalPath; } @Override @@ -122,7 +106,7 @@ public static Bind of(final Path path) { * Create a {@link Bind} for a specified {@link Path}. * * @param externalPath the external {@link Path} - * @param internalPath the external {@link Path} + * @param internalPath the internal {@link Path} * @return a {@link Bind} */ public static Bind of(final Path externalPath, final Path internalPath) { diff --git a/spawn-docker/src/main/java/build/spawn/docker/option/Command.java b/spawn-docker/src/main/java/build/spawn/docker/option/Command.java index df38e8f..7b9a5f9 100644 --- a/spawn-docker/src/main/java/build/spawn/docker/option/Command.java +++ b/spawn-docker/src/main/java/build/spawn/docker/option/Command.java @@ -9,9 +9,9 @@ * 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,11 +20,8 @@ * #L% */ -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.node.ObjectNode; - -import java.util.ArrayList; import java.util.Arrays; +import java.util.List; import java.util.Objects; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -35,13 +32,13 @@ * @author brian.oliver * @since Jun-2021 */ -public class Command +public final class Command implements DockerOption { /** * The values for the {@link Command}. */ - private final ArrayList values; + private final List values; /** * Constructs a {@link Command}. @@ -50,18 +47,17 @@ public class Command */ private Command(final Stream values) { this.values = values == null - ? new ArrayList<>() - : values.collect(Collectors.toCollection(ArrayList::new)); + ? List.of() + : values.collect(Collectors.toUnmodifiableList()); } - @Override - public void configure(final ObjectNode objectNode, final ObjectMapper objectMapper) { - - // establish the "Cmd" for the Container - final var arrayNode = objectNode.arrayNode(); - this.values.forEach(arrayNode::add); - - objectNode.set("Cmd", arrayNode); + /** + * Obtains the command values. + * + * @return the command values + */ + public List values() { + return this.values; } /** diff --git a/spawn-docker/src/main/java/build/spawn/docker/option/DockerOption.java b/spawn-docker/src/main/java/build/spawn/docker/option/DockerOption.java index 18e039b..c5bef95 100644 --- a/spawn-docker/src/main/java/build/spawn/docker/option/DockerOption.java +++ b/spawn-docker/src/main/java/build/spawn/docker/option/DockerOption.java @@ -9,9 +9,9 @@ * 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. @@ -21,23 +21,20 @@ */ import build.base.configuration.Option; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.node.ObjectNode; /** - * An {@link Option} to set up a {@code Docker Engine} JSON-based request. + * Sealed marker interface for options that influence a {@code Docker Engine} request body. + *

+ * Serialization of each concrete type is handled entirely by the command that issues the + * request, keeping Jackson (and any future JSON library) out of the public API. + *

+ * The sealed hierarchy ensures that any new subtype must be handled exhaustively wherever + * a pattern-matching switch over {@link DockerOption} is used — enforced at compile time. * * @author brian.oliver * @since Jun-2021 */ -public interface DockerOption - extends Option { - - /** - * Configures the provided {@link ObjectNode} for the {@code Docker Engine} request. - * - * @param objectNode the {@link ObjectNode} - * @param objectMapper the {@link ObjectMapper} - */ - void configure(ObjectNode objectNode, ObjectMapper objectMapper); +public sealed interface DockerOption + extends Option + permits Bind, Command, ExposedPort, ExtraHost, Link, PublishAllPorts, PublishPort { } diff --git a/spawn-docker/src/main/java/build/spawn/docker/option/ExposedPort.java b/spawn-docker/src/main/java/build/spawn/docker/option/ExposedPort.java index 3546fbd..8ff5358 100644 --- a/spawn-docker/src/main/java/build/spawn/docker/option/ExposedPort.java +++ b/spawn-docker/src/main/java/build/spawn/docker/option/ExposedPort.java @@ -22,8 +22,6 @@ import build.base.configuration.CollectedOption; import build.base.foundation.Strings; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.node.ObjectNode; import java.util.LinkedHashSet; import java.util.Objects; @@ -99,21 +97,6 @@ public String toString() { return "ExposedPort{" + this.port + '/' + this.type.toString().toLowerCase() + '}'; } - @Override - public void configure(final ObjectNode objectNode, final ObjectMapper objectMapper) { - - // ensure the "ExposedPorts" exists as an ArrayNode - final ObjectNode exposedPorts = objectNode.get("ExposedPorts") == null - || !(objectNode.get("ExposedPorts") instanceof ObjectNode) - ? objectMapper.createObjectNode() - : (ObjectNode) objectNode.get("ExposedPorts"); - - // add the ExposedPort - exposedPorts.set(this.port + "/" + this.type.toString().toLowerCase(), objectMapper.createObjectNode()); - - objectNode.set("ExposedPorts", exposedPorts); - } - /** * Creates a {@link Type#TCP} exposed port. * diff --git a/spawn-docker/src/main/java/build/spawn/docker/option/ExtraHost.java b/spawn-docker/src/main/java/build/spawn/docker/option/ExtraHost.java index 8c22c3f..3939c04 100644 --- a/spawn-docker/src/main/java/build/spawn/docker/option/ExtraHost.java +++ b/spawn-docker/src/main/java/build/spawn/docker/option/ExtraHost.java @@ -22,9 +22,6 @@ import build.base.configuration.AbstractValueOption; import build.base.configuration.CollectedOption; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.node.ArrayNode; -import com.fasterxml.jackson.databind.node.ObjectNode; import java.util.LinkedHashSet; @@ -35,7 +32,7 @@ * @author brian.oliver * @since Aug-2021 */ -public class ExtraHost +public final class ExtraHost extends AbstractValueOption implements DockerOption, CollectedOption { @@ -58,24 +55,4 @@ public static ExtraHost of(final String host) { return new ExtraHost(host); } - @Override - public void configure(final ObjectNode objectNode, final ObjectMapper objectMapper) { - - // ensure the "/HostConfig" exists - final ObjectNode hostConfig = objectNode.get("HostConfig") == null - ? objectMapper.createObjectNode() - : (ObjectNode) objectNode.get("HostConfig"); - - objectNode.set("HostConfig", hostConfig); - - // ensure "/HostConfig/ExtraHosts" exists - final ArrayNode extraHosts = hostConfig.get("ExtraHosts") == null - ? objectMapper.createArrayNode() - : (ArrayNode) hostConfig.get("ExtraHosts"); - - hostConfig.set("ExtraHosts", extraHosts); - - // add this ExtraHost - extraHosts.add(get()); - } } diff --git a/spawn-docker/src/main/java/build/spawn/docker/option/Link.java b/spawn-docker/src/main/java/build/spawn/docker/option/Link.java index e0c7919..ec087a5 100644 --- a/spawn-docker/src/main/java/build/spawn/docker/option/Link.java +++ b/spawn-docker/src/main/java/build/spawn/docker/option/Link.java @@ -21,9 +21,6 @@ */ import build.base.configuration.CollectedOption; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.node.ArrayNode; -import com.fasterxml.jackson.databind.node.ObjectNode; import java.util.LinkedHashSet; import java.util.Objects; @@ -34,7 +31,7 @@ * @author anand.sankaran * @since Aug-2022 */ -public class Link +public final class Link implements DockerOption, CollectedOption { /** @@ -63,27 +60,6 @@ private Link(final String existingNameOrId, this.nameToLink = nameToLink; } - @Override - public void configure(final ObjectNode objectNode, final ObjectMapper objectMapper) { - - // ensure the "HostConfig" exists an ObjectNode - final ObjectNode hostConfig = objectNode.get("HostConfig") == null - || !(objectNode.get("HostConfig") instanceof ObjectNode) - ? objectMapper.createObjectNode() - : (ObjectNode) objectNode.get("HostConfig"); - - // ensure the "Links" exists as an ObjectNode - final ArrayNode links = hostConfig.get("Links") == null - || !(hostConfig.get("Links") instanceof ArrayNode) - ? objectMapper.createArrayNode() - : (ArrayNode) hostConfig.get("Links"); - - links.add(this.existingNameOrId + ":" + this.nameToLink); - - hostConfig.set("Links", links); - objectNode.set("HostConfig", hostConfig); - } - @Override public boolean equals(final Object object) { if (this == object) { diff --git a/spawn-docker/src/main/java/build/spawn/docker/option/PublishAllPorts.java b/spawn-docker/src/main/java/build/spawn/docker/option/PublishAllPorts.java index de7ef13..c46f690 100644 --- a/spawn-docker/src/main/java/build/spawn/docker/option/PublishAllPorts.java +++ b/spawn-docker/src/main/java/build/spawn/docker/option/PublishAllPorts.java @@ -9,9 +9,9 @@ * 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. @@ -21,8 +21,6 @@ */ import build.base.configuration.Default; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.node.ObjectNode; /** * A {@link DockerOption} to publish {@link ExposedPort}s for a {@link build.spawn.docker.Container}. @@ -44,17 +42,7 @@ public enum PublishAllPorts */ DISABLED; - @Override - public void configure(final ObjectNode objectNode, final ObjectMapper objectMapper) { - - // ensure the "HostConfig" exists an ObjectNode - final ObjectNode hostConfig = objectNode.get("HostConfig") == null - || !(objectNode.get("HostConfig") instanceof ObjectNode) - ? objectMapper.createObjectNode() - : (ObjectNode) objectNode.get("HostConfig"); - - hostConfig.put("PublishAllPorts", this == ENABLED); - - objectNode.set("HostConfig", hostConfig); + public boolean isEnabled() { + return this == ENABLED; } } diff --git a/spawn-docker/src/main/java/build/spawn/docker/option/PublishPort.java b/spawn-docker/src/main/java/build/spawn/docker/option/PublishPort.java index 01566cd..5071c02 100644 --- a/spawn-docker/src/main/java/build/spawn/docker/option/PublishPort.java +++ b/spawn-docker/src/main/java/build/spawn/docker/option/PublishPort.java @@ -21,9 +21,6 @@ */ import build.base.configuration.CollectedOption; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.node.ArrayNode; -import com.fasterxml.jackson.databind.node.ObjectNode; import java.net.InetSocketAddress; import java.util.LinkedHashSet; @@ -126,47 +123,6 @@ public String toString() { + '}'; } - @Override - public void configure(final ObjectNode objectNode, final ObjectMapper objectMapper) { - - // ensure the "HostConfig" exists an ObjectNode - final ObjectNode hostConfig = objectNode.get("HostConfig") == null - || !(objectNode.get("HostConfig") instanceof ObjectNode) - ? objectMapper.createObjectNode() - : (ObjectNode) objectNode.get("HostConfig"); - - // ensure the "PortBindings" exists as an ObjectNode - final ObjectNode portBindings = hostConfig.get("PortBindings") == null - || !(hostConfig.get("PortBindings") instanceof ObjectNode) - ? objectMapper.createObjectNode() - : (ObjectNode) hostConfig.get("PortBindings"); - - final String key = this.port + "/" + this.type.toString().toLowerCase(); - - // ensure the PortBinding exists as an ArrayNode - final ArrayNode array = portBindings.get(key) == null - || !(portBindings.get(key) instanceof ArrayNode) - ? objectMapper.createArrayNode() - : (ArrayNode) portBindings.get(key); - - // create the PortBinding - final ObjectNode portBinding = objectMapper.createObjectNode(); - final InetSocketAddress address = this.socketAddress - .orElse(new InetSocketAddress("localhost", this.port)); - - if (!address.getHostName().equals("localhost")) { - portBinding.put("HostIp", address.getHostName()); - } - - portBinding.put("HostPort", Integer.toString(address.getPort())); - - array.add(portBinding); - - portBindings.set(key, array); - hostConfig.set("PortBindings", portBindings); - objectNode.set("HostConfig", hostConfig); - } - /** * Creates a {@link Type#TCP} exposed port. * diff --git a/spawn-docker/src/main/java/module-info.java b/spawn-docker/src/main/java/module-info.java index 28b6c01..45b2952 100644 --- a/spawn-docker/src/main/java/module-info.java +++ b/spawn-docker/src/main/java/module-info.java @@ -32,8 +32,6 @@ requires transitive build.codemodel.injection; - requires com.fasterxml.jackson.databind; - exports build.spawn.docker; exports build.spawn.docker.option; diff --git a/spawn-docker/src/test/java/build/spawn/docker/option/PublishPortTests.java b/spawn-docker/src/test/java/build/spawn/docker/option/PublishPortTests.java index 6edd72b..650ef8f 100644 --- a/spawn-docker/src/test/java/build/spawn/docker/option/PublishPortTests.java +++ b/spawn-docker/src/test/java/build/spawn/docker/option/PublishPortTests.java @@ -1,8 +1,5 @@ package build.spawn.docker.option; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.node.ArrayNode; -import com.fasterxml.jackson.databind.node.ObjectNode; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; @@ -16,21 +13,17 @@ class PublishPortTests { /** - * Ensure that configuring multiple external bindings for the same internal port - * accumulates all bindings rather than silently discarding prior ones. + * Ensure that two {@link PublishPort} instances with the same internal port but different external ports + * are not equal, preserving the distinct bindings when collected. */ @Test - void shouldAccumulateMultipleBindingsForSamePort() { - final var objectMapper = new ObjectMapper(); - final var container = objectMapper.createObjectNode(); + void distinctExternalPortsProduceDifferentInstances() { + final var first = PublishPort.of(8080, PublishPort.Type.TCP, 8080); + final var second = PublishPort.of(8080, PublishPort.Type.TCP, 9090); - // configure two distinct external ports mapped to the same internal port - PublishPort.of(8080, PublishPort.Type.TCP, 8080).configure(container, objectMapper); - PublishPort.of(8080, PublishPort.Type.TCP, 9090).configure(container, objectMapper); - - final var portBindings = (ObjectNode) container.path("HostConfig").path("PortBindings"); - final var array = (ArrayNode) portBindings.get("8080/tcp"); - - assertThat(array).hasSize(2); + assertThat(first).isNotEqualTo(second); + assertThat(first.port()).isEqualTo(8080); + assertThat(second.port()).isEqualTo(8080); + assertThat(first.type()).isEqualTo(PublishPort.Type.TCP); } }