DeployConfRunner.java
/**
* Copyright (c) 2013-2017 Polago AB
* All rights reserved.
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
* LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
* OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package org.polago.deployconf;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.channels.Channels;
import java.nio.channels.FileChannel;
import java.nio.channels.ReadableByteChannel;
import java.nio.file.FileSystem;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.util.List;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.CommandLineParser;
import org.apache.commons.cli.DefaultParser;
import org.apache.commons.cli.HelpFormatter;
import org.apache.commons.cli.Option;
import org.apache.commons.cli.Options;
import org.apache.commons.cli.ParseException;
import org.jline.reader.EndOfFileException;
import org.jline.reader.UserInterruptException;
import org.polago.deployconf.group.ConfigGroupManager;
import org.polago.deployconf.group.FileSystemConfigGroupManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ch.qos.logback.classic.LoggerContext;
import ch.qos.logback.classic.joran.JoranConfigurator;
import ch.qos.logback.core.joran.spi.JoranException;
import ch.qos.logback.core.util.StatusPrinter;
/**
* Main Runner class.
*/
public class DeployConfRunner {
private static Logger logger = LoggerFactory.getLogger(DeployConfRunner.class);
/**
* Available RunModes.
*/
enum RunMode {
// Never prompt the user
NON_INTERACTIVE,
// Prompt the user for non-configured tasks
INTERACTIVE,
// Prompt the user for all tasks
FORCE_INTERACTIVE
};
/**
* The Environment Variable used to set the local repository for storing config files. This may be overridden by
* command line options.
*/
private static final String ENV_DEPLOYCONF_REPO = "DEPLOYCONF_REPO";
/**
* The default local repository relative the user's HOME directory for storing config files. This may be overridden
* by command line options.
*/
private static final String DEFAULT_DEPLOYCONF_REPO = "/.deployconf_repo";
/**
* The default deployment template path to use.
*/
private static final String DEFAULT_TEMPLATE_PATH = "META-INF/deployment-template.xml";
/**
* The deployment config file suffix to use when creating deploymentConfig paths.
*/
protected static final String DEPLOYMENT_CONFIG_SUFFIX = "deployment-config.xml";
/**
* The RunMode to use.
*/
private final RunMode runMode;
/**
* The Zip Path to the deployment template.
*/
private String deploymentTemplatePath = DEFAULT_TEMPLATE_PATH;
/**
* The explicit deployment config file to use. This is normally null.
*/
private Path deploymentConfigFile = null;
/**
* The local repository for storing deployment config files. Null means current directory.
*/
private String repositoryDirectory = null;
/**
* The Configuration Group Manager to use.
*/
private ConfigGroupManager groupManager;
/**
* Public Constructor.
*
* @param runMode how the program should interact with the user
*/
public DeployConfRunner(RunMode runMode) {
this.runMode = runMode;
}
/**
* Main entry point.
*
* @param args the runtime program arguments
*/
public static void main(String[] args) {
Options options = new Options();
Option help = new Option("h", "help", false, "Display usage information");
options.addOption(help);
Option version = new Option("v", "version", false, "Display version information and exit");
options.addOption(version);
Option interactive = new Option("i", "interactive", false, "Run in interactive mode");
options.addOption(interactive);
Option forceInteractive =
new Option("I", "force-interactive", false, "Run in interactive mode and configure all tasks");
options.addOption(forceInteractive);
Option quiet = new Option("q", "quiet", false, "Suppress most messages");
options.addOption(quiet);
Option debug = new Option("d", "debug", false, "Print Debug Information");
options.addOption(debug);
boolean debugEnabled = false;
Option repoDir = new Option("r", "repo", true, "Repository directory to use for storing deployment configs");
options.addOption(repoDir);
Option configFile =
new Option("f", "deployment-config-file", true, "File to use for storing the deployment config");
options.addOption(configFile);
Option templatePath =
new Option("t", "deployment-template-path", true, "Path to use for locating the deployment template in the "
+ "<INPUT> file. Default is '" + DEFAULT_TEMPLATE_PATH + "'");
options.addOption(templatePath);
CommandLineParser parser = new DefaultParser();
try {
CommandLine cmd = parser.parse(options, args);
ProjectProperties projectProperties = getProjectProperties();
if (cmd.hasOption(version.getOpt())) {
System.out.print(projectProperties.getName());
System.out.print(" version ");
System.out.println(projectProperties.getVersion());
System.out.println(projectProperties.getCopyrightMessage());
System.exit(0);
}
if (cmd.hasOption(help.getOpt())) {
HelpFormatter formatter = new HelpFormatter();
formatter.printHelp(projectProperties.getName() + " [OPTION]... <INPUT> <OUTPUT>",
projectProperties.getHelpHeader(), options, "");
System.exit(0);
}
if (cmd.hasOption(debug.getOpt())) {
logger.info("Activating Debug Logging");
debugEnabled = true;
setLogConfig("logback-debug.xml");
} else if (cmd.hasOption(quiet.getOpt())) {
setLogConfig("logback-quiet.xml");
}
RunMode mode = RunMode.NON_INTERACTIVE;
if (cmd.hasOption(forceInteractive.getOpt())) {
mode = RunMode.FORCE_INTERACTIVE;
} else if (cmd.hasOption(interactive.getOpt())) {
mode = RunMode.INTERACTIVE;
}
DeployConfRunner instance = new DeployConfRunner(mode);
String envRepoDir = instance.getRepositoryDirectoryFromEnvironment();
if (cmd.hasOption(repoDir.getOpt())) {
String rd = cmd.getOptionValue(repoDir.getOpt());
logger.debug("Using repository directory: {}", rd);
instance.setRepositoryDirectory(rd);
} else if (envRepoDir != null) {
logger.debug("Using repository directory from environment {}: {}", ENV_DEPLOYCONF_REPO, envRepoDir);
instance.setRepositoryDirectory(envRepoDir);
} else {
String rd = getDefaultRepository();
instance.setRepositoryDirectory(rd);
logger.debug("Using default repository directory: {}", rd);
}
Path repo = FileSystems.getDefault().getPath(instance.getRepositoryDirectory());
if (!Files.exists(repo)) {
Files.createDirectories(repo);
} else if (!Files.isDirectory(repo)) {
logger.error("Specified repository is not a directory: {}", repo);
System.exit(1);
}
instance.setGroupManager(new FileSystemConfigGroupManager(Paths.get(instance.getRepositoryDirectory())));
if (cmd.hasOption(configFile.getOpt())) {
String f = cmd.getOptionValue(configFile.getOpt());
logger.debug("Using explicit deployment file: {}", f);
instance.setDeploymentConfigPath(FileSystems.getDefault().getPath(f));
}
if (cmd.hasOption(templatePath.getOpt())) {
String path = cmd.getOptionValue(templatePath.getOpt());
logger.debug("Using deployment template path: {}", path);
instance.setDeploymentTemplatePath(path);
}
List<String> argList = cmd.getArgList();
if (argList.size() != 2) {
System.out.println("usage: " + projectProperties.getName() + " <INPUT> <OUTPUT>");
System.exit(1);
}
System.exit(instance.run(argList.get(0), argList.get(1)));
} catch (ParseException e) {
logger.error("Command Line Parse Error: " + e.getMessage(), e);
System.exit(1);
} catch (UserInterruptException e) {
logger.info("Intertupted by user");
System.exit(1);
} catch (EndOfFileException e) {
logger.info("EOF detected");
System.exit(1);
} catch (Exception e) {
String msg = "Internal Error: " + e.toString();
if (!debugEnabled) {
msg += "\n(use the -d option to print stacktraces)";
}
logger.error(msg, e);
System.exit(2);
}
}
/**
* Gets the default repository directory based on the users home directory.
*
* @return the default repository for the user
*/
private static String getDefaultRepository() {
return System.getProperty("user.home") + DEFAULT_DEPLOYCONF_REPO;
}
/**
* Sets the log configuration to use.
*
* @param config the log configuration to use
*/
private static void setLogConfig(String config) {
LoggerContext loggerContext = (LoggerContext) LoggerFactory.getILoggerFactory();
try {
JoranConfigurator configurator = new JoranConfigurator();
configurator.setContext(loggerContext);
loggerContext.reset();
configurator.doConfigure(Thread.currentThread().getContextClassLoader().getResource(config));
} catch (JoranException e) {
logger.warn("Error setting log config: " + config, e);
}
StatusPrinter.printInCaseOfErrorsOrWarnings(loggerContext);
}
/**
* Run this program.
*
* @param source the input file
* @param destination the destination file
* @return the exit status
* @throws Exception indicating processing error
*/
public int run(String source, String destination) throws Exception {
int result = 0;
DeploymentConfig template = getDeploymentConfigFromZip(source);
DeploymentConfig config = null;
Path repoFile = getDeploymentConfigPath(template.getName());
boolean repoFileExists = Files.exists(repoFile);
if (repoFileExists) {
logger.info("Loading Deployment Configuration from: " + repoFile);
config = getDeploymentConfigFromPath(repoFile);
} else {
logger.info("Creating new Deployment Config: " + repoFile);
config = new DeploymentConfig();
config.setGroupManager(groupManager);
}
logger.debug("Running in mode: {}", runMode);
if (config.merge(template) && !(runMode == RunMode.FORCE_INTERACTIVE)) {
if (!repoFileExists) {
save(config);
}
apply(config, source, destination);
} else {
// Needs manual merge
boolean interactive = runMode == RunMode.INTERACTIVE || runMode == RunMode.FORCE_INTERACTIVE;
if (interactive
&& config.interactiveMerge(newInteractiveConfigurer(), runMode == RunMode.FORCE_INTERACTIVE)) {
save(config);
apply(config, source, destination);
} else {
save(config);
System.err.println("Deployment Configuration is incomplete");
System.err.println("Rerun in interactive mode " + "by using the '-i' option");
System.err.println(" or");
System.err.println(
"Edit '" + repoFile + "' and make sure that each " + "deployment property has a valid value");
result = 2;
}
}
return result;
}
/**
* Gets the deploymentTemplatePath property value.
*
* @return the current value of the deploymentTemplatePath property
*/
public String getDeploymentTemplatePath() {
return deploymentTemplatePath;
}
/**
* Sets the deploymentTemplatePath property.
*
* @param deploymentTemplatePath the new property value
*/
public void setDeploymentTemplatePath(String deploymentTemplatePath) {
this.deploymentTemplatePath = deploymentTemplatePath;
}
/**
* Sets the deploymentConfigFile property.
*
* @param deploymentConfigPath the new property value
*/
public void setDeploymentConfigPath(Path deploymentConfigPath) {
this.deploymentConfigFile = deploymentConfigPath;
}
/**
* Gets the Path to use for storing the DeploymentConfig.
* <p>
* Unless an explicit deployment config File is set, the File is created in the configured repository and based on
* the config name. If no repository is set, the current working directory is used.
*
* @param name the DeploymentConfig name to use
* @return the Path to the DeploymentConfig
*/
public Path getDeploymentConfigPath(String name) {
Path result = deploymentConfigFile;
String dir = getRepositoryDirectory();
if (dir == null) {
dir = "";
}
if (deploymentConfigFile != null) {
result = deploymentConfigFile;
} else {
FileSystem fs = FileSystems.getDefault();
if (name != null) {
result = fs.getPath(dir, name + "-" + DEPLOYMENT_CONFIG_SUFFIX);
} else {
result = fs.getPath(dir, DEPLOYMENT_CONFIG_SUFFIX);
}
}
return result;
}
/**
* Gets the repositoryDirectory property value.
*
* @return the current value of the repositoryDirectory property
*/
public String getRepositoryDirectory() {
return repositoryDirectory;
}
/**
* Sets the repositoryDirectory property.
*
* @param repositoryDirectory the new property value
*/
public void setRepositoryDirectory(String repositoryDirectory) {
this.repositoryDirectory = repositoryDirectory;
}
/**
* Gets the groupManager property value.
*
* @return the current value of the groupManager property
*/
public ConfigGroupManager getGroupManager() {
return groupManager;
}
/**
* Sets the groupManager property.
*
* @param groupManager the new property value
*/
public void setGroupManager(ConfigGroupManager groupManager) {
this.groupManager = groupManager;
}
/**
* Create a InteractiveConfigurer instance.
*
* @return a InteractiveConfigurer instance.
* @throws IOException indicating failure
*/
protected InteractiveConfigurer newInteractiveConfigurer() throws IOException {
return new ConsoleInteractiveConfigurer();
}
/**
* Gets the repository directory from the Environment, if possible.
*
* @return a repository directory or null indicating not set
*/
protected String getRepositoryDirectoryFromEnvironment() {
return System.getenv(ENV_DEPLOYCONF_REPO);
}
/**
* Gets the Project Properties for this program.
*
* @return the Project Properteis for this program
* @throws IOException indicating failure to load properties
*/
private static ProjectProperties getProjectProperties() throws IOException {
return ProjectProperties.instance();
}
/**
* Apply the given DeploymentConfig to the source and create the destination.
*
* @param config the DeploymentConfig to apply
* @param source the input file
* @param destination the destination file
* @throws Exception indicating processing error
*/
private void apply(DeploymentConfig config, String source, String destination) throws Exception {
FileSystem fs = FileSystems.getDefault();
Path sourceFile = fs.getPath(source);
Path destFile = fs.getPath(destination);
InputStream srcStream = null;
OutputStream destStream = null;
try {
srcStream = Files.newInputStream(sourceFile);
logger.debug("Using input file: {}", sourceFile);
destStream = Files.newOutputStream(destFile);
logger.debug("Using output file: {}", destFile);
config.apply(srcStream, destStream, getDeploymentTemplatePath());
} finally {
if (srcStream != null) {
srcStream.close();
}
if (destStream != null) {
destStream.close();
}
}
}
/**
* Save the DeploymentConfig to the configured persistent storage.
*
* @param config the DeploymentConfig to save
* @throws IOException indicating IO error
*/
private void save(DeploymentConfig config) throws IOException {
Path file = getDeploymentConfigPath(config.getName());
logger.info("Saving Deployment Configuration to '" + file + "'");
FileOutputStream os = new FileOutputStream(file.toFile());
try {
config.save(os);
} finally {
os.close();
}
}
/**
* Gets a DeploymentConfig instance from a Zip file.
*
* @param source the ZipFile to use
* @return a DeploymentConfig representation of the ReadableByteChannel
* @throws Exception indicating error
*/
private DeploymentConfig getDeploymentConfigFromZip(String source) throws Exception {
ZipFile zipFile = new ZipFile(source);
ZipEntry entry = zipFile.getEntry(deploymentTemplatePath);
if (entry == null) {
zipFile.close();
throw new IllegalArgumentException(
"No deployment template file found in file '" + source + "': " + deploymentTemplatePath);
}
InputStream is = zipFile.getInputStream(entry);
DeploymentReader reader = new DeploymentReader(is, groupManager);
DeploymentConfig result = reader.parse();
is.close();
zipFile.close();
return result;
}
/**
* Gets a DeploymentConfig instance from a Path.
*
* @param path the file to use
* @return a DeploymentConfig representation of the ReadableByteChannel
* @throws Exception indicating error
*/
private DeploymentConfig getDeploymentConfigFromPath(Path path) throws Exception {
ReadableByteChannel ch = FileChannel.open(path, StandardOpenOption.READ);
InputStream is = Channels.newInputStream(ch);
DeploymentReader reader = new DeploymentReader(is, getGroupManager());
DeploymentConfig result = reader.parse();
ch.close();
return result;
}
}