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!“
3 * Copyright © 2013 František Kučera (frantovo.cz)
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.
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.
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/>.
18 package info.globalcode.sql.dk;
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;
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;
52 * Entry point of the command line interface of SQL-DK.
54 * @author Ing. František Kučera (frantovo.cz)
56 public class CLIStarter implements ConfigurationProvider {
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;
73 public static void main(String[] args) {
74 log.log(Level.FINE, "Starting " + Constants.PROGRAM_NAME);
77 if (args.length == 0) {
78 args = new String[]{CLIParser.Tokens.INFO_HELP};
82 CLIParser parser = new CLIParser();
83 CLIOptions options = parser.parseOptions(args, System.in);
85 CLIStarter starter = new CLIStarter(options);
86 starter.installDefaultConfiguration();
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()});
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;
116 System.exit(exitCode);
119 public CLIStarter(CLIOptions options) {
120 this.options = options;
123 private void process() throws ConfigurationException, SQLException, FormatterException, BatchException {
124 MODE mode = options.getMode();
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();
138 processPrepareBatch();
141 processExecuteBatch();
144 // already done above
147 log.log(Level.SEVERE, "Unsupported mode: {0}", mode);
151 generateBashCompletion();
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());
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);
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);
173 private void processExecuteBatch() throws ConfigurationException, SQLException, FormatterException, BatchException {
174 BatchDecoder dec = new BatchDecoder();
175 Batch b = dec.decode(options.getInputStream());
177 DatabaseDefinition dd = getConfiguration().getDatabase(options.getDatabaseName());
178 FormatterDefinition fd = configuration.getFormatter(options.getFormatterName());
179 ConnectionManagement jmxBean = ManagementUtils.registerMBean(dd.getName());
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);
190 public Configuration getConfiguration() throws ConfigurationException {
191 if (configuration == null) {
192 configuration = configurationLoader.loadConfiguration();
194 return configuration;
197 private void installDefaultConfiguration() throws ConfigurationException {
198 Constants.DIR.mkdir();
200 if (Constants.CONFIG_FILE.exists()) {
201 log.log(Level.FINER, "Config file already exists: {0}", Constants.CONFIG_FILE);
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);
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.");
217 File dir = new File(Constants.DIR, "bash-completion");
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);
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());
235 log.log(Level.FINE, "Bash completion helper file was written: {0}", target);
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});
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();
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());
257 File choicesFile = new File(propertyDir, "choices");
258 try (PrintWriter fw = new PrintWriter(choicesFile)) {
259 // TODO: refactor, move
260 if (p.type() == Boolean.class) {
268 log.log(Level.FINE, "Bash completion helper files was written in: {0}", formattersDir);
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});