diff -r 7e08730da258 -r 4a1864c3e867 java/sql-dk/src/main/java/info/globalcode/sql/dk/InfoLister.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/java/sql-dk/src/main/java/info/globalcode/sql/dk/InfoLister.java Mon Mar 04 20:15:24 2019 +0100 @@ -0,0 +1,673 @@ +/** + * SQL-DK + * Copyright © 2013 František Kučera (frantovo.cz) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package info.globalcode.sql.dk; + +import info.globalcode.sql.dk.configuration.CommandArgument; +import info.globalcode.sql.dk.configuration.Configuration; +import info.globalcode.sql.dk.configuration.ConfigurationException; +import info.globalcode.sql.dk.configuration.ConfigurationProvider; +import info.globalcode.sql.dk.configuration.DatabaseDefinition; +import info.globalcode.sql.dk.configuration.FormatterDefinition; +import info.globalcode.sql.dk.configuration.Properties; +import info.globalcode.sql.dk.configuration.Property; +import info.globalcode.sql.dk.configuration.PropertyDeclaration; +import info.globalcode.sql.dk.configuration.TunnelDefinition; +import info.globalcode.sql.dk.formatting.ColumnsHeader; +import info.globalcode.sql.dk.formatting.CommonProperties; +import info.globalcode.sql.dk.formatting.FakeSqlArray; +import info.globalcode.sql.dk.formatting.Formatter; +import info.globalcode.sql.dk.formatting.FormatterContext; +import info.globalcode.sql.dk.formatting.FormatterException; +import java.io.BufferedReader; +import java.io.ByteArrayOutputStream; +import java.io.InputStreamReader; +import java.io.PrintStream; +import java.sql.Array; +import java.sql.Driver; +import java.sql.DriverManager; +import java.sql.DriverPropertyInfo; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.EnumSet; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.ServiceLoader; +import java.util.Set; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.logging.Level; +import java.util.logging.LogRecord; +import java.util.logging.Logger; +import javax.sql.rowset.RowSetMetaDataImpl; + +/** + * Displays info like help, version etc. + * + * @author Ing. František Kučera (frantovo.cz) + */ +public class InfoLister { + + private static final Logger log = Logger.getLogger(InfoLister.class.getName()); + /** + * Fake database name for output formatting + */ + public static final String CONFIG_DB_NAME = "sqldk_configuration"; + private final PrintStream out; + private final ConfigurationProvider configurationProvider; + private final CLIOptions options; + private Formatter formatter; + + public InfoLister(PrintStream out, ConfigurationProvider configurationProvider, CLIOptions options) { + this.out = out; + this.configurationProvider = configurationProvider; + this.options = options; + } + + public void showInfo() throws ConfigurationException, FormatterException { + EnumSet commands = options.getShowInfo(); + + boolean formattinNeeded = false; + + for (InfoType infoType : commands) { + switch (infoType) { + case CONNECTION: + case JDBC_DRIVERS: + case JDBC_PROPERTIES: + case DATABASES: + case FORMATTERS: + case FORMATTER_PROPERTIES: + case TYPES: + case JAVA_PROPERTIES: + case ENVIRONMENT_VARIABLES: + formattinNeeded = true; + break; + } + } + + if (formattinNeeded) { + try (Formatter f = getFormatter()) { + formatter = f; + formatter.writeStartBatch(); + DatabaseDefinition dd = new DatabaseDefinition(); + dd.setName(CONFIG_DB_NAME); + formatter.writeStartDatabase(dd); + showInfos(commands); + formatter.writeEndDatabase(); + formatter.writeEndBatch(); + formatter.close(); + } + } else { + showInfos(commands); + } + } + + private void showInfos(EnumSet commands) throws ConfigurationException, FormatterException { + for (InfoType infoType : commands) { + infoType.showInfo(this); + } + } + + private void listJavaProperties() throws FormatterException, ConfigurationException { + ColumnsHeader header = constructHeader(new HeaderField("name", SQLType.VARCHAR), new HeaderField("value", SQLType.VARCHAR)); + List data = new ArrayList<>(); + for (Entry e : System.getProperties().entrySet()) { + data.add(new Object[]{e.getKey(), e.getValue()}); + } + printTable(formatter, header, "-- Java system properties", null, data, 0); + } + + private void listEnvironmentVariables() throws FormatterException, ConfigurationException { + ColumnsHeader header = constructHeader(new HeaderField("name", SQLType.VARCHAR), new HeaderField("value", SQLType.VARCHAR)); + List data = new ArrayList<>(); + for (Entry e : System.getenv().entrySet()) { + data.add(new Object[]{e.getKey(), e.getValue()}); + } + printTable(formatter, header, "-- environment variables", null, data, 0); + } + + private void listFormatters() throws ConfigurationException, FormatterException { + ColumnsHeader header = constructHeader( + new HeaderField("name", SQLType.VARCHAR), + new HeaderField("built_in", SQLType.BOOLEAN), + new HeaderField("default", SQLType.BOOLEAN), + new HeaderField("class_name", SQLType.VARCHAR), + new HeaderField("valid", SQLType.BOOLEAN)); + List data = new ArrayList<>(); + + String defaultFormatter = configurationProvider.getConfiguration().getDefaultFormatter(); + defaultFormatter = defaultFormatter == null ? Configuration.DEFAULT_FORMATTER : defaultFormatter; + + for (FormatterDefinition fd : configurationProvider.getConfiguration().getBuildInFormatters()) { + data.add(new Object[]{fd.getName(), true, defaultFormatter.equals(fd.getName()), fd.getClassName(), isInstantiable(fd)}); + } + + for (FormatterDefinition fd : configurationProvider.getConfiguration().getFormatters()) { + data.add(new Object[]{fd.getName(), false, defaultFormatter.equals(fd.getName()), fd.getClassName(), isInstantiable(fd)}); + } + + printTable(formatter, header, "-- configured and built-in output formatters", null, data); + } + + private boolean isInstantiable(FormatterDefinition fd) { + try { + try (ByteArrayOutputStream testStream = new ByteArrayOutputStream()) { + fd.getInstance(new FormatterContext(testStream, new Properties(0))); + return true; + } + } catch (Exception e) { + log.log(Level.SEVERE, "Unable to create an instance of formatter: " + fd.getName(), e); + return false; + } + } + + private void listFormatterProperties() throws FormatterException, ConfigurationException { + for (String formatterName : options.getFormatterNamesToListProperties()) { + listFormatterProperties(formatterName); + } + } + + private void listFormatterProperties(String formatterName) throws FormatterException, ConfigurationException { + FormatterDefinition fd = configurationProvider.getConfiguration().getFormatter(formatterName); + try { + + // currently only for debugging purposes + // TODO: introduce --info-lister-property or generic filtering capability in printTable() ? + boolean printDeclaredIn = options.getFormatterProperties().getBoolean("InfoLister:print:declared_in", false); + + List headerFields = new ArrayList<>(); + headerFields.add(new HeaderField("name", SQLType.VARCHAR)); + headerFields.add(new HeaderField("type", SQLType.VARCHAR)); + headerFields.add(new HeaderField("default", SQLType.VARCHAR)); + headerFields.add(new HeaderField("description", SQLType.VARCHAR)); + if (printDeclaredIn) { + headerFields.add(new HeaderField("declared_in", SQLType.VARCHAR)); + } + + ColumnsHeader header = constructHeader(headerFields.toArray(new HeaderField[0])); + + Map data = new HashMap<>(); + Class formatterClass = (Class) Class.forName(fd.getClassName()); + List> hierarchy = Functions.getClassHierarchy(formatterClass, Formatter.class); + Collections.reverse(hierarchy); + hierarchy.stream().forEach((c) -> { + for (PropertyDeclaration p : Functions.getPropertyDeclarations(c)) { + data.put(p.name(), propertyDeclarationToRow(p, c, printDeclaredIn)); + } + }); + + List parameters = new ArrayList<>(); + parameters.add(new NamedParameter("formatter", formatterName, SQLType.VARCHAR)); + + printTable(formatter, header, "-- formatter properties", parameters, new ArrayList<>(data.values())); + } catch (ClassNotFoundException e) { + throw new ConfigurationException("Unable to find class " + fd.getClassName() + " of formatter" + fd.getName(), e); + } + } + + private static Object[] propertyDeclarationToRow(PropertyDeclaration p, Class formatterClass, boolean printDeclaredIn) { + List list = new ArrayList(); + + list.add(p.name()); + list.add(CommonProperties.getSimpleTypeName(p.type())); + list.add(p.defaultValue()); + list.add(p.description()); + if (printDeclaredIn) { + list.add(formatterClass.getName()); + } + + return list.toArray(); + } + + private void listTypes() throws FormatterException, ConfigurationException { + ColumnsHeader header = constructHeader(new HeaderField("name", SQLType.VARCHAR), new HeaderField("code", SQLType.INTEGER)); + List data = new ArrayList<>(); + for (SQLType sqlType : SQLType.values()) { + data.add(new Object[]{sqlType.name(), sqlType.getCode()}); + } + printTable(formatter, header, "-- data types", null, data); + log.log(Level.INFO, "Type names in --types option are case insensitive"); + } + + private void listDatabases() throws ConfigurationException, FormatterException { + ColumnsHeader header = constructHeader( + new HeaderField("database_name", SQLType.VARCHAR), + new HeaderField("user_name", SQLType.VARCHAR), + new HeaderField("database_url", SQLType.VARCHAR)); + List data = new ArrayList<>(); + + final List configuredDatabases = configurationProvider.getConfiguration().getDatabases(); + if (configuredDatabases.isEmpty()) { + log.log(Level.WARNING, "No databases are configured."); + } else { + for (DatabaseDefinition dd : configuredDatabases) { + data.add(new Object[]{dd.getName(), dd.getUserName(), dd.getUrl()}); + + final TunnelDefinition tunnel = dd.getTunnel(); + if (tunnel != null) { + log.log(Level.INFO, "Tunnel command: {0}", tunnel.getCommand()); + for (CommandArgument ca : Functions.notNull(tunnel.getArguments())) { + log.log(Level.INFO, "\targument: {0}/{1}", new Object[]{ca.getType(), ca.getValue()}); + } + } + + } + } + + printTable(formatter, header, "-- configured databases", null, data); + } + + private void listJdbcDrivers() throws FormatterException, ConfigurationException { + ColumnsHeader header = constructHeader( + new HeaderField("class", SQLType.VARCHAR), + new HeaderField("version", SQLType.VARCHAR), + new HeaderField("major", SQLType.INTEGER), + new HeaderField("minor", SQLType.INTEGER), + new HeaderField("jdbc_compliant", SQLType.BOOLEAN)); + List data = new ArrayList<>(); + + final ServiceLoader drivers = ServiceLoader.load(Driver.class); + for (Driver d : drivers) { + data.add(new Object[]{ + d.getClass().getName(), + d.getMajorVersion() + "." + d.getMinorVersion(), + d.getMajorVersion(), + d.getMinorVersion(), + d.jdbcCompliant() + }); + } + + printTable(formatter, header, "-- discovered JDBC drivers (available on the CLASSPATH)", null, data); + } + + private void listJdbcProperties() throws FormatterException, ConfigurationException { + for (String dbName : options.getDatabaseNamesToListProperties()) { + ColumnsHeader header = constructHeader( + new HeaderField("property_name", SQLType.VARCHAR), + new HeaderField("required", SQLType.BOOLEAN), + new HeaderField("choices", SQLType.ARRAY), + new HeaderField("configured_value", SQLType.VARCHAR), + new HeaderField("description", SQLType.VARCHAR)); + List data = new ArrayList<>(); + + DatabaseDefinition dd = configurationProvider.getConfiguration().getDatabase(dbName); + + Driver driver = findDriver(dd); + + if (driver == null) { + log.log(Level.WARNING, "No JDBC driver was found for DB: {0} with URL: {1}", new Object[]{dd.getName(), dd.getUrl()}); + } else { + log.log(Level.INFO, "For DB: {0} was found JDBC driver: {1}", new Object[]{dd.getName(), driver.getClass().getName()}); + + try { + DriverPropertyInfo[] propertyInfos = driver.getPropertyInfo(dd.getUrl(), dd.getProperties().getJavaProperties()); + + Set standardProperties = new HashSet<>(); + + for (DriverPropertyInfo pi : propertyInfos) { + Array choices = new FakeSqlArray(pi.choices, SQLType.VARCHAR); + data.add(new Object[]{ + pi.name, + pi.required, + choices.getArray() == null ? "" : choices, + pi.value == null ? "" : pi.value, + pi.description + }); + standardProperties.add(pi.name); + } + + for (Property p : dd.getProperties()) { + if (!standardProperties.contains(p.getName())) { + data.add(new Object[]{ + p.getName(), + "", + "", + p.getValue(), + "" + }); + log.log(Level.WARNING, "Your configuration contains property „{0}“ not declared by the JDBC driver.", p.getName()); + } + } + + } catch (SQLException e) { + log.log(Level.WARNING, "Error during getting property infos.", e); + } + + List parameters = new ArrayList<>(); + parameters.add(new NamedParameter("database", dbName, SQLType.VARCHAR)); + parameters.add(new NamedParameter("driver_class", driver.getClass().getName(), SQLType.VARCHAR)); + parameters.add(new NamedParameter("driver_major_version", driver.getMajorVersion(), SQLType.INTEGER)); + parameters.add(new NamedParameter("driver_minor_version", driver.getMinorVersion(), SQLType.INTEGER)); + + printTable(formatter, header, "-- configured and configurable JDBC driver properties", parameters, data); + } + } + + } + + private Driver findDriver(DatabaseDefinition dd) { + final ServiceLoader drivers = ServiceLoader.load(Driver.class); + for (Driver d : drivers) { + try { + if (d.acceptsURL(dd.getUrl())) { + return d; + } + } catch (SQLException e) { + log.log(Level.WARNING, "Error during finding JDBC driver for: " + dd.getName(), e); + } + } + return null; + } + + /** + * Parallelism for connection testing – maximum concurrent database connections. + */ + private static final int TESTING_THREAD_COUNT = 64; + /** + * Time limit for all connection testing threads – particular timeouts per connection will be + * much smaller. + */ + private static final long TESTING_AWAIT_LIMIT = 1; + private static final TimeUnit TESTING_AWAIT_UNIT = TimeUnit.DAYS; + + private void testConnections() throws FormatterException, ConfigurationException { + ColumnsHeader header = constructHeader( + new HeaderField("database_name", SQLType.VARCHAR), + new HeaderField("configured", SQLType.BOOLEAN), + new HeaderField("connected", SQLType.BOOLEAN), + new HeaderField("product_name", SQLType.VARCHAR), + new HeaderField("product_version", SQLType.VARCHAR)); + + log.log(Level.FINE, "Testing DB connections in {0} threads", TESTING_THREAD_COUNT); + + ExecutorService es = Executors.newFixedThreadPool(TESTING_THREAD_COUNT); + + final Formatter currentFormatter = formatter; + + printHeader(currentFormatter, header, "-- database configuration and connectivity test", null); + + for (final String dbName : options.getDatabaseNamesToTest()) { + preloadDriver(dbName); + } + + for (final String dbName : options.getDatabaseNamesToTest()) { + es.submit(() -> { + final Object[] row = testConnection(dbName); + synchronized (currentFormatter) { + printRow(currentFormatter, row); + } + } + ); + } + + es.shutdown(); + + try { + log.log(Level.FINEST, "Waiting for test results: {0} {1}", new Object[]{TESTING_AWAIT_LIMIT, TESTING_AWAIT_UNIT.name()}); + boolean finished = es.awaitTermination(TESTING_AWAIT_LIMIT, TESTING_AWAIT_UNIT); + if (finished) { + log.log(Level.FINEST, "All testing threads finished in time limit."); + } else { + throw new FormatterException("Exceeded total time limit for test threads – this should never happen"); + } + } catch (InterruptedException e) { + throw new FormatterException("Interrupted while waiting for test results", e); + } + + printFooter(currentFormatter); + } + + /** + * JDBC driver classes should be preloaded in single thread to avoid deadlocks while doing + * {@linkplain DriverManager#registerDriver(java.sql.Driver)} during parallel connections. + * + * @param dbName + */ + private void preloadDriver(String dbName) { + try { + DatabaseDefinition dd = configurationProvider.getConfiguration().getDatabase(dbName); + Driver driver = findDriver(dd); + if (driver == null) { + log.log(Level.WARNING, "No Driver found for DB: {0}", dbName); + } else { + log.log(Level.FINEST, "Driver preloading for DB: {0} was successfull", dbName); + } + } catch (Exception e) { + LogRecord r = new LogRecord(Level.WARNING, "Failed to preload the Driver for DB: {0}"); + r.setParameters(new Object[]{dbName}); + r.setThrown(e); + log.log(r); + } + } + + private Object[] testConnection(String dbName) { + log.log(Level.FINE, "Testing connection to database: {0}", dbName); + + boolean succesfullyConnected = false; + boolean succesfullyConfigured = false; + String productName = null; + String productVersion = null; + + try { + DatabaseDefinition dd = configurationProvider.getConfiguration().getDatabase(dbName); + log.log(Level.FINE, "Database definition was loaded from configuration"); + succesfullyConfigured = true; + try (DatabaseConnection dc = dd.connect(options.getDatabaseProperties())) { + succesfullyConnected = dc.test(); + productName = dc.getProductName(); + productVersion = dc.getProductVersion(); + } + log.log(Level.FINE, "Database connection test was successful"); + } catch (ConfigurationException | SQLException | RuntimeException e) { + log.log(Level.SEVERE, "Error during testing connection " + dbName, e); + } + + return new Object[]{dbName, succesfullyConfigured, succesfullyConnected, productName, productVersion}; + } + + private void printResource(String fileName) { + try (BufferedReader reader = new BufferedReader(new InputStreamReader(getClass().getClassLoader().getResourceAsStream(fileName)))) { + while (true) { + String line = reader.readLine(); + if (line == null) { + break; + } else { + println(line); + } + } + } catch (Exception e) { + log.log(Level.SEVERE, "Unable to print this info. Please see our website for it: " + Constants.WEBSITE, e); + } + } + + private void println(String line) { + out.println(line); + } + + private void printTable(Formatter formatter, ColumnsHeader header, String sql, List parameters, List data) throws ConfigurationException, FormatterException { + printTable(formatter, header, sql, parameters, data, null); + } + + private void printTable(Formatter formatter, ColumnsHeader header, String sql, List parameters, List data, final Integer sortByColumn) throws ConfigurationException, FormatterException { + printHeader(formatter, header, sql, parameters); + + if (sortByColumn != null) { + Collections.sort(data, new Comparator() { + + @Override + public int compare(Object[] o1, Object[] o2) { + String s1 = String.valueOf(o1[sortByColumn]); + String s2 = String.valueOf(o2[sortByColumn]); + return s1.compareTo(s2); + } + }); + } + + for (Object[] row : data) { + printRow(formatter, row); + } + + printFooter(formatter); + } + + private void printHeader(Formatter formatter, ColumnsHeader header, String sql, List parameters) { + formatter.writeStartStatement(); + if (sql != null) { + formatter.writeQuery(sql); + if (parameters != null) { + formatter.writeParameters(parameters); + } + } + formatter.writeStartResultSet(header); + } + + private void printRow(Formatter formatter, Object[] row) { + formatter.writeStartRow(); + for (Object cell : row) { + formatter.writeColumnValue(cell); + } + formatter.writeEndRow(); + } + + private void printFooter(Formatter formatter) { + formatter.writeEndResultSet(); + formatter.writeEndStatement(); + } + + private Formatter getFormatter() throws ConfigurationException, FormatterException { + String formatterName = options.getFormatterName(); + formatterName = formatterName == null ? Configuration.DEFAULT_FORMATTER_PREFETCHING : formatterName; + FormatterDefinition fd = configurationProvider.getConfiguration().getFormatter(formatterName); + FormatterContext context = new FormatterContext(out, options.getFormatterProperties()); + return fd.getInstance(context); + } + + private ColumnsHeader constructHeader(HeaderField... fields) throws FormatterException { + try { + RowSetMetaDataImpl metaData = new RowSetMetaDataImpl(); + metaData.setColumnCount(fields.length); + + for (int i = 0; i < fields.length; i++) { + HeaderField hf = fields[i]; + int sqlIndex = i + 1; + metaData.setColumnName(sqlIndex, hf.name); + metaData.setColumnLabel(sqlIndex, hf.name); + metaData.setColumnType(sqlIndex, hf.type.getCode()); + metaData.setColumnTypeName(sqlIndex, hf.type.name()); + } + + return new ColumnsHeader(metaData); + } catch (SQLException e) { + throw new FormatterException("Error while constructing table headers", e); + } + } + + private static class HeaderField { + + String name; + SQLType type; + + public HeaderField(String name, SQLType type) { + this.name = name; + this.type = type; + } + } + + public enum InfoType { + + HELP { + @Override + public void showInfo(InfoLister infoLister) { + infoLister.printResource(Constants.HELP_FILE); + } + }, + VERSION { + @Override + public void showInfo(InfoLister infoLister) { + infoLister.printResource(Constants.VERSION_FILE); + } + }, + LICENSE { + @Override + public void showInfo(InfoLister infoLister) { + infoLister.printResource(Constants.LICENSE_FILE); + } + }, + JAVA_PROPERTIES { + @Override + public void showInfo(InfoLister infoLister) throws FormatterException, ConfigurationException { + infoLister.listJavaProperties(); + } + }, + ENVIRONMENT_VARIABLES { + @Override + public void showInfo(InfoLister infoLister) throws FormatterException, ConfigurationException { + infoLister.listEnvironmentVariables(); + } + }, + FORMATTERS { + @Override + public void showInfo(InfoLister infoLister) throws FormatterException, ConfigurationException { + infoLister.listFormatters(); + } + }, + FORMATTER_PROPERTIES { + @Override + public void showInfo(InfoLister infoLister) throws FormatterException, ConfigurationException { + infoLister.listFormatterProperties(); + } + }, + TYPES { + @Override + public void showInfo(InfoLister infoLister) throws FormatterException, ConfigurationException { + infoLister.listTypes(); + } + }, + JDBC_DRIVERS { + @Override + public void showInfo(InfoLister infoLister) throws ConfigurationException, FormatterException { + infoLister.listJdbcDrivers(); + } + }, + JDBC_PROPERTIES { + @Override + public void showInfo(InfoLister infoLister) throws ConfigurationException, FormatterException { + infoLister.listJdbcProperties(); + } + }, + DATABASES { + @Override + public void showInfo(InfoLister infoLister) throws FormatterException, ConfigurationException { + infoLister.listDatabases(); + } + }, + CONNECTION { + @Override + public void showInfo(InfoLister infoLister) throws FormatterException, ConfigurationException { + infoLister.testConnections(); + } + }; + + public abstract void showInfo(InfoLister infoLister) throws ConfigurationException, FormatterException; + } +}