NativeConnectorsProcessRunner.java
/*-
* ========================LICENSE_START=================================
* flyway-nc-core
* ========================================================================
* Copyright (C) 2010 - 2026 Red Gate Software Ltd
* ========================================================================
* 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.
* =========================LICENSE_END==================================
*/
package org.flywaydb.nc;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import lombok.CustomLog;
import org.flywaydb.core.api.FlywayException;
import org.flywaydb.core.internal.util.DockerUtils;
import org.flywaydb.core.internal.util.FileUtils;
import org.flywaydb.core.internal.util.FlywayDbWebsiteLinks;
import org.flywaydb.core.internal.util.StringUtils;
@CustomLog
public class NativeConnectorsProcessRunner {
private final ProcessBuilder processBuilder;
private final String tool;
private final Collection<String> printStrings = new ArrayList<>();
private boolean redirectOutput;
public NativeConnectorsProcessRunner(final List<String> commands, final String tool) {
processBuilder = new ProcessBuilder(commands);
processBuilder.environment();
this.tool = tool;
}
public void addTextToPrint(final String textToAdd) {
printStrings.add(textToAdd);
}
public void addEnvironmentVariable(final String key, final String value) {
processBuilder.environment().put(key, value);
}
public void removeEnvironmentVariable(final String key) {
processBuilder.environment().remove(key);
}
public void setWorkingDirectory(final String workingDirectory) {
processBuilder.directory(new File(workingDirectory));
}
public void redirectOutput() {
final File output = createTempLogFile("output");
processBuilder.redirectOutput(output);
final File errorOutput = createTempLogFile("errorOutput");
processBuilder.redirectError(errorOutput);
redirectOutput = true;
}
private File createTempLogFile(final String filename) {
try {
final File tempFile = File.createTempFile(filename, ".log");
tempFile.deleteOnExit();
return tempFile;
} catch (final Exception e) {
throw new FlywayException("Failed to execute SQL file due to: " + e.getMessage());
}
}
private String getOutputFromStream(final InputStream inputStream) throws IOException {
return FileUtils.copyToString(new InputStreamReader(inputStream,
StandardCharsets.UTF_8)).strip();
}
private String getOutputFromFile(final File file) throws IOException {
return Files.readString(Path.of(file.getAbsolutePath())).strip();
}
public void executeMigrations(final boolean outputQueryResults, final boolean combinedStreams) {
try {
LOG.debug("Executing " + tool);
final Process process = processBuilder.start();
final boolean exited = process.waitFor(5, TimeUnit.MINUTES);
if (!exited) {
throw new FlywayException(tool + " execution timeout. Consider using smaller migrations");
}
final String stdOut = redirectOutput ? getOutputFromFile(processBuilder.redirectOutput().file()) : getOutputFromStream(process.getInputStream());
final String stdErr = redirectOutput ? getOutputFromFile(processBuilder.redirectError().file()) : getOutputFromStream(process.getErrorStream());
final int exitCode = process.exitValue();
if (outputQueryResults) {
if (StringUtils.hasText(stdOut)) {
LOG.info(stdOut);
}
if (StringUtils.hasText(stdErr)) {
LOG.warn(stdErr);
}
}
if (exitCode != 0) {
if (combinedStreams) {
final String exceptionMessage = Stream.of(stdOut, stdErr)
.filter(StringUtils::hasText)
.collect(Collectors.joining("\n"));
throw new FlywayException(exceptionMessage);
}
throw new FlywayException(stdErr + " (ExitCode: " + exitCode + ")");
}
} catch (final FlywayException e) {
throw e;
} catch (final Exception e) {
throw new FlywayException(e);
}
}
public boolean checkToolInstalled(final boolean silent, final String errorMessage) {
LOG.debug("Executing " + String.join(" ", processBuilder.command()));
try {
processBuilder.start();
} catch (final Exception e) {
if (silent) {
return false;
}
if (DockerUtils.isContainer()) {
throw new FlywayException(
tool + " is not installed on this docker image. Please use the " + tool + " docker image on our repository: "
+ FlywayDbWebsiteLinks.OSS_DOCKER_REPOSITORY);
}
throw new FlywayException(errorMessage);
}
return true;
}
public void checkToolConnectivity() {
try {
final Process process = processBuilder.start();
if (!printStrings.isEmpty()) {
try (final PrintWriter writer = new PrintWriter(process.getOutputStream(), true)) {
printStrings.forEach(writer::println);
}
}
final boolean exited = process.waitFor(1, TimeUnit.MINUTES);
if (!exited) {
throw new FlywayException(tool + " connection timeout");
}
final int exitCode = process.exitValue();
if (exitCode != 0) {
throw new FlywayException(tool + " failed to connect to the provided connection URL");
}
} catch (final Exception e) {
throw new FlywayException(e.getMessage());
}
}
}