java/sql-dk/src/info/globalcode/sql/dk/CLIStarter.java
author František Kučera <franta-hg@frantovo.cz>
Tue, 26 Feb 2019 18:19:49 +0100
branchv_0
changeset 236 a3ec71fa8e17
parent 221 e38910065d55
permissions -rw-r--r--
Avoid reusing/rewriting the DB connection properties.
There was weird random errors while testing connection to multiple DB in parallel when one of them was meta connection to same DB connection.
Two kinds of exception: 1) missing password 2) „Passing DB password as CLI parameter is insecure!“
     1 /**
     2  * SQL-DK
     3  * Copyright © 2013 František Kučera (frantovo.cz)
     4  *
     5  * This program is free software: you can redistribute it and/or modify
     6  * it under the terms of the GNU General Public License as published by
     7  * the Free Software Foundation, either version 3 of the License, or
     8  * (at your option) any later version.
     9  *
    10  * This program is distributed in the hope that it will be useful,
    11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
    12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
    13  * GNU General Public License for more details.
    14  *
    15  * You should have received a copy of the GNU General Public License
    16  * along with this program. If not, see <http://www.gnu.org/licenses/>.
    17  */
    18 package info.globalcode.sql.dk;
    19 
    20 import info.globalcode.sql.dk.configuration.ConfigurationProvider;
    21 import info.globalcode.sql.dk.CLIOptions.MODE;
    22 import info.globalcode.sql.dk.batch.Batch;
    23 import info.globalcode.sql.dk.batch.BatchDecoder;
    24 import info.globalcode.sql.dk.batch.BatchException;
    25 import info.globalcode.sql.dk.batch.BatchEncoder;
    26 import info.globalcode.sql.dk.configuration.Configuration;
    27 import info.globalcode.sql.dk.configuration.ConfigurationException;
    28 import info.globalcode.sql.dk.configuration.DatabaseDefinition;
    29 import info.globalcode.sql.dk.configuration.FormatterDefinition;
    30 import info.globalcode.sql.dk.configuration.Loader;
    31 import info.globalcode.sql.dk.configuration.NameIdentified;
    32 import info.globalcode.sql.dk.configuration.PropertyDeclaration;
    33 import info.globalcode.sql.dk.formatting.Formatter;
    34 import info.globalcode.sql.dk.formatting.FormatterContext;
    35 import info.globalcode.sql.dk.formatting.FormatterException;
    36 import info.globalcode.sql.dk.jmx.ConnectionManagement;
    37 import info.globalcode.sql.dk.jmx.ManagementUtils;
    38 import java.io.File;
    39 import java.io.FileNotFoundException;
    40 import java.io.IOException;
    41 import java.io.PrintStream;
    42 import java.io.PrintWriter;
    43 import java.sql.SQLException;
    44 import java.util.Collection;
    45 import java.util.Collections;
    46 import java.util.List;
    47 import java.util.logging.Level;
    48 import java.util.logging.LogRecord;
    49 import java.util.logging.Logger;
    50 
    51 /**
    52  * Entry point of the command line interface of SQL-DK.
    53  *
    54  * @author Ing. František Kučera (frantovo.cz)
    55  */
    56 public class CLIStarter implements ConfigurationProvider {
    57 
    58 	// help:exit-codes
    59 	public static final int EXIT_SUCCESS = 0; // doc:success
    60 	public static final int EXIT_UNEXPECTED_ERROR = 1; // doc:unexpected error (probably bug)
    61 	// 2 is reserved: http://www.tldp.org/LDP/abs/html/exitcodes.html#EXITCODESREF
    62 	public static final int EXIT_SQL_ERROR = 3; // doc:SQL error
    63 	public static final int EXIT_CLI_PARSE_ERROR = 4; // doc:CLI options parse error
    64 	public static final int EXIT_CLI_VALIDATE_ERROR = 5; // doc:CLI options validation error
    65 	public static final int EXIT_CONFIGURATION_ERROR = 6; // doc:configuration error
    66 	public static final int EXIT_FORMATTING_ERROR = 7; // doc:formatting error
    67 	public static final int EXIT_BATCH_ERROR = 8; // doc:batch error
    68 	private static final Logger log = Logger.getLogger(CLIStarter.class.getName());
    69 	private final CLIOptions options;
    70 	private final Loader configurationLoader = new Loader();
    71 	private Configuration configuration;
    72 
    73 	public static void main(String[] args) {
    74 		log.log(Level.FINE, "Starting " + Constants.PROGRAM_NAME);
    75 		int exitCode;
    76 
    77 		if (args.length == 0) {
    78 			args = new String[]{CLIParser.Tokens.INFO_HELP};
    79 		}
    80 
    81 		try {
    82 			CLIParser parser = new CLIParser();
    83 			CLIOptions options = parser.parseOptions(args, System.in);
    84 			options.validate();
    85 			CLIStarter starter = new CLIStarter(options);
    86 			starter.installDefaultConfiguration();
    87 			starter.process();
    88 			log.log(Level.FINE, "All done");
    89 			exitCode = EXIT_SUCCESS;
    90 		} catch (CLIParserException e) {
    91 			log.log(Level.SEVERE, "Unable to parse CLI options", e);
    92 			exitCode = EXIT_CLI_PARSE_ERROR;
    93 		} catch (InvalidOptionsException e) {
    94 			log.log(Level.SEVERE, "Invalid CLI options", e);
    95 			for (InvalidOptionsException.OptionProblem p : e.getProblems()) {
    96 				LogRecord r = new LogRecord(Level.SEVERE, "Option problem: {0}");
    97 				r.setThrown(p.getException());
    98 				r.setParameters(new Object[]{p.getDescription()});
    99 				log.log(r);
   100 			}
   101 			exitCode = EXIT_CLI_VALIDATE_ERROR;
   102 		} catch (ConfigurationException e) {
   103 			log.log(Level.SEVERE, "Configuration problem", e);
   104 			exitCode = EXIT_CONFIGURATION_ERROR;
   105 		} catch (SQLException e) {
   106 			log.log(Level.SEVERE, "SQL problem", e);
   107 			exitCode = EXIT_SQL_ERROR;
   108 		} catch (FormatterException e) {
   109 			log.log(Level.SEVERE, "Formatting problem", e);
   110 			exitCode = EXIT_FORMATTING_ERROR;
   111 		} catch (BatchException e) {
   112 			log.log(Level.SEVERE, "Batch problem", e);
   113 			exitCode = EXIT_BATCH_ERROR;
   114 		}
   115 
   116 		System.exit(exitCode);
   117 	}
   118 
   119 	public CLIStarter(CLIOptions options) {
   120 		this.options = options;
   121 	}
   122 
   123 	private void process() throws ConfigurationException, SQLException, FormatterException, BatchException {
   124 		MODE mode = options.getMode();
   125 
   126 		/** Show info */
   127 		if (!options.getShowInfo().isEmpty()) {
   128 			PrintStream infoOut = mode == MODE.JUST_SHOW_INFO ? System.out : System.err;
   129 			InfoLister infoLister = new InfoLister(infoOut, this, options);
   130 			infoLister.showInfo();
   131 		}
   132 
   133 		switch (mode) {
   134 			case QUERY_NOW:
   135 				processQueryNow();
   136 				break;
   137 			case PREPARE_BATCH:
   138 				processPrepareBatch();
   139 				break;
   140 			case EXECUTE_BATCH:
   141 				processExecuteBatch();
   142 				break;
   143 			case JUST_SHOW_INFO:
   144 				// already done above
   145 				break;
   146 			default:
   147 				log.log(Level.SEVERE, "Unsupported mode: {0}", mode);
   148 				break;
   149 		}
   150 
   151 		generateBashCompletion();
   152 	}
   153 
   154 	private void processQueryNow() throws ConfigurationException, SQLException, FormatterException {
   155 		DatabaseDefinition dd = getConfiguration().getDatabase(options.getDatabaseName());
   156 		FormatterDefinition fd = configuration.getFormatter(options.getFormatterName());
   157 		ConnectionManagement jmxBean = ManagementUtils.registerMBean(dd.getName());
   158 
   159 		try (DatabaseConnection c = dd.connect(options.getDatabaseProperties(), jmxBean)) {
   160 			log.log(Level.FINE, "Database connected");
   161 			try (Formatter f = fd.getInstance(new FormatterContext(options.getOutputStream(), options.getFormatterProperties()))) {
   162 				c.executeQuery(options.getSQLCommand(), f);
   163 			}
   164 		}
   165 	}
   166 
   167 	private void processPrepareBatch() throws BatchException {
   168 		BatchEncoder enc = new BatchEncoder();
   169 		int length = enc.encode(options.getSQLCommand(), options.getOutputStream());
   170 		log.log(Level.FINE, "Prepared batch size: {0} bytes", length);
   171 	}
   172 
   173 	private void processExecuteBatch() throws ConfigurationException, SQLException, FormatterException, BatchException {
   174 		BatchDecoder dec = new BatchDecoder();
   175 		Batch b = dec.decode(options.getInputStream());
   176 
   177 		DatabaseDefinition dd = getConfiguration().getDatabase(options.getDatabaseName());
   178 		FormatterDefinition fd = configuration.getFormatter(options.getFormatterName());
   179 		ConnectionManagement jmxBean = ManagementUtils.registerMBean(dd.getName());
   180 
   181 		try (DatabaseConnection c = dd.connect(options.getDatabaseProperties(), jmxBean)) {
   182 			log.log(Level.FINE, "Database connected");
   183 			try (Formatter f = fd.getInstance(new FormatterContext(options.getOutputStream(), options.getFormatterProperties()))) {
   184 				c.executeBatch(b, f);
   185 			}
   186 		}
   187 	}
   188 
   189 	@Override
   190 	public Configuration getConfiguration() throws ConfigurationException {
   191 		if (configuration == null) {
   192 			configuration = configurationLoader.loadConfiguration();
   193 		}
   194 		return configuration;
   195 	}
   196 
   197 	private void installDefaultConfiguration() throws ConfigurationException {
   198 		Constants.DIR.mkdir();
   199 
   200 		if (Constants.CONFIG_FILE.exists()) {
   201 			log.log(Level.FINER, "Config file already exists: {0}", Constants.CONFIG_FILE);
   202 		} else {
   203 			try {
   204 				Functions.installResource(Constants.EXAMPLE_CONFIG_FILE, Constants.CONFIG_FILE);
   205 				log.log(Level.FINE, "Installing default config file: {0}", Constants.CONFIG_FILE);
   206 			} catch (IOException e) {
   207 				throw new ConfigurationException("Unable to write example configuration to " + Constants.CONFIG_FILE, e);
   208 			}
   209 		}
   210 	}
   211 
   212 	private void generateBashCompletion() {
   213 		if (configuration == null) {
   214 			log.log(Level.FINER, "Not writing Bash completion helper files. In order to generate these files please run some command which requires configuration.");
   215 		} else {
   216 			try {
   217 				File dir = new File(Constants.DIR, "bash-completion");
   218 				dir.mkdir();
   219 				writeBashCompletionHelperFile(configuration.getDatabases(), new File(dir, "databases"));
   220 				writeBashCompletionHelperFile(configuration.getAllFormatters(), new File(dir, "formatters"));
   221 				writeBashCompletionHelperFileForFormatterProperties(new File(dir, "formatter-properties"));
   222 			} catch (Exception e) {
   223 				log.log(Level.WARNING, "Unable to generate Bash completion helper files", e);
   224 			}
   225 		}
   226 	}
   227 
   228 	private void writeBashCompletionHelperFile(Collection<? extends NameIdentified> items, File target) throws FileNotFoundException {
   229 		if (Constants.CONFIG_FILE.lastModified() > target.lastModified()) {
   230 			try (PrintWriter fw = new PrintWriter(target)) {
   231 				for (NameIdentified dd : items) {
   232 					fw.println(dd.getName());
   233 				}
   234 				fw.close();
   235 				log.log(Level.FINE, "Bash completion helper file was written: {0}", target);
   236 			}
   237 		} else {
   238 			log.log(Level.FINER, "Not writing Bash completion helper file: {0} because configuration {1} has not been changed", new Object[]{target, Constants.CONFIG_FILE});
   239 		}
   240 	}
   241 
   242 	private void writeBashCompletionHelperFileForFormatterProperties(File formattersDir) throws ClassNotFoundException, FileNotFoundException {
   243 		if (Constants.CONFIG_FILE.lastModified() > formattersDir.lastModified()) {
   244 			// TODO: delete old directory
   245 			formattersDir.mkdir();
   246 			for (FormatterDefinition fd : configuration.getAllFormatters()) {
   247 				File formatterDir = new File(formattersDir, fd.getName());
   248 				formatterDir.mkdir();
   249 
   250 				Class<Formatter> formatterClass = (Class<Formatter>) Class.forName(fd.getClassName());
   251 				List<Class<? extends Formatter>> hierarchy = Functions.getClassHierarchy(formatterClass, Formatter.class);
   252 				Collections.reverse(hierarchy);
   253 				for (Class<? extends Formatter> c : hierarchy) {
   254 					for (PropertyDeclaration p : Functions.getPropertyDeclarations(c)) {
   255 						File propertyDir = new File(formatterDir, p.name());
   256 						propertyDir.mkdir();
   257 						File choicesFile = new File(propertyDir, "choices");
   258 						try (PrintWriter fw = new PrintWriter(choicesFile)) {
   259 							// TODO: refactor, move
   260 							if (p.type() == Boolean.class) {
   261 								fw.println("true");
   262 								fw.println("false");
   263 							}
   264 						}
   265 					}
   266 				}
   267 			}
   268 			log.log(Level.FINE, "Bash completion helper files was written in: {0}", formattersDir);
   269 		} else {
   270 			log.log(Level.FINER, "Not writing Bash completion helper directory: {0} because configuration {1} has not been changed", new Object[]{formattersDir, Constants.CONFIG_FILE});
   271 		}
   272 
   273 	}
   274 }