diff --git a/daemon/src/main/java/org/apache/maven/cli/DaemonMavenCli.java b/daemon/src/main/java/org/apache/maven/cli/DaemonMavenCli.java index f52e53646..6d1fdb0dc 100644 --- a/daemon/src/main/java/org/apache/maven/cli/DaemonMavenCli.java +++ b/daemon/src/main/java/org/apache/maven/cli/DaemonMavenCli.java @@ -25,6 +25,8 @@ import java.io.PrintStream; import java.lang.reflect.Field; import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; @@ -100,6 +102,9 @@ import org.mvndaemon.mvnd.cache.invalidating.InvalidatingPluginRealmCache; import org.mvndaemon.mvnd.cache.invalidating.InvalidatingProjectArtifactsCache; import org.mvndaemon.mvnd.common.Environment; +import org.mvndaemon.mvnd.execution.BuildResumptionPersistenceException; +import org.mvndaemon.mvnd.execution.DefaultBuildResumptionAnalyzer; +import org.mvndaemon.mvnd.execution.DefaultBuildResumptionDataRepository; import org.mvndaemon.mvnd.logging.internal.Slf4jLoggerManager; import org.mvndaemon.mvnd.logging.smart.BuildEventListener; import org.mvndaemon.mvnd.logging.smart.LoggingExecutionListener; @@ -113,6 +118,7 @@ import org.sonatype.plexus.components.sec.dispatcher.DefaultSecDispatcher; import org.sonatype.plexus.components.sec.dispatcher.SecDispatcher; +import static java.util.Comparator.comparing; import static org.apache.maven.shared.utils.logging.MessageUtils.buffer; /** @@ -142,6 +148,8 @@ public class DaemonMavenCli { public static final String STYLE_COLOR_PROPERTY = "style.color"; + public static final String RESUME = "r"; + private final Slf4jLoggerManager plexusLoggerManager; private final ILoggerFactory slf4jLoggerFactory; @@ -260,7 +268,7 @@ void initialize(CliRequest cliRequest) void cli(CliRequest cliRequest) throws Exception { - CLIManager cliManager = new CLIManager(); + CLIManager cliManager = newCLIManager(); List args = new ArrayList<>(); CommandLine mavenConfig = null; @@ -301,10 +309,16 @@ void cli(CliRequest cliRequest) private void help(CliRequest cliRequest) throws Exception { if (cliRequest.commandLine.hasOption(CLIManager.HELP)) { - buildEventListener.log(MvndHelpFormatter.displayHelp(new CLIManager())); + buildEventListener.log(MvndHelpFormatter.displayHelp(newCLIManager())); throw new ExitException(0); } + } + private CLIManager newCLIManager() { + CLIManager cliManager = new CLIManager(); + cliManager.options.addOption(Option.builder(RESUME).longOpt("resume").desc("Resume reactor from " + + "the last failed project, using the resume.properties file in the build directory").build()); + return cliManager; } private CommandLine cliMerge(CommandLine mavenArgs, CommandLine mavenConfig) { @@ -739,15 +753,15 @@ private int execute(CliRequest cliRequest) Map references = new LinkedHashMap<>(); - MavenProject project = null; + List failedProjects = new ArrayList<>(); for (Throwable exception : result.getExceptions()) { ExceptionSummary summary = handler.handleException(exception); logSummary(summary, references, "", cliRequest.showErrors); - if (project == null && exception instanceof LifecycleExecutionException) { - project = ((LifecycleExecutionException) exception).getProject(); + if (exception instanceof LifecycleExecutionException) { + failedProjects.add(((LifecycleExecutionException) exception).getProject()); } } @@ -772,11 +786,30 @@ private int execute(CliRequest cliRequest) } } - if (project != null && !project.equals(result.getTopologicallySortedProjects().get(0))) { - slf4jLogger.error(""); - slf4jLogger.error("After correcting the problems, you can resume the build with the command"); - slf4jLogger.error(buffer().a(" ").strong("mvn -rf " - + getResumeFrom(result.getTopologicallySortedProjects(), project)).toString()); + boolean canResume = new DefaultBuildResumptionAnalyzer().determineBuildResumptionData(result).map(resumption -> { + try { + Path directory = Paths.get(request.getBaseDirectory()).resolve("target"); + new DefaultBuildResumptionDataRepository().persistResumptionData(directory, resumption); + return true; + } catch (BuildResumptionPersistenceException e) { + slf4jLogger.warn("Could not persist build resumption data", e); + } + return false; + }).orElse(false); + + if (canResume) { + logBuildResumeHint("mvn -r"); + } else if (!failedProjects.isEmpty()) { + List sortedProjects = result.getTopologicallySortedProjects(); + + // Sort the failedProjects list in the topologically sorted order. + failedProjects.sort(comparing(sortedProjects::indexOf)); + + MavenProject firstFailedProject = failedProjects.get(0); + if (!firstFailedProject.equals(sortedProjects.get(0))) { + String resumeFromSelector = getResumeFromSelector(sortedProjects, firstFailedProject); + logBuildResumeHint("mvn -rf " + resumeFromSelector); + } } if (MavenExecutionRequest.REACTOR_FAIL_NEVER.equals(cliRequest.request.getReactorFailureBehavior())) { @@ -787,10 +820,18 @@ private int execute(CliRequest cliRequest) return 1; } } else { + Path directory = Paths.get(request.getBaseDirectory()).resolve("target"); + new DefaultBuildResumptionDataRepository().removeResumptionData(directory); return 0; } } + private void logBuildResumeHint(String resumeBuildHint) { + slf4jLogger.error(""); + slf4jLogger.error("After correcting the problems, you can resume the build with the command"); + slf4jLogger.error(buffer().a(" ").strong(resumeBuildHint).toString()); + } + /** * A helper method to determine the value to resume the build with {@code -rf} taking into account the * edge case where multiple modules in the reactor have the same artifactId. @@ -808,7 +849,7 @@ private int execute(CliRequest cliRequest) * @return Value for -rf flag to resume build exactly from place where it failed ({@code :artifactId} in * general and {@code groupId:artifactId} when there is a name clash). */ - private String getResumeFrom(List mavenProjects, MavenProject failedProject) { + private String getResumeFromSelector(List mavenProjects, MavenProject failedProject) { for (MavenProject buildProject : mavenProjects) { if (failedProject.getArtifactId().equals(buildProject.getArtifactId()) && !failedProject.equals( buildProject)) { @@ -1172,6 +1213,11 @@ private static void populateRequest( request.setBaseDirectory(request.getPom().getParentFile()); } + if (commandLine.hasOption(RESUME)) { + new DefaultBuildResumptionDataRepository() + .applyResumptionData(request, Paths.get(request.getBaseDirectory()).resolve("target")); + } + if (commandLine.hasOption(CLIManager.RESUME_FROM)) { request.setResumeFrom(commandLine.getOptionValue(CLIManager.RESUME_FROM)); } diff --git a/daemon/src/main/java/org/mvndaemon/mvnd/execution/BuildResumptionAnalyzer.java b/daemon/src/main/java/org/mvndaemon/mvnd/execution/BuildResumptionAnalyzer.java new file mode 100644 index 000000000..07d0fc737 --- /dev/null +++ b/daemon/src/main/java/org/mvndaemon/mvnd/execution/BuildResumptionAnalyzer.java @@ -0,0 +1,52 @@ +/* + * Copyright 2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.mvndaemon.mvnd.execution; + +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import java.util.Optional; +import org.apache.maven.execution.MavenExecutionResult; + +/** + * Instances of this class are responsible for determining whether it makes sense to "resume" a build (i.e., using + * the {@code --resume} flag. + */ +public interface BuildResumptionAnalyzer { + /** + * Construct an instance of {@link BuildResumptionData} based on the outcome of the current Maven build. + * + * @param result Outcome of the current Maven build. + * @return A {@link BuildResumptionData} instance or {@link Optional#empty()} if resuming the build is not + * possible. + */ + Optional determineBuildResumptionData(final MavenExecutionResult result); +} diff --git a/daemon/src/main/java/org/mvndaemon/mvnd/execution/BuildResumptionData.java b/daemon/src/main/java/org/mvndaemon/mvnd/execution/BuildResumptionData.java new file mode 100644 index 000000000..cbe3eb4d2 --- /dev/null +++ b/daemon/src/main/java/org/mvndaemon/mvnd/execution/BuildResumptionData.java @@ -0,0 +1,60 @@ +/* + * Copyright 2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.mvndaemon.mvnd.execution; + +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import java.util.List; + +/** + * This class holds the information required to enable resuming a Maven build with {@code --resume}. + */ +public class BuildResumptionData { + /** + * The list of projects that remain to be built. + */ + private final List remainingProjects; + + public BuildResumptionData(final List remainingProjects) { + this.remainingProjects = remainingProjects; + } + + /** + * Returns the projects that still need to be built when resuming. + * + * @return A list containing the group and artifact id of the projects. + */ + public List getRemainingProjects() { + return this.remainingProjects; + } + +} diff --git a/daemon/src/main/java/org/mvndaemon/mvnd/execution/BuildResumptionDataRepository.java b/daemon/src/main/java/org/mvndaemon/mvnd/execution/BuildResumptionDataRepository.java new file mode 100644 index 000000000..52b998e99 --- /dev/null +++ b/daemon/src/main/java/org/mvndaemon/mvnd/execution/BuildResumptionDataRepository.java @@ -0,0 +1,72 @@ +/* + * Copyright 2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.mvndaemon.mvnd.execution; + +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import org.apache.maven.execution.MavenExecutionRequest; +import org.apache.maven.project.MavenProject; + +/** + * Instances of this interface retrieve and store data for the --resume / -r feature. This data is used to ensure newer + * builds of the same project, that have the -r command-line flag, skip successfully built projects during earlier + * invocations of Maven. + */ +public interface BuildResumptionDataRepository { + /** + * Persists any data needed to resume the build at a later point in time, using a new Maven invocation. This method + * may also decide it is not needed or meaningful to persist such data, and return false to indicate + * so. + * + * @param rootProject The root project that is being built. + * @param buildResumptionData Information needed to resume the build. + * @throws BuildResumptionPersistenceException When an error occurs while persisting data. + */ + void persistResumptionData(final MavenProject rootProject, final BuildResumptionData buildResumptionData) + throws BuildResumptionPersistenceException; + + /** + * Uses previously stored resumption data to enrich an existing execution request. + * + * @param request The execution request that will be enriched. + * @param rootProject The root project that is being built. + */ + void applyResumptionData(final MavenExecutionRequest request, final MavenProject rootProject); + + /** + * Removes previously stored resumption data. + * + * @param rootProject The root project that is being built. + */ + void removeResumptionData(final MavenProject rootProject); + +} diff --git a/daemon/src/main/java/org/mvndaemon/mvnd/execution/BuildResumptionPersistenceException.java b/daemon/src/main/java/org/mvndaemon/mvnd/execution/BuildResumptionPersistenceException.java new file mode 100644 index 000000000..c80bede24 --- /dev/null +++ b/daemon/src/main/java/org/mvndaemon/mvnd/execution/BuildResumptionPersistenceException.java @@ -0,0 +1,46 @@ +/* + * Copyright 2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.mvndaemon.mvnd.execution; + +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/** + * This exception will be thrown when something fails while persisting build resumption data. + * + * @see BuildResumptionDataRepository#persistResumptionData + */ +public class BuildResumptionPersistenceException extends Exception { + public BuildResumptionPersistenceException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/daemon/src/main/java/org/mvndaemon/mvnd/execution/DefaultBuildResumptionAnalyzer.java b/daemon/src/main/java/org/mvndaemon/mvnd/execution/DefaultBuildResumptionAnalyzer.java new file mode 100644 index 000000000..844169f47 --- /dev/null +++ b/daemon/src/main/java/org/mvndaemon/mvnd/execution/DefaultBuildResumptionAnalyzer.java @@ -0,0 +1,85 @@ +/* + * Copyright 2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.mvndaemon.mvnd.execution; + +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; +import javax.inject.Named; +import javax.inject.Singleton; +import org.apache.maven.execution.BuildFailure; +import org.apache.maven.execution.BuildSuccess; +import org.apache.maven.execution.MavenExecutionResult; +import org.apache.maven.project.MavenProject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Default implementation of {@link BuildResumptionAnalyzer}. + */ +@Named +@Singleton +public class DefaultBuildResumptionAnalyzer implements BuildResumptionAnalyzer { + private static final Logger LOGGER = LoggerFactory.getLogger(DefaultBuildResumptionAnalyzer.class); + + @Override + public Optional determineBuildResumptionData(final MavenExecutionResult result) { + if (!result.hasExceptions()) { + return Optional.empty(); + } + + List sortedProjects = result.getTopologicallySortedProjects(); + + boolean hasNoSuccess = sortedProjects.stream() + .noneMatch(project -> result.getBuildSummary(project) instanceof BuildSuccess); + + if (hasNoSuccess) { + return Optional.empty(); + } + + List remainingProjects = sortedProjects.stream() + .filter(project -> result.getBuildSummary(project) == null + || result.getBuildSummary(project) instanceof BuildFailure) + .map(project -> project.getGroupId() + ":" + project.getArtifactId()) + .collect(Collectors.toList()); + + if (remainingProjects.isEmpty()) { + LOGGER.info("No remaining projects found, resuming the build would not make sense."); + return Optional.empty(); + } + + return Optional.of(new BuildResumptionData(remainingProjects)); + } + +} diff --git a/daemon/src/main/java/org/mvndaemon/mvnd/execution/DefaultBuildResumptionDataRepository.java b/daemon/src/main/java/org/mvndaemon/mvnd/execution/DefaultBuildResumptionDataRepository.java new file mode 100644 index 000000000..8bc381767 --- /dev/null +++ b/daemon/src/main/java/org/mvndaemon/mvnd/execution/DefaultBuildResumptionDataRepository.java @@ -0,0 +1,150 @@ +/* + * Copyright 2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.mvndaemon.mvnd.execution; + +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import java.io.IOException; +import java.io.Reader; +import java.io.Writer; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Properties; +import java.util.stream.Stream; +import javax.inject.Named; +import javax.inject.Singleton; +import org.apache.commons.lang3.StringUtils; +import org.apache.maven.execution.MavenExecutionRequest; +import org.apache.maven.project.MavenProject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This implementation of {@link BuildResumptionDataRepository} persists information in a properties file. The file is + * stored in the build output directory under the Maven execution root. + */ +@Named +@Singleton +public class DefaultBuildResumptionDataRepository implements BuildResumptionDataRepository { + private static final String RESUME_PROPERTIES_FILENAME = "resume.properties"; + private static final String REMAINING_PROJECTS = "remainingProjects"; + private static final String PROPERTY_DELIMITER = ", "; + private static final Logger LOGGER = LoggerFactory.getLogger(DefaultBuildResumptionDataRepository.class); + + @Override + public void persistResumptionData(MavenProject rootProject, BuildResumptionData buildResumptionData) + throws BuildResumptionPersistenceException { + Path directory = Paths.get(rootProject.getBuild().getDirectory()); + persistResumptionData(directory, buildResumptionData); + } + + public void persistResumptionData(Path directory, BuildResumptionData buildResumptionData) + throws BuildResumptionPersistenceException { + Properties properties = convertToProperties(buildResumptionData); + + Path resumeProperties = directory.resolve(RESUME_PROPERTIES_FILENAME); + try { + Files.createDirectories(resumeProperties.getParent()); + try (Writer writer = Files.newBufferedWriter(resumeProperties)) { + properties.store(writer, null); + } + } catch (IOException e) { + String message = "Could not create " + RESUME_PROPERTIES_FILENAME + " file."; + throw new BuildResumptionPersistenceException(message, e); + } + } + + private Properties convertToProperties(final BuildResumptionData buildResumptionData) { + Properties properties = new Properties(); + + String value = String.join(PROPERTY_DELIMITER, buildResumptionData.getRemainingProjects()); + properties.setProperty(REMAINING_PROJECTS, value); + + return properties; + } + + @Override + public void applyResumptionData(MavenExecutionRequest request, MavenProject rootProject) { + Path directory = Paths.get(rootProject.getBuild().getDirectory()); + applyResumptionData(request, directory); + } + + public void applyResumptionData(MavenExecutionRequest request, Path directory) { + Properties properties = loadResumptionFile(directory); + applyResumptionProperties(request, properties); + } + + @Override + public void removeResumptionData(MavenProject rootProject) { + Path directory = Paths.get(rootProject.getBuild().getDirectory()); + removeResumptionData(directory); + } + + public void removeResumptionData(Path directory) { + Path resumeProperties = directory.resolve(RESUME_PROPERTIES_FILENAME); + try { + Files.deleteIfExists(resumeProperties); + } catch (IOException e) { + LOGGER.warn("Could not delete {} file. ", RESUME_PROPERTIES_FILENAME, e); + } + } + + private Properties loadResumptionFile(Path rootBuildDirectory) { + Properties properties = new Properties(); + Path path = rootBuildDirectory.resolve(RESUME_PROPERTIES_FILENAME); + if (!Files.exists(path)) { + LOGGER.warn("The {} file does not exist. The --resume / -r feature will not work.", path); + return properties; + } + + try (Reader reader = Files.newBufferedReader(path)) { + properties.load(reader); + } catch (IOException e) { + LOGGER.warn("Unable to read {}. The --resume / -r feature will not work.", path); + } + + return properties; + } + + // This method is made package-private for testing purposes + void applyResumptionProperties(MavenExecutionRequest request, Properties properties) { + if (properties.containsKey(REMAINING_PROJECTS) + && StringUtils.isEmpty(request.getResumeFrom())) { + String propertyValue = properties.getProperty(REMAINING_PROJECTS); + Stream.of(propertyValue.split(PROPERTY_DELIMITER)) + .filter(StringUtils::isNotEmpty) + .forEach(request.getSelectedProjects()::add); + LOGGER.info("Resuming from {} due to the --resume / -r feature.", propertyValue); + } + } +}