All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.liquibase.ext.tools.MongoshRunner Maven / Gradle / Ivy

The newest version!
package com.liquibase.ext.tools;

import com.datical.liquibase.ext.util.NativeRunnerUtil;
import com.liquibase.ext.config.MongoshConfiguration;
import liquibase.Scope;
import liquibase.change.core.ExecuteShellCommandChange;
import liquibase.changelog.ChangeSet;
import liquibase.database.Database;
import liquibase.exception.LiquibaseException;
import liquibase.exception.UnexpectedLiquibaseException;
import liquibase.ext.mongodb.database.MongoConnection;
import liquibase.ext.mongodb.database.MongoLiquibaseDatabase;
import liquibase.logging.Logger;
import liquibase.resource.Resource;
import liquibase.resource.ResourceAccessor;
import liquibase.servicelocator.LiquibaseService;
import liquibase.sql.Sql;
import liquibase.util.StringUtil;
import lombok.Getter;
import lombok.Setter;

import java.io.BufferedWriter;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Properties;
import java.util.ResourceBundle;
import java.util.concurrent.TimeoutException;

import static com.liquibase.ext.config.MongoshConfiguration.ConfigurationKeys.*;
import static com.datical.liquibase.ext.util.NativeRunnerUtil.getBooleanFromProperties;
import static com.datical.liquibase.ext.util.NativeRunnerUtil.validateTimeout;

@LiquibaseService(skip = true)
public class MongoshRunner extends ExecuteShellCommandChange {

    private ChangeSet changeSet;
    private Sql[] sqlStrings;
    private File outFile = null;
    private Boolean keepTempFile = false;
    private List args = new ArrayList<>();
    private String tempName;
    private String tempPath;
    private String logFile;
    private Integer timeout;
    private File mongoshExec;
    private static final String EXECUTABLE_NAME = "mongosh";
    private static final String MONGOSH_CONF = "liquibase.mongosh.conf";
    private static final ResourceBundle mongoshBundle = ResourceBundle.getBundle("liquibase/i18n/liquibase-mongosh");
    private static final String MSG_UNABLE_TO_RUN_MONGOSH = mongoshBundle.getString("unable.to.run.mongosh");

    public MongoshRunner() {
    }

    /**
     * Constructor which is normally used
     *
     * @param changeSet  The current change set
     * @param sqlStrings SQL that will be executed
     */
    public MongoshRunner(ChangeSet changeSet, Sql[] sqlStrings) {
        this.changeSet = changeSet;
        this.sqlStrings = sqlStrings;
        setTimeout("1800");
    }


    /**
     * This method creates the command line arguments that are specific to mongosh execution.
     *
     * @param database The Database we are working on
     * @return List       The command args in a List
     */
    @Override
    protected List createFinalCommandArray(Database database) {
        loadMongoshProperties();
        List commandArray = super.createFinalCommandArray(database);
        try {
            writeSqlStrings();
        } catch (Exception ioe) {
            throw new UnexpectedLiquibaseException(ioe);
        }
        if (!args.isEmpty()) {
            commandArray.addAll(Collections.unmodifiableList(args));
        }

        if (sqlStrings != null) { //if we have a script
            MongoLiquibaseDatabase mongoDatabase = (MongoLiquibaseDatabase) database;
            MongoConnection connection = (MongoConnection) mongoDatabase.getConnection();
            commandArray.add(connection.getConnectionString().getConnectionString());
            if (outFile != null) { //if we have a script
                commandArray.add("--file");
                commandArray.add(outFile.getAbsolutePath());
            } else {
                //TODO we may not need it at all
                commandArray.add("--eval");
                commandArray.add(this.sqlStrings[0].toSql());
            }
        } else {
            //otherwise, we're checking to make sure mongosh exists sending --version flag
            commandArray.add("--version");
        }

        String commandLine = StringUtil.join(commandArray, " ");

        //command could contain something like this: `mongodb://uname:pass@localhost:27017/lbcat`
        //if that's the case, don't log the exact string
        Scope.getCurrentScope().getLog(getClass()).info("mongosh command:\n" + commandLine.replaceAll("://.*:.*@", "://@"));

        return commandArray;
    }

    /**
     * Process the result.  If we get an exception and there is output from the command output stream then add
     * that to the exception message.
     *
     * @param returnCode     - it's an error code from SqlPlus DB client
     * @param errorStreamOut - error stream from processing sql
     * @param infoStreamOut  - stream from processing sql
     * @param database       - db object we use in process
     * @throws UnexpectedLiquibaseException when return code is non-zero
     */
    @Override
    protected void processResult(int returnCode, String errorStreamOut, String infoStreamOut, Database database) {
        if (logFile != null && outFile != null) {
            //TODO figure out if we can get underlying error when mongosh return error code
            //mongosh --output and --log-file arguments will only send a small subset of relevant logs to the files specified.
            //Instead of using those we need to write the output streams to the log file ourselves.
            try {
                if (!infoStreamOut.isEmpty()) {
                    Files.write(Paths.get(logFile), infoStreamOut.getBytes(), StandardOpenOption.CREATE, StandardOpenOption.APPEND);
                }
                if (!errorStreamOut.isEmpty()) {
                    Files.write(Paths.get(logFile), errorStreamOut.getBytes(), StandardOpenOption.CREATE, StandardOpenOption.APPEND);
                }
            } catch (IOException writeException) {
                throw new UnexpectedLiquibaseException(writeException);
            }
        }
        if (returnCode != 0 && !StringUtil.isEmpty(infoStreamOut)) {
            String returnString = getCommandString() + " returned a code of " + returnCode + "\n" + infoStreamOut + "Learn more at https://docs.liquibase.com/mongodb";
            throw new UnexpectedLiquibaseException(returnString);
        }
        super.processResult(returnCode, errorStreamOut, infoStreamOut, database);
    }

    private void writeSqlStrings() throws Exception {
        if (sqlStrings == null || sqlStrings.length == 0) {
            return;
        }
        final Logger log = Scope.getCurrentScope().getLog(getClass());

        log.info("Creating the mongosh run script");
        MongoshFileCreator mongoshFileCreator = new MongoshFileCreator(changeSet, tempName, tempPath, true,
                keepTempFile == null ? MongoshConfiguration.TEMP_KEEP.getDefaultValue() : keepTempFile);

        try {
            outFile = mongoshFileCreator.generateTemporaryFile(".txt");
        } catch (IOException e) {
            throw new UnexpectedLiquibaseException(e);
        }
        Path path = Paths.get(outFile.getAbsolutePath());
        try (BufferedWriter writer = Files.newBufferedWriter(path)) {
            for (Sql sql : sqlStrings) {
                String line = sql.toSql();
                line = line.replace("\r", "");
                writer.write(line);
            }
            writer.write(";\n");
        }
    }

    /**
     * Execute the mongosh command
     * The base class handles all the I/O issues from shelling out to run the command
     *
     * @param database The Database we are working against
     * @throws IllegalArgumentException when invalid arguments are found
     * @throws LiquibaseException       when any other error is encountered
     */
    @Override
    public void executeCommand(Database database) throws Exception {
        try {
            this.finalCommandArray = createFinalCommandArray(database);
            super.executeCommand(database);
        } catch (TimeoutException e) {
            try {
                Thread.sleep(10000);
            } catch (InterruptedException ie) {
                Thread.currentThread().interrupt();
            }
            processResult(0, null, null, database);
            String message = e.getMessage() + System.lineSeparator() + "Error: The mongosh executable failed to return a response with the configured timeout. " +
                    "Please check liquibase.mongosh.timeout specified in liquibase.mongosh.conf file, the LIQUIBASE_MONGOSH_TIMEOUT environment variable, " +
                    "or other config locations. Learn more at https://docs.liquibase.com/concepts/advanced/runwith.html and https://docs.liquibase.com/mongodb" + System.lineSeparator();
            Scope.getCurrentScope().getUI().sendMessage("WARNING: " + message);
            Scope.getCurrentScope().getLog(MongoshRunner.class).warning(message);
            throw new LiquibaseException(e);
        } catch (IOException e) {
            if (e.getMessage().contains("mongosh")) {
                throw new LiquibaseException(MSG_UNABLE_TO_RUN_MONGOSH, e);
            }
            throw new LiquibaseException(e);
        } catch (Exception e) {
            throw new LiquibaseException(e);
        } finally {
            if (outFile != null && outFile.exists() && keepTempFile != null && keepTempFile) {
                Scope.getCurrentScope().getLog(getClass()).info("Mongosh run script can be located at: " + outFile.getAbsolutePath());
            }
        }
    }

    /**
     * Base mongosh configuration loader.
     */
    private void loadMongoshProperties() {
        setExecutable(NativeRunnerUtil.getExecutable(EXECUTABLE_NAME));
        Properties properties = getPropertiesFromConf(MONGOSH_CONF);
        setupConfProperties(properties);
        assignPropertiesFromConfiguration();
        handleMongoShExecutable(mongoshExec);
        handleTimeout(timeout);
        logProperties();
    }

    //TODO this mostly duplicate NativeExecutorRunner's method that accepts enum, need to rework ConfigFile enum to java class that allow inheritance
    public Properties getPropertiesFromConf(String configFile) {
        Properties properties = new Properties();
        ResourceAccessor resourceAccessor = Scope.getCurrentScope().getResourceAccessor();
        InputStream is = null;
        try {
            Resource resource = resourceAccessor.get(configFile);
            if (!resource.exists()) {
                Scope.getCurrentScope().getLog(getClass()).info(String.format("No configuration file named '%s' found.", configFile));
            } else {
                Scope.getCurrentScope().getLog(getClass()).info(String.format("%s configuration file located at '%s'.", configFile, resource.getUri()));
                is = resource.openInputStream();
                properties.load(is);
            }
        } catch (IOException ioe) {
            throw new UnexpectedLiquibaseException(ioe);
        } finally {
            try {
                if (is != null) {
                    is.close();
                }
            } catch (Exception ignored) {
                //ignored
            }
        }
        return properties;
    }

    private int determineTimeout(Properties properties) {
        String timeoutString = properties.getProperty("liquibase.mongosh.timeout");
        if (timeoutString == null) {
            return -1;
        }
        try {
            return Integer.parseInt(timeoutString);
        } catch (Exception e) {
            throw new UnexpectedLiquibaseException("Invalid value '" + timeoutString +
                    "' for property 'liquibase.mongosh.timeout'. Must be a valid integer. Learn more at https://docs.liquibase.com/mongodb");
        }
    }

    private void logProperties() {
        if (keepTempFile != null) {
            Scope.getCurrentScope().getLog(getClass()).info("Executing 'mongosh' with a keep temp file value of '" + keepTempFile + "'");
        }
        if (tempPath != null) {
            Scope.getCurrentScope().getLog(getClass()).info("Executing 'mongosh' with a keep temp file path value of '" + tempPath + "'");
        }
        if (tempName != null) {
            Scope.getCurrentScope().getLog(getClass()).info("Executing 'mongosh' with a keep temp file name value of '" + tempName + "'");
        }
        if (logFile != null) {
            Scope.getCurrentScope().getLog(getClass()).info("Executing 'mongosh' with a log file value of '" + logFile + "'");
        }
    }

    private void handleArgs(String argsString) {
        if (argsString != null) {
            argsString = argsString.trim();
            Scope.getCurrentScope().getLog(getClass()).info("Executing 'mongosh' with a extra arguments of '" + argsString + "'");
            args = StringUtil.splitAndTrim(argsString, " ");
        }
    }

    private void handleTimeout(Integer timeout) {
        if (timeout != null) {
            validateTimeout(timeout);
            setTimeout(String.valueOf(timeout));
            Scope.getCurrentScope().getLog(getClass()).info("Executing 'mongosh' with a timeout of '" + timeout + "'");
        }
    }

    private void handleMongoShExecutable(File mongoshExec) {
        if (mongoshExec == null) {
            return;
        }
        if (!mongoshExec.exists()) {
            throw new UnexpectedLiquibaseException("The executable for the native executor 'mongosh' cannot be found at path '" + mongoshExec.getAbsolutePath() + "' " +
                    "as specified in the liquibase.mongosh.conf file, the LIQUIBASE_MONGOSH_* environment variables, or other config locations. " +
                    "Learn more at https://docs.liquibase.com/concepts/advanced/runwith.html and https://docs.liquibase.com/mongodb");
        }
        if (!mongoshExec.canExecute()) {
            throw new UnexpectedLiquibaseException("The 'mongosh' executable in the liquibase.mongosh.conf file at " + mongoshExec.getAbsolutePath() + " cannot be executed. Learn more at https://docs.liquibase.com/mongodb");
        }
        try {
            setExecutable(mongoshExec.getCanonicalPath());
            Scope.getCurrentScope().getLog(getClass()).info("Using the 'mongosh' executable located at:  '" + mongoshExec.getCanonicalPath() + "'");
            this.mongoshExec = mongoshExec;
        } catch (IOException ioe) {
            throw new UnexpectedLiquibaseException(ioe);
        }
    }

    private void setupConfProperties(Properties properties) {
        if (properties.containsKey(MongoshConfiguration.ConfigurationKeys.getFullKey(KEEP_TEMP_BASE))) {
            keepTempFile = getBooleanFromProperties(properties, MongoshConfiguration.ConfigurationKeys.getFullKey(KEEP_TEMP_BASE));
        }
        if (properties.containsKey(MongoshConfiguration.ConfigurationKeys.getFullKey(KEEP_TEMP_NAME))) {
            tempName = properties.getProperty(MongoshConfiguration.ConfigurationKeys.getFullKey(KEEP_TEMP_NAME));
        }
        if (properties.containsKey(MongoshConfiguration.ConfigurationKeys.getFullKey(KEEP_TEMP_PATH))) {
            tempPath = properties.getProperty(MongoshConfiguration.ConfigurationKeys.getFullKey(KEEP_TEMP_PATH));
        }
        if (properties.containsKey(MongoshConfiguration.ConfigurationKeys.getFullKey(LOG_FILE))) {
            logFile = properties.getProperty(MongoshConfiguration.ConfigurationKeys.getFullKey(LOG_FILE));
        }
        if (properties.containsKey("liquibase.mongosh.path")) {
            mongoshExec = new File(properties.getProperty("liquibase.mongosh.path"));
        }
        if (properties.containsKey("liquibase.mongosh.timeout")) {
            timeout = determineTimeout(properties);
        }
        if (properties.containsKey("liquibase.mongosh.args")) {
            handleArgs(properties.getProperty("liquibase.mongosh.args"));
        }
    }

    private void assignPropertiesFromConfiguration() {
        //Override .conf properties with cli/mvn/.properties properties.
        keepTempFile = MongoshConfiguration.TEMP_KEEP.getCurrentValue() != null ? MongoshConfiguration.TEMP_KEEP.getCurrentValue() : keepTempFile;
        tempName = MongoshConfiguration.TEMP_NAME.getCurrentValue() != null ? MongoshConfiguration.TEMP_NAME.getCurrentValue() : tempName;
        tempPath = MongoshConfiguration.TEMP_PATH.getCurrentValue() != null ? MongoshConfiguration.TEMP_PATH.getCurrentValue() : tempPath;
        logFile = MongoshConfiguration.LOG_FILE.getCurrentValue() != null ? MongoshConfiguration.LOG_FILE.getCurrentValue() : logFile;
        timeout = MongoshConfiguration.TIMEOUT.getCurrentValue() != null ? MongoshConfiguration.TIMEOUT.getCurrentValue() : timeout;
        if (MongoshConfiguration.PATH.getCurrentValue() != null) {
            mongoshExec = new File(MongoshConfiguration.PATH.getCurrentValue());
        }
        if (MongoshConfiguration.ARGS.getCurrentValue() != null) {
            handleArgs(MongoshConfiguration.ARGS.getCurrentValue());
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy