Add nullability annotations to core/spring-boot-docker-compose

See gh-46587
This commit is contained in:
Moritz Halbritter 2025-07-22 13:36:51 +02:00
parent b6e4533296
commit cb2a26ceec
27 changed files with 150 additions and 85 deletions

View File

@ -18,6 +18,8 @@ package org.springframework.boot.docker.compose.core;
import java.util.List; import java.util.List;
import org.jspecify.annotations.Nullable;
/** /**
* Provides access to the ports that can be used to connect to a {@link RunningService}. * Provides access to the ports that can be used to connect to a {@link RunningService}.
* *
@ -52,6 +54,6 @@ public interface ConnectionPorts {
* all host ports * all host ports
* @return a list of all host ports using the given protocol * @return a list of all host ports using the given protocol
*/ */
List<Integer> getAll(String protocol); List<Integer> getAll(@Nullable String protocol);
} }

View File

@ -22,6 +22,8 @@ import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import org.jspecify.annotations.Nullable;
import org.springframework.boot.docker.compose.core.DockerCliInspectResponse.Config; import org.springframework.boot.docker.compose.core.DockerCliInspectResponse.Config;
import org.springframework.boot.docker.compose.core.DockerCliInspectResponse.HostConfig; import org.springframework.boot.docker.compose.core.DockerCliInspectResponse.HostConfig;
import org.springframework.boot.docker.compose.core.DockerCliInspectResponse.HostPort; import org.springframework.boot.docker.compose.core.DockerCliInspectResponse.HostPort;
@ -57,7 +59,7 @@ class DefaultConnectionPorts implements ConnectionPorts {
return (config != null) && "host".equals(config.networkMode()); return (config != null) && "host".equals(config.networkMode());
} }
private Map<ContainerPort, Integer> buildMappingsForNetworkSettings(NetworkSettings networkSettings) { private Map<ContainerPort, Integer> buildMappingsForNetworkSettings(@Nullable NetworkSettings networkSettings) {
if (networkSettings == null || CollectionUtils.isEmpty(networkSettings.ports())) { if (networkSettings == null || CollectionUtils.isEmpty(networkSettings.ports())) {
return Collections.emptyMap(); return Collections.emptyMap();
} }
@ -73,7 +75,7 @@ class DefaultConnectionPorts implements ConnectionPorts {
return Collections.unmodifiableMap(mappings); return Collections.unmodifiableMap(mappings);
} }
private boolean isIpV4(HostPort hostPort) { private boolean isIpV4(@Nullable HostPort hostPort) {
String ip = (hostPort != null) ? hostPort.hostIp() : null; String ip = (hostPort != null) ? hostPort.hostIp() : null;
return !StringUtils.hasLength(ip) || ip.contains("."); return !StringUtils.hasLength(ip) || ip.contains(".");
} }
@ -108,7 +110,7 @@ class DefaultConnectionPorts implements ConnectionPorts {
} }
@Override @Override
public List<Integer> getAll(String protocol) { public List<Integer> getAll(@Nullable String protocol) {
List<Integer> hostPorts = new ArrayList<>(); List<Integer> hostPorts = new ArrayList<>();
this.mappings.forEach((containerPort, hostPort) -> { this.mappings.forEach((containerPort, hostPort) -> {
if (protocol == null || protocol.equalsIgnoreCase(containerPort.protocol())) { if (protocol == null || protocol.equalsIgnoreCase(containerPort.protocol())) {

View File

@ -25,6 +25,8 @@ import java.util.Map.Entry;
import java.util.function.Function; import java.util.function.Function;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import org.jspecify.annotations.Nullable;
import org.springframework.boot.logging.LogLevel; import org.springframework.boot.logging.LogLevel;
import org.springframework.util.Assert; import org.springframework.util.Assert;
@ -41,7 +43,7 @@ class DefaultDockerCompose implements DockerCompose {
private final DockerHost hostname; private final DockerHost hostname;
DefaultDockerCompose(DockerCli cli, String host) { DefaultDockerCompose(DockerCli cli, @Nullable String host) {
this.cli = cli; this.cli = cli;
this.hostname = DockerHost.get(host, () -> cli.run(new DockerCliCommand.Context())); this.hostname = DockerHost.get(host, () -> cli.run(new DockerCliCommand.Context()));
} }
@ -114,7 +116,8 @@ class DefaultDockerCompose implements DockerCompose {
return inspectResponses.stream().collect(Collectors.toMap(DockerCliInspectResponse::id, Function.identity())); return inspectResponses.stream().collect(Collectors.toMap(DockerCliInspectResponse::id, Function.identity()));
} }
private DockerCliInspectResponse inspectContainer(String id, Map<String, DockerCliInspectResponse> inspected) { private @Nullable DockerCliInspectResponse inspectContainer(String id,
Map<String, DockerCliInspectResponse> inspected) {
DockerCliInspectResponse inspect = inspected.get(id); DockerCliInspectResponse inspect = inspected.get(id);
if (inspect != null) { if (inspect != null) {
return inspect; return inspect;

View File

@ -19,6 +19,8 @@ package org.springframework.boot.docker.compose.core;
import java.util.Collections; import java.util.Collections;
import java.util.Map; import java.util.Map;
import org.jspecify.annotations.Nullable;
import org.springframework.boot.origin.Origin; import org.springframework.boot.origin.Origin;
import org.springframework.boot.origin.OriginProvider; import org.springframework.boot.origin.OriginProvider;
@ -45,10 +47,10 @@ class DefaultRunningService implements RunningService, OriginProvider {
private final DockerEnv env; private final DockerEnv env;
private final DockerComposeFile composeFile; private final @Nullable DockerComposeFile composeFile;
DefaultRunningService(DockerHost host, DockerComposeFile composeFile, DockerCliComposePsResponse composePsResponse, DefaultRunningService(DockerHost host, @Nullable DockerComposeFile composeFile,
DockerCliInspectResponse inspectResponse) { DockerCliComposePsResponse composePsResponse, DockerCliInspectResponse inspectResponse) {
this.origin = new DockerComposeOrigin(composeFile, composePsResponse.name()); this.origin = new DockerComposeOrigin(composeFile, composePsResponse.name());
this.name = composePsResponse.name(); this.name = composePsResponse.name();
this.image = ImageReference this.image = ImageReference
@ -101,7 +103,7 @@ class DefaultRunningService implements RunningService, OriginProvider {
} }
@Override @Override
public DockerComposeFile composeFile() { public @Nullable DockerComposeFile composeFile() {
return this.composeFile; return this.composeFile;
} }

View File

@ -27,6 +27,7 @@ import java.util.function.Consumer;
import org.apache.commons.logging.Log; import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory; import org.apache.commons.logging.LogFactory;
import org.jspecify.annotations.Nullable;
import org.springframework.boot.docker.compose.core.DockerCliCommand.ComposeVersion; import org.springframework.boot.docker.compose.core.DockerCliCommand.ComposeVersion;
import org.springframework.boot.docker.compose.core.DockerCliCommand.Type; import org.springframework.boot.docker.compose.core.DockerCliCommand.Type;
@ -60,7 +61,7 @@ class DockerCli {
* @param workingDirectory the working directory or {@code null} * @param workingDirectory the working directory or {@code null}
* @param dockerComposeOptions the Docker Compose options to use or {@code null}. * @param dockerComposeOptions the Docker Compose options to use or {@code null}.
*/ */
DockerCli(File workingDirectory, DockerComposeOptions dockerComposeOptions) { DockerCli(@Nullable File workingDirectory, @Nullable DockerComposeOptions dockerComposeOptions) {
this.processRunner = new ProcessRunner(workingDirectory); this.processRunner = new ProcessRunner(workingDirectory);
this.dockerCommands = dockerCommandsCache.computeIfAbsent(workingDirectory, this.dockerCommands = dockerCommandsCache.computeIfAbsent(workingDirectory,
(key) -> new DockerCommands(this.processRunner)); (key) -> new DockerCommands(this.processRunner));
@ -82,7 +83,7 @@ class DockerCli {
return dockerCommand.deserialize(json); return dockerCommand.deserialize(json);
} }
private Consumer<String> createOutputConsumer(LogLevel logLevel) { private @Nullable Consumer<String> createOutputConsumer(@Nullable LogLevel logLevel) {
if (logLevel == null || logLevel == LogLevel.OFF) { if (logLevel == null || logLevel == LogLevel.OFF) {
return null; return null;
} }
@ -123,7 +124,7 @@ class DockerCli {
* Return the {@link DockerComposeFile} being used by this CLI instance. * Return the {@link DockerComposeFile} being used by this CLI instance.
* @return the Docker Compose file * @return the Docker Compose file
*/ */
DockerComposeFile getDockerComposeFile() { @Nullable DockerComposeFile getDockerComposeFile() {
return this.dockerComposeOptions.composeFile(); return this.dockerComposeOptions.composeFile();
} }
@ -205,11 +206,14 @@ class DockerCli {
* @param activeProfiles the profiles to activate * @param activeProfiles the profiles to activate
* @param arguments the arguments to pass to Docker Compose * @param arguments the arguments to pass to Docker Compose
*/ */
record DockerComposeOptions(DockerComposeFile composeFile, Set<String> activeProfiles, List<String> arguments) { record DockerComposeOptions(@Nullable DockerComposeFile composeFile, Set<String> activeProfiles,
List<String> arguments) {
DockerComposeOptions { DockerComposeOptions(@Nullable DockerComposeFile composeFile, @Nullable Set<String> activeProfiles,
activeProfiles = (activeProfiles != null) ? activeProfiles : Collections.emptySet(); @Nullable List<String> arguments) {
arguments = (arguments != null) ? arguments : Collections.emptyList(); this.composeFile = composeFile;
this.activeProfiles = (activeProfiles != null) ? activeProfiles : Collections.emptySet();
this.arguments = (arguments != null) ? arguments : Collections.emptyList();
} }
static DockerComposeOptions none() { static DockerComposeOptions none() {

View File

@ -78,8 +78,8 @@ abstract sealed class DockerCliCommand<R> {
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
R deserialize(String json) { R deserialize(String json) {
if (this.responseType == Void.class) { if (this.responseType == None.class) {
return null; return (R) None.INSTANCE;
} }
return (R) ((!this.listResponse) ? DockerJson.deserialize(json, this.responseType) return (R) ((!this.listResponse) ? DockerJson.deserialize(json, this.responseType)
: DockerJson.deserializeToList(json, this.responseType)); : DockerJson.deserializeToList(json, this.responseType));
@ -175,10 +175,10 @@ abstract sealed class DockerCliCommand<R> {
/** /**
* The {@code docker compose up} command. * The {@code docker compose up} command.
*/ */
static final class ComposeUp extends DockerCliCommand<Void> { static final class ComposeUp extends DockerCliCommand<None> {
ComposeUp(LogLevel logLevel, List<String> arguments) { ComposeUp(LogLevel logLevel, List<String> arguments) {
super(Type.DOCKER_COMPOSE, logLevel, Void.class, false, getCommand(arguments)); super(Type.DOCKER_COMPOSE, logLevel, None.class, false, getCommand(arguments));
} }
private static String[] getCommand(List<String> arguments) { private static String[] getCommand(List<String> arguments) {
@ -196,10 +196,10 @@ abstract sealed class DockerCliCommand<R> {
/** /**
* The {@code docker compose down} command. * The {@code docker compose down} command.
*/ */
static final class ComposeDown extends DockerCliCommand<Void> { static final class ComposeDown extends DockerCliCommand<None> {
ComposeDown(Duration timeout, List<String> arguments) { ComposeDown(Duration timeout, List<String> arguments) {
super(Type.DOCKER_COMPOSE, Void.class, false, getCommand(timeout, arguments)); super(Type.DOCKER_COMPOSE, None.class, false, getCommand(timeout, arguments));
} }
private static String[] getCommand(Duration timeout, List<String> arguments) { private static String[] getCommand(Duration timeout, List<String> arguments) {
@ -216,10 +216,10 @@ abstract sealed class DockerCliCommand<R> {
/** /**
* The {@code docker compose start} command. * The {@code docker compose start} command.
*/ */
static final class ComposeStart extends DockerCliCommand<Void> { static final class ComposeStart extends DockerCliCommand<None> {
ComposeStart(LogLevel logLevel, List<String> arguments) { ComposeStart(LogLevel logLevel, List<String> arguments) {
super(Type.DOCKER_COMPOSE, logLevel, Void.class, false, getCommand(arguments)); super(Type.DOCKER_COMPOSE, logLevel, None.class, false, getCommand(arguments));
} }
private static String[] getCommand(List<String> arguments) { private static String[] getCommand(List<String> arguments) {
@ -234,10 +234,10 @@ abstract sealed class DockerCliCommand<R> {
/** /**
* The {@code docker compose stop} command. * The {@code docker compose stop} command.
*/ */
static final class ComposeStop extends DockerCliCommand<Void> { static final class ComposeStop extends DockerCliCommand<None> {
ComposeStop(Duration timeout, List<String> arguments) { ComposeStop(Duration timeout, List<String> arguments) {
super(Type.DOCKER_COMPOSE, Void.class, false, getCommand(timeout, arguments)); super(Type.DOCKER_COMPOSE, None.class, false, getCommand(timeout, arguments));
} }
private static String[] getCommand(Duration timeout, List<String> arguments) { private static String[] getCommand(Duration timeout, List<String> arguments) {
@ -268,6 +268,15 @@ abstract sealed class DockerCliCommand<R> {
} }
static final class None {
public static final None INSTANCE = new None();
private None() {
}
}
/** /**
* Docker compose version. * Docker compose version.
* *

View File

@ -16,6 +16,8 @@
package org.springframework.boot.docker.compose.core; package org.springframework.boot.docker.compose.core;
import org.jspecify.annotations.Nullable;
/** /**
* Response from {@link DockerCliCommand.ComposePs docker compose ps}. * Response from {@link DockerCliCommand.ComposePs docker compose ps}.
* *
@ -27,6 +29,6 @@ package org.springframework.boot.docker.compose.core;
* @author Andy Wilkinson * @author Andy Wilkinson
* @author Phillip Webb * @author Phillip Webb
*/ */
record DockerCliComposePsResponse(String id, String name, String image, String state) { record DockerCliComposePsResponse(String id, String name, @Nullable String image, String state) {
} }

View File

@ -19,6 +19,8 @@ package org.springframework.boot.docker.compose.core;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import org.jspecify.annotations.Nullable;
/** /**
* Response from {@link DockerCliCommand.Inspect docker inspect}. * Response from {@link DockerCliCommand.Inspect docker inspect}.
* *
@ -31,7 +33,8 @@ import java.util.Map;
* @author Phillip Webb * @author Phillip Webb
*/ */
record DockerCliInspectResponse(String id, DockerCliInspectResponse.Config config, record DockerCliInspectResponse(String id, DockerCliInspectResponse.Config config,
DockerCliInspectResponse.NetworkSettings networkSettings, DockerCliInspectResponse.HostConfig hostConfig) { DockerCliInspectResponse.NetworkSettings networkSettings,
DockerCliInspectResponse.@Nullable HostConfig hostConfig) {
/** /**
* Configuration for the container that is portable between hosts. * Configuration for the container that is portable between hosts.

View File

@ -21,6 +21,8 @@ import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
import org.jspecify.annotations.Nullable;
import org.springframework.boot.docker.compose.core.DockerCli.DockerComposeOptions; import org.springframework.boot.docker.compose.core.DockerCli.DockerComposeOptions;
import org.springframework.boot.logging.LogLevel; import org.springframework.boot.logging.LogLevel;
@ -126,7 +128,7 @@ public interface DockerCompose {
* @param activeProfiles a set of the profiles that should be activated * @param activeProfiles a set of the profiles that should be activated
* @return a {@link DockerCompose} instance * @return a {@link DockerCompose} instance
*/ */
static DockerCompose get(DockerComposeFile file, String hostname, Set<String> activeProfiles) { static DockerCompose get(DockerComposeFile file, @Nullable String hostname, Set<String> activeProfiles) {
return get(file, hostname, activeProfiles, Collections.emptyList()); return get(file, hostname, activeProfiles, Collections.emptyList());
} }
@ -140,7 +142,7 @@ public interface DockerCompose {
* @return a {@link DockerCompose} instance * @return a {@link DockerCompose} instance
* @since 3.4.0 * @since 3.4.0
*/ */
static DockerCompose get(DockerComposeFile file, String hostname, Set<String> activeProfiles, static DockerCompose get(DockerComposeFile file, @Nullable String hostname, Set<String> activeProfiles,
List<String> arguments) { List<String> arguments) {
DockerCli cli = new DockerCli(null, new DockerComposeOptions(file, activeProfiles, arguments)); DockerCli cli = new DockerCli(null, new DockerComposeOptions(file, activeProfiles, arguments));
return new DefaultDockerCompose(cli, hostname); return new DefaultDockerCompose(cli, hostname);

View File

@ -26,6 +26,8 @@ import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import org.jspecify.annotations.Nullable;
import org.springframework.util.Assert; import org.springframework.util.Assert;
/** /**
@ -107,7 +109,7 @@ public final class DockerComposeFile {
* current directory * current directory
* @return the located file or {@code null} if no Docker Compose file can be found * @return the located file or {@code null} if no Docker Compose file can be found
*/ */
public static DockerComposeFile find(File workingDirectory) { public static @Nullable DockerComposeFile find(@Nullable File workingDirectory) {
File base = (workingDirectory != null) ? workingDirectory : new File("."); File base = (workingDirectory != null) ? workingDirectory : new File(".");
if (!base.exists()) { if (!base.exists()) {
return null; return null;

View File

@ -16,6 +16,8 @@
package org.springframework.boot.docker.compose.core; package org.springframework.boot.docker.compose.core;
import org.jspecify.annotations.Nullable;
import org.springframework.boot.origin.Origin; import org.springframework.boot.origin.Origin;
/** /**
@ -27,7 +29,7 @@ import org.springframework.boot.origin.Origin;
* @author Andy Wilkinson * @author Andy Wilkinson
* @since 3.1.0 * @since 3.1.0
*/ */
public record DockerComposeOrigin(DockerComposeFile composeFile, String serviceName) implements Origin { public record DockerComposeOrigin(@Nullable DockerComposeFile composeFile, String serviceName) implements Origin {
@Override @Override
public String toString() { public String toString() {

View File

@ -21,6 +21,8 @@ import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import org.jspecify.annotations.Nullable;
import org.springframework.util.CollectionUtils; import org.springframework.util.CollectionUtils;
/** /**
@ -69,7 +71,7 @@ class DockerEnv {
return this.map; return this.map;
} }
private record Entry(String key, String value) { private record Entry(String key, @Nullable String value) {
} }

View File

@ -21,6 +21,8 @@ import java.util.List;
import java.util.function.Function; import java.util.function.Function;
import java.util.function.Supplier; import java.util.function.Supplier;
import org.jspecify.annotations.Nullable;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
/** /**
@ -69,7 +71,7 @@ final class DockerHost {
* {@link DockerCliContextResponse} * {@link DockerCliContextResponse}
* @return a new docker host instance * @return a new docker host instance
*/ */
static DockerHost get(String host, Supplier<List<DockerCliContextResponse>> contextsSupplier) { static DockerHost get(@Nullable String host, Supplier<List<DockerCliContextResponse>> contextsSupplier) {
return get(host, System::getenv, contextsSupplier); return get(host, System::getenv, contextsSupplier);
} }
@ -81,7 +83,7 @@ final class DockerHost {
* {@link DockerCliContextResponse} * {@link DockerCliContextResponse}
* @return a new docker host instance * @return a new docker host instance
*/ */
static DockerHost get(String host, Function<String, String> systemEnv, static DockerHost get(@Nullable String host, Function<String, String> systemEnv,
Supplier<List<DockerCliContextResponse>> contextsSupplier) { Supplier<List<DockerCliContextResponse>> contextsSupplier) {
host = (StringUtils.hasText(host)) ? host : fromServicesHostEnv(systemEnv); host = (StringUtils.hasText(host)) ? host : fromServicesHostEnv(systemEnv);
host = (StringUtils.hasText(host)) ? host : fromDockerHostEnv(systemEnv); host = (StringUtils.hasText(host)) ? host : fromDockerHostEnv(systemEnv);
@ -94,24 +96,24 @@ final class DockerHost {
return systemEnv.apply("SERVICES_HOST"); return systemEnv.apply("SERVICES_HOST");
} }
private static String fromDockerHostEnv(Function<String, String> systemEnv) { private static @Nullable String fromDockerHostEnv(Function<String, String> systemEnv) {
return fromEndpoint(systemEnv.apply("DOCKER_HOST")); return fromEndpoint(systemEnv.apply("DOCKER_HOST"));
} }
private static String fromCurrentContext(Supplier<List<DockerCliContextResponse>> contextsSupplier) { private static @Nullable String fromCurrentContext(Supplier<List<DockerCliContextResponse>> contextsSupplier) {
DockerCliContextResponse current = getCurrentContext(contextsSupplier.get()); DockerCliContextResponse current = getCurrentContext(contextsSupplier.get());
return (current != null) ? fromEndpoint(current.dockerEndpoint()) : null; return (current != null) ? fromEndpoint(current.dockerEndpoint()) : null;
} }
private static DockerCliContextResponse getCurrentContext(List<DockerCliContextResponse> candidates) { private static @Nullable DockerCliContextResponse getCurrentContext(List<DockerCliContextResponse> candidates) {
return candidates.stream().filter(DockerCliContextResponse::current).findFirst().orElse(null); return candidates.stream().filter(DockerCliContextResponse::current).findFirst().orElse(null);
} }
private static String fromEndpoint(String endpoint) { private static @Nullable String fromEndpoint(@Nullable String endpoint) {
return (StringUtils.hasLength(endpoint)) ? fromUri(URI.create(endpoint)) : null; return (StringUtils.hasLength(endpoint)) ? fromUri(URI.create(endpoint)) : null;
} }
private static String fromUri(URI uri) { private static @Nullable String fromUri(URI uri) {
try { try {
return switch (uri.getScheme()) { return switch (uri.getScheme()) {
case "http", "https", "tcp" -> uri.getHost(); case "http", "https", "tcp" -> uri.getHost();

View File

@ -16,6 +16,8 @@
package org.springframework.boot.docker.compose.core; package org.springframework.boot.docker.compose.core;
import org.jspecify.annotations.Nullable;
import org.springframework.util.Assert; import org.springframework.util.Assert;
/** /**
@ -38,7 +40,7 @@ class ImageName {
private final String string; private final String string;
ImageName(String domain, String path) { ImageName(@Nullable String domain, String path) {
Assert.hasText(path, "'path' must not be empty"); Assert.hasText(path, "'path' must not be empty");
this.domain = getDomainOrDefault(domain); this.domain = getDomainOrDefault(domain);
this.name = getNameWithDefaultPath(this.domain, path); this.name = getNameWithDefaultPath(this.domain, path);
@ -90,7 +92,7 @@ class ImageName {
return this.string; return this.string;
} }
private String getDomainOrDefault(String domain) { private String getDomainOrDefault(@Nullable String domain) {
if (domain == null || LEGACY_DOMAIN.equals(domain)) { if (domain == null || LEGACY_DOMAIN.equals(domain)) {
return DEFAULT_DOMAIN; return DEFAULT_DOMAIN;
} }
@ -104,7 +106,7 @@ class ImageName {
return name; return name;
} }
static String parseDomain(String value) { static @Nullable String parseDomain(String value) {
int firstSlash = value.indexOf('/'); int firstSlash = value.indexOf('/');
String candidate = (firstSlash != -1) ? value.substring(0, firstSlash) : null; String candidate = (firstSlash != -1) ? value.substring(0, firstSlash) : null;
if (candidate != null && Regex.DOMAIN.matcher(candidate).matches()) { if (candidate != null && Regex.DOMAIN.matcher(candidate).matches()) {

View File

@ -18,6 +18,8 @@ package org.springframework.boot.docker.compose.core;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import org.jspecify.annotations.Nullable;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils; import org.springframework.util.ObjectUtils;
@ -32,13 +34,13 @@ public final class ImageReference {
private final ImageName name; private final ImageName name;
private final String tag; private final @Nullable String tag;
private final String digest; private final @Nullable String digest;
private final String string; private final String string;
private ImageReference(ImageName name, String tag, String digest) { private ImageReference(ImageName name, @Nullable String tag, @Nullable String digest) {
Assert.notNull(name, "'name' must not be null"); Assert.notNull(name, "'name' must not be null");
this.name = name; this.name = name;
this.tag = tag; this.tag = tag;
@ -68,7 +70,7 @@ public final class ImageReference {
* Return the tag from the reference or {@code null}. * Return the tag from the reference or {@code null}.
* @return the referenced tag * @return the referenced tag
*/ */
public String getTag() { public @Nullable String getTag() {
return this.tag; return this.tag;
} }
@ -76,7 +78,7 @@ public final class ImageReference {
* Return the digest from the reference or {@code null}. * Return the digest from the reference or {@code null}.
* @return the referenced digest * @return the referenced digest
*/ */
public String getDigest() { public @Nullable String getDigest() {
return this.digest; return this.digest;
} }
@ -111,7 +113,7 @@ public final class ImageReference {
return this.string; return this.string;
} }
private String buildString(String name, String tag, String digest) { private String buildString(String name, @Nullable String tag, @Nullable String digest) {
StringBuilder string = new StringBuilder(name); StringBuilder string = new StringBuilder(name);
if (tag != null) { if (tag != null) {
string.append(":").append(tag); string.append(":").append(tag);

View File

@ -16,6 +16,8 @@
package org.springframework.boot.docker.compose.core; package org.springframework.boot.docker.compose.core;
import org.jspecify.annotations.Nullable;
/** /**
* Exception thrown by {@link ProcessRunner} when the process exits with a non-zero code. * Exception thrown by {@link ProcessRunner} when the process exits with a non-zero code.
* *
@ -37,7 +39,7 @@ class ProcessExitException extends RuntimeException {
this(exitCode, command, stdOut, stdErr, null); this(exitCode, command, stdOut, stdErr, null);
} }
ProcessExitException(int exitCode, String[] command, String stdOut, String stdErr, Throwable cause) { ProcessExitException(int exitCode, String[] command, String stdOut, String stdErr, @Nullable Throwable cause) {
super(buildMessage(exitCode, command, stdOut, stdErr), cause); super(buildMessage(exitCode, command, stdOut, stdErr), cause);
this.exitCode = exitCode; this.exitCode = exitCode;
this.command = command; this.command = command;

View File

@ -29,6 +29,7 @@ import java.util.function.Consumer;
import org.apache.commons.logging.Log; import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory; import org.apache.commons.logging.LogFactory;
import org.jspecify.annotations.Nullable;
import org.springframework.core.log.LogMessage; import org.springframework.core.log.LogMessage;
@ -47,7 +48,7 @@ class ProcessRunner {
private static final Log logger = LogFactory.getLog(ProcessRunner.class); private static final Log logger = LogFactory.getLog(ProcessRunner.class);
private final File workingDirectory; private final @Nullable File workingDirectory;
/** /**
* Create a new {@link ProcessRunner} instance. * Create a new {@link ProcessRunner} instance.
@ -60,7 +61,7 @@ class ProcessRunner {
* Create a new {@link ProcessRunner} instance. * Create a new {@link ProcessRunner} instance.
* @param workingDirectory the working directory for the process * @param workingDirectory the working directory for the process
*/ */
ProcessRunner(File workingDirectory) { ProcessRunner(@Nullable File workingDirectory) {
this.workingDirectory = workingDirectory; this.workingDirectory = workingDirectory;
} }
@ -83,7 +84,7 @@ class ProcessRunner {
* @return the output of the command * @return the output of the command
* @throws ProcessExitException if execution failed * @throws ProcessExitException if execution failed
*/ */
String run(Consumer<String> outputConsumer, String... command) { String run(@Nullable Consumer<String> outputConsumer, String... command) {
logger.trace(LogMessage.of(() -> "Running '%s'".formatted(String.join(" ", command)))); logger.trace(LogMessage.of(() -> "Running '%s'".formatted(String.join(" ", command))));
Process process = startProcess(command); Process process = startProcess(command);
ReaderThread stdOutReader = new ReaderThread(process.getInputStream(), "stdout", outputConsumer); ReaderThread stdOutReader = new ReaderThread(process.getInputStream(), "stdout", outputConsumer);
@ -133,13 +134,13 @@ class ProcessRunner {
private final InputStream source; private final InputStream source;
private final Consumer<String> outputConsumer; private final @Nullable Consumer<String> outputConsumer;
private final StringBuilder output = new StringBuilder(); private final StringBuilder output = new StringBuilder();
private final CountDownLatch latch = new CountDownLatch(1); private final CountDownLatch latch = new CountDownLatch(1);
ReaderThread(InputStream source, String name, Consumer<String> outputConsumer) { ReaderThread(InputStream source, String name, @Nullable Consumer<String> outputConsumer) {
this.source = source; this.source = source;
this.outputConsumer = outputConsumer; this.outputConsumer = outputConsumer;
setName("OutputReader-" + name); setName("OutputReader-" + name);
@ -174,7 +175,7 @@ class ProcessRunner {
return this.output.toString(); return this.output.toString();
} }
catch (InterruptedException ex) { catch (InterruptedException ex) {
return null; return "";
} }
} }

View File

@ -18,6 +18,8 @@ package org.springframework.boot.docker.compose.core;
import java.util.Map; import java.util.Map;
import org.jspecify.annotations.Nullable;
/** /**
* Provides details of a running Docker Compose service. * Provides details of a running Docker Compose service.
* *
@ -69,7 +71,7 @@ public interface RunningService {
* @return the Docker Compose file * @return the Docker Compose file
* @since 3.5.0 * @since 3.5.0
*/ */
default DockerComposeFile composeFile() { default @Nullable DockerComposeFile composeFile() {
return null; return null;
} }

View File

@ -17,4 +17,7 @@
/** /**
* Core interfaces and classes for working with Docker Compose. * Core interfaces and classes for working with Docker Compose.
*/ */
@NullMarked
package org.springframework.boot.docker.compose.core; package org.springframework.boot.docker.compose.core;
import org.jspecify.annotations.NullMarked;

View File

@ -23,6 +23,7 @@ import java.util.Set;
import org.apache.commons.logging.Log; import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory; import org.apache.commons.logging.LogFactory;
import org.jspecify.annotations.Nullable;
import org.springframework.aot.AotDetector; import org.springframework.aot.AotDetector;
import org.springframework.boot.SpringApplicationShutdownHandlers; import org.springframework.boot.SpringApplicationShutdownHandlers;
@ -57,11 +58,11 @@ class DockerComposeLifecycleManager {
private static final String IGNORE_LABEL = "org.springframework.boot.ignore"; private static final String IGNORE_LABEL = "org.springframework.boot.ignore";
private final File workingDirectory; private final @Nullable File workingDirectory;
private final ApplicationContext applicationContext; private final ApplicationContext applicationContext;
private final ClassLoader classLoader; private final @Nullable ClassLoader classLoader;
private final SpringApplicationShutdownHandlers shutdownHandlers; private final SpringApplicationShutdownHandlers shutdownHandlers;
@ -80,10 +81,10 @@ class DockerComposeLifecycleManager {
new DockerComposeSkipCheck(), null); new DockerComposeSkipCheck(), null);
} }
DockerComposeLifecycleManager(File workingDirectory, ApplicationContext applicationContext, Binder binder, DockerComposeLifecycleManager(@Nullable File workingDirectory, ApplicationContext applicationContext, Binder binder,
SpringApplicationShutdownHandlers shutdownHandlers, DockerComposeProperties properties, SpringApplicationShutdownHandlers shutdownHandlers, DockerComposeProperties properties,
Set<ApplicationListener<?>> eventListeners, DockerComposeSkipCheck skipCheck, Set<ApplicationListener<?>> eventListeners, DockerComposeSkipCheck skipCheck,
ServiceReadinessChecks serviceReadinessChecks) { @Nullable ServiceReadinessChecks serviceReadinessChecks) {
this.workingDirectory = workingDirectory; this.workingDirectory = workingDirectory;
this.applicationContext = applicationContext; this.applicationContext = applicationContext;
this.classLoader = applicationContext.getClassLoader(); this.classLoader = applicationContext.getClassLoader();

View File

@ -23,6 +23,8 @@ import java.util.LinkedHashSet;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
import org.jspecify.annotations.Nullable;
import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.bind.Binder; import org.springframework.boot.context.properties.bind.Binder;
import org.springframework.boot.docker.compose.core.RunningService; import org.springframework.boot.docker.compose.core.RunningService;
@ -64,7 +66,7 @@ public class DockerComposeProperties {
/** /**
* Hostname or IP of the machine where the docker containers are started. * Hostname or IP of the machine where the docker containers are started.
*/ */
private String host; private @Nullable String host;
/** /**
* Start configuration. * Start configuration.
@ -109,11 +111,11 @@ public class DockerComposeProperties {
this.lifecycleManagement = lifecycleManagement; this.lifecycleManagement = lifecycleManagement;
} }
public String getHost() { public @Nullable String getHost() {
return this.host; return this.host;
} }
public void setHost(String host) { public void setHost(@Nullable String host) {
this.host = host; this.host = host;
} }

View File

@ -20,6 +20,8 @@ import java.util.Collections;
import java.util.LinkedHashSet; import java.util.LinkedHashSet;
import java.util.Set; import java.util.Set;
import org.jspecify.annotations.Nullable;
import org.springframework.boot.SpringApplicationAotProcessor; import org.springframework.boot.SpringApplicationAotProcessor;
import org.springframework.util.ClassUtils; import org.springframework.util.ClassUtils;
@ -44,7 +46,7 @@ class DockerComposeSkipCheck {
SKIPPED_STACK_ELEMENTS = Collections.unmodifiableSet(skipped); SKIPPED_STACK_ELEMENTS = Collections.unmodifiableSet(skipped);
} }
boolean shouldSkip(ClassLoader classLoader, DockerComposeProperties.Skip properties) { boolean shouldSkip(@Nullable ClassLoader classLoader, DockerComposeProperties.Skip properties) {
if (properties.isInTests() && hasAtLeastOneRequiredClass(classLoader)) { if (properties.isInTests() && hasAtLeastOneRequiredClass(classLoader)) {
Thread thread = Thread.currentThread(); Thread thread = Thread.currentThread();
for (StackTraceElement element : thread.getStackTrace()) { for (StackTraceElement element : thread.getStackTrace()) {
@ -56,7 +58,7 @@ class DockerComposeSkipCheck {
return false; return false;
} }
private boolean hasAtLeastOneRequiredClass(ClassLoader classLoader) { private boolean hasAtLeastOneRequiredClass(@Nullable ClassLoader classLoader) {
for (String requiredClass : REQUIRED_CLASSES) { for (String requiredClass : REQUIRED_CLASSES) {
if (ClassUtils.isPresent(requiredClass, classLoader)) { if (ClassUtils.isPresent(requiredClass, classLoader)) {
return true; return true;

View File

@ -16,6 +16,8 @@
package org.springframework.boot.docker.compose.lifecycle; package org.springframework.boot.docker.compose.lifecycle;
import org.jspecify.annotations.Nullable;
import org.springframework.boot.docker.compose.core.RunningService; import org.springframework.boot.docker.compose.core.RunningService;
/** /**
@ -33,7 +35,7 @@ class ServiceNotReadyException extends RuntimeException {
this(service, message, null); this(service, message, null);
} }
ServiceNotReadyException(RunningService service, String message, Throwable cause) { ServiceNotReadyException(RunningService service, String message, @Nullable Throwable cause) {
super(message, cause); super(message, cause);
this.service = service; this.service = service;
} }

View File

@ -17,4 +17,7 @@
/** /**
* Lifecycle management for Docker Compose with the context of a Spring application. * Lifecycle management for Docker Compose with the context of a Spring application.
*/ */
@NullMarked
package org.springframework.boot.docker.compose.lifecycle; package org.springframework.boot.docker.compose.lifecycle;
import org.jspecify.annotations.NullMarked;

View File

@ -21,6 +21,8 @@ import java.util.Arrays;
import java.util.Set; import java.util.Set;
import java.util.function.Predicate; import java.util.function.Predicate;
import org.jspecify.annotations.Nullable;
import org.springframework.boot.autoconfigure.service.connection.ConnectionDetails; import org.springframework.boot.autoconfigure.service.connection.ConnectionDetails;
import org.springframework.boot.autoconfigure.service.connection.ConnectionDetailsFactory; import org.springframework.boot.autoconfigure.service.connection.ConnectionDetailsFactory;
import org.springframework.boot.docker.compose.core.DockerComposeFile; import org.springframework.boot.docker.compose.core.DockerComposeFile;
@ -92,7 +94,7 @@ public abstract class DockerComposeConnectionDetailsFactory<D extends Connection
} }
@Override @Override
public final D getConnectionDetails(DockerComposeConnectionSource source) { public final @Nullable D getConnectionDetails(DockerComposeConnectionSource source) {
return (!accept(source)) ? null : getDockerComposeConnectionDetails(source); return (!accept(source)) ? null : getDockerComposeConnectionDetails(source);
} }
@ -112,7 +114,7 @@ public abstract class DockerComposeConnectionDetailsFactory<D extends Connection
* @param source the source * @param source the source
* @return the service connection or {@code null}. * @return the service connection or {@code null}.
*/ */
protected abstract D getDockerComposeConnectionDetails(DockerComposeConnectionSource source); protected abstract @Nullable D getDockerComposeConnectionDetails(DockerComposeConnectionSource source);
/** /**
* Convenient base class for {@link ConnectionDetails} results that are backed by a * Convenient base class for {@link ConnectionDetails} results that are backed by a
@ -120,9 +122,9 @@ public abstract class DockerComposeConnectionDetailsFactory<D extends Connection
*/ */
protected static class DockerComposeConnectionDetails implements ConnectionDetails, OriginProvider { protected static class DockerComposeConnectionDetails implements ConnectionDetails, OriginProvider {
private final Origin origin; private final @Nullable Origin origin;
private volatile SslBundle sslBundle; private volatile @Nullable SslBundle sslBundle;
/** /**
* Create a new {@link DockerComposeConnectionDetails} instance. * Create a new {@link DockerComposeConnectionDetails} instance.
@ -134,11 +136,11 @@ public abstract class DockerComposeConnectionDetailsFactory<D extends Connection
} }
@Override @Override
public Origin getOrigin() { public @Nullable Origin getOrigin() {
return this.origin; return this.origin;
} }
protected SslBundle getSslBundle(RunningService service) { protected @Nullable SslBundle getSslBundle(RunningService service) {
if (this.sslBundle != null) { if (this.sslBundle != null) {
return this.sslBundle; return this.sslBundle;
} }
@ -155,7 +157,7 @@ public abstract class DockerComposeConnectionDetailsFactory<D extends Connection
return sslBundle; return sslBundle;
} }
private SslBundle getJksSslBundle(RunningService service) { private @Nullable SslBundle getJksSslBundle(RunningService service) {
JksSslStoreDetails keyStoreDetails = getJksSslStoreDetails(service, "keystore"); JksSslStoreDetails keyStoreDetails = getJksSslStoreDetails(service, "keystore");
JksSslStoreDetails trustStoreDetails = getJksSslStoreDetails(service, "truststore"); JksSslStoreDetails trustStoreDetails = getJksSslStoreDetails(service, "truststore");
if (keyStoreDetails == null && trustStoreDetails == null) { if (keyStoreDetails == null && trustStoreDetails == null) {
@ -173,13 +175,13 @@ public abstract class DockerComposeConnectionDetailsFactory<D extends Connection
options, protocol); options, protocol);
} }
private ResourceLoader getResourceLoader(Path workingDirectory) { private ResourceLoader getResourceLoader(@Nullable Path workingDirectory) {
ClassLoader classLoader = ApplicationResourceLoader.get().getClassLoader(); ClassLoader classLoader = ApplicationResourceLoader.get().getClassLoader();
return ApplicationResourceLoader.get(classLoader, return ApplicationResourceLoader.get(classLoader,
SpringFactoriesLoader.forDefaultResourceLocation(classLoader), workingDirectory); SpringFactoriesLoader.forDefaultResourceLocation(classLoader), workingDirectory);
} }
private JksSslStoreDetails getJksSslStoreDetails(RunningService service, String storeType) { private @Nullable JksSslStoreDetails getJksSslStoreDetails(RunningService service, String storeType) {
String type = service.labels().get("org.springframework.boot.sslbundle.jks.%s.type".formatted(storeType)); String type = service.labels().get("org.springframework.boot.sslbundle.jks.%s.type".formatted(storeType));
String provider = service.labels() String provider = service.labels()
.get("org.springframework.boot.sslbundle.jks.%s.provider".formatted(storeType)); .get("org.springframework.boot.sslbundle.jks.%s.provider".formatted(storeType));
@ -193,7 +195,7 @@ public abstract class DockerComposeConnectionDetailsFactory<D extends Connection
return new JksSslStoreDetails(type, provider, location, password); return new JksSslStoreDetails(type, provider, location, password);
} }
private Path getWorkingDirectory(RunningService runningService) { private @Nullable Path getWorkingDirectory(RunningService runningService) {
DockerComposeFile composeFile = runningService.composeFile(); DockerComposeFile composeFile = runningService.composeFile();
if (composeFile == null || CollectionUtils.isEmpty(composeFile.getFiles())) { if (composeFile == null || CollectionUtils.isEmpty(composeFile.getFiles())) {
return Path.of("."); return Path.of(".");
@ -201,7 +203,7 @@ public abstract class DockerComposeConnectionDetailsFactory<D extends Connection
return composeFile.getFiles().get(0).toPath().getParent(); return composeFile.getFiles().get(0).toPath().getParent();
} }
private SslOptions createSslOptions(String ciphers, String enabledProtocols) { private SslOptions createSslOptions(@Nullable String ciphers, @Nullable String enabledProtocols) {
Set<String> ciphersSet = null; Set<String> ciphersSet = null;
if (StringUtils.hasLength(ciphers)) { if (StringUtils.hasLength(ciphers)) {
ciphersSet = StringUtils.commaDelimitedListToSet(ciphers); ciphersSet = StringUtils.commaDelimitedListToSet(ciphers);
@ -213,7 +215,7 @@ public abstract class DockerComposeConnectionDetailsFactory<D extends Connection
return SslOptions.of(ciphersSet, enabledProtocolsSet); return SslOptions.of(ciphersSet, enabledProtocolsSet);
} }
private SslBundle getPemSslBundle(RunningService service) { private @Nullable SslBundle getPemSslBundle(RunningService service) {
PemSslStoreDetails keyStoreDetails = getPemSslStoreDetails(service, "keystore"); PemSslStoreDetails keyStoreDetails = getPemSslStoreDetails(service, "keystore");
PemSslStoreDetails trustStoreDetails = getPemSslStoreDetails(service, "truststore"); PemSslStoreDetails trustStoreDetails = getPemSslStoreDetails(service, "truststore");
if (keyStoreDetails == null && trustStoreDetails == null) { if (keyStoreDetails == null && trustStoreDetails == null) {
@ -231,7 +233,7 @@ public abstract class DockerComposeConnectionDetailsFactory<D extends Connection
PemSslStore.load(trustStoreDetails, resourceLoader)), key, options, protocol); PemSslStore.load(trustStoreDetails, resourceLoader)), key, options, protocol);
} }
private PemSslStoreDetails getPemSslStoreDetails(RunningService service, String storeType) { private @Nullable PemSslStoreDetails getPemSslStoreDetails(RunningService service, String storeType) {
String type = service.labels().get("org.springframework.boot.sslbundle.pem.%s.type".formatted(storeType)); String type = service.labels().get("org.springframework.boot.sslbundle.pem.%s.type".formatted(storeType));
String certificate = service.labels() String certificate = service.labels()
.get("org.springframework.boot.sslbundle.pem.%s.certificate".formatted(storeType)); .get("org.springframework.boot.sslbundle.pem.%s.certificate".formatted(storeType));

View File

@ -17,4 +17,7 @@
/** /**
* Service connection support for Docker Compose. * Service connection support for Docker Compose.
*/ */
@NullMarked
package org.springframework.boot.docker.compose.service.connection; package org.springframework.boot.docker.compose.service.connection;
import org.jspecify.annotations.NullMarked;

View File

@ -22,6 +22,7 @@ import java.util.List;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.boot.docker.compose.core.DockerCliCommand.ComposeVersion; import org.springframework.boot.docker.compose.core.DockerCliCommand.ComposeVersion;
import org.springframework.boot.docker.compose.core.DockerCliCommand.None;
import org.springframework.boot.logging.LogLevel; import org.springframework.boot.logging.LogLevel;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
@ -85,7 +86,7 @@ class DockerCliCommandTests {
assertThat(command.getLogLevel()).isEqualTo(LogLevel.INFO); assertThat(command.getLogLevel()).isEqualTo(LogLevel.INFO);
assertThat(command.getCommand(COMPOSE_VERSION)).containsExactly("up", "--no-color", "--detach", "--wait", assertThat(command.getCommand(COMPOSE_VERSION)).containsExactly("up", "--no-color", "--detach", "--wait",
"--renew-anon-volumes"); "--renew-anon-volumes");
assertThat(command.deserialize("[]")).isNull(); assertThat(command.deserialize("[]")).isSameAs(None.INSTANCE);
} }
@Test @Test
@ -94,7 +95,7 @@ class DockerCliCommandTests {
List.of("--remove-orphans")); List.of("--remove-orphans"));
assertThat(command.getType()).isEqualTo(DockerCliCommand.Type.DOCKER_COMPOSE); assertThat(command.getType()).isEqualTo(DockerCliCommand.Type.DOCKER_COMPOSE);
assertThat(command.getCommand(COMPOSE_VERSION)).containsExactly("down", "--timeout", "1", "--remove-orphans"); assertThat(command.getCommand(COMPOSE_VERSION)).containsExactly("down", "--timeout", "1", "--remove-orphans");
assertThat(command.deserialize("[]")).isNull(); assertThat(command.deserialize("[]")).isSameAs(None.INSTANCE);
} }
@Test @Test
@ -103,7 +104,7 @@ class DockerCliCommandTests {
assertThat(command.getType()).isEqualTo(DockerCliCommand.Type.DOCKER_COMPOSE); assertThat(command.getType()).isEqualTo(DockerCliCommand.Type.DOCKER_COMPOSE);
assertThat(command.getLogLevel()).isEqualTo(LogLevel.INFO); assertThat(command.getLogLevel()).isEqualTo(LogLevel.INFO);
assertThat(command.getCommand(COMPOSE_VERSION)).containsExactly("start", "--dry-run"); assertThat(command.getCommand(COMPOSE_VERSION)).containsExactly("start", "--dry-run");
assertThat(command.deserialize("[]")).isNull(); assertThat(command.deserialize("[]")).isSameAs(None.INSTANCE);
} }
@Test @Test
@ -111,7 +112,7 @@ class DockerCliCommandTests {
DockerCliCommand<?> command = new DockerCliCommand.ComposeStop(Duration.ofSeconds(1), List.of("--dry-run")); DockerCliCommand<?> command = new DockerCliCommand.ComposeStop(Duration.ofSeconds(1), List.of("--dry-run"));
assertThat(command.getType()).isEqualTo(DockerCliCommand.Type.DOCKER_COMPOSE); assertThat(command.getType()).isEqualTo(DockerCliCommand.Type.DOCKER_COMPOSE);
assertThat(command.getCommand(COMPOSE_VERSION)).containsExactly("stop", "--timeout", "1", "--dry-run"); assertThat(command.getCommand(COMPOSE_VERSION)).containsExactly("stop", "--timeout", "1", "--dry-run");
assertThat(command.deserialize("[]")).isNull(); assertThat(command.deserialize("[]")).isSameAs(None.INSTANCE);
} }
@Test @Test