diff --git a/sqlcsv b/sqlcsv new file mode 100755 index 0000000..d449cb0 --- /dev/null +++ b/sqlcsv @@ -0,0 +1,1333 @@ +#!/usr/bin/env compileAndGo +# -*- coding: utf-8 mode: java -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8 +compiler=javac +mainClass=sqlcsv +compileAndGo + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.Closeable; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.FilenameFilter; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; +import java.io.Reader; +import java.io.StringReader; +import java.io.StringWriter; +import java.io.Writer; +import java.lang.reflect.InvocationTargetException; +import java.math.BigDecimal; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLClassLoader; +import java.nio.charset.Charset; +import java.sql.Clob; +import java.sql.Connection; +import java.sql.Date; +import java.sql.Driver; +import java.sql.DriverManager; +import java.sql.DriverPropertyInfo; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.ResultSetMetaData; +import java.sql.SQLException; +import java.sql.Time; +import java.sql.Timestamp; +import java.sql.Types; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.Iterator; +import java.util.List; +import java.util.Properties; +import java.util.logging.ConsoleHandler; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class sqlcsv { + static final String CLASS_NAME = sqlcsv.class.getName(); + + static final Logger log = Logger.getLogger(CLASS_NAME); + static { + ConsoleHandler handler = new ConsoleHandler(); + handler.setLevel(Level.ALL); + log.addHandler(handler); + log.setUseParentHandlers(false); + } + + static final Charset UTF8; + static { + UTF8 = Charset.forName("UTF-8"); + } + + static final String USER_CONFDIR = System.getProperty("user.home") + "/." + CLASS_NAME; + + static final String SYSTEM_CONFDIR = "/etc/" + CLASS_NAME; + + static final String DEFAULT_CONFIG = CLASS_NAME + ".properties"; + + static final String USER_CONFIG = USER_CONFDIR + "/" + DEFAULT_CONFIG, + SYSTEM_CONFIG = SYSTEM_CONFDIR + "/" + DEFAULT_CONFIG; + + // ------------------------------------------------------------------------ + public static class ResultSetHelper { + public static final int CLOBBUFFERSIZE = 2048; + + // note: we want to maintain compatibility with Java 5 VM's + // These types don't exist in Java 5 + protected static final int NVARCHAR = -9, NCHAR = -15, LONGNVARCHAR = -16, NCLOB = 2011; + + public static final String[] getColumnNames(ResultSet rs) throws SQLException { + List names = new ArrayList(); + ResultSetMetaData metadata = rs.getMetaData(); + for (int i = 0; i < metadata.getColumnCount(); i++) { + names.add(metadata.getColumnName(i + 1)); + } + String[] nameArray = new String[names.size()]; + return names.toArray(nameArray); + } + + public String[] getColumnValues(ResultSet rs) throws SQLException, IOException { + List values = new ArrayList(); + ResultSetMetaData metadata = rs.getMetaData(); + for (int i = 0; i < metadata.getColumnCount(); i++) { + values.add(getColumnValue(rs, metadata.getColumnType(i + 1), i + 1)); + } + String[] valueArray = new String[values.size()]; + return values.toArray(valueArray); + } + + private String handleObject(Object obj) { + return obj == null? "": String.valueOf(obj); + } + + private String handleBigDecimal(BigDecimal decimal) { + return decimal == null? "": decimal.toString(); + } + + private String handleLong(ResultSet rs, int columnIndex) throws SQLException { + long lv = rs.getLong(columnIndex); + return rs.wasNull()? "": Long.toString(lv); + } + + private String handleInteger(ResultSet rs, int columnIndex) throws SQLException { + int i = rs.getInt(columnIndex); + return rs.wasNull()? "": Integer.toString(i); + } + + private String handleDate(ResultSet rs, int columnIndex) throws SQLException { + java.sql.Date date = rs.getDate(columnIndex); + String value = null; + if (date != null) { + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd"); + value = dateFormat.format(date); + } + return value; + } + + private String handleTime(Time time) { + return time == null? null: time.toString(); + } + + private String handleTimestamp(Timestamp timestamp) { + SimpleDateFormat timeFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + return timestamp == null? null: timeFormat.format(timestamp); + } + + private String getColumnValue(ResultSet rs, int colType, int colIndex) throws SQLException, + IOException { + String value = ""; + switch (colType) { + case Types.BIT: + case Types.JAVA_OBJECT: + value = handleObject(rs.getObject(colIndex)); + break; + case Types.BOOLEAN: + boolean b = rs.getBoolean(colIndex); + value = Boolean.valueOf(b).toString(); + break; + case NCLOB: // todo : use rs.getNClob + case Types.CLOB: + Clob c = rs.getClob(colIndex); + if (c != null) value = read(c); + break; + case Types.BIGINT: + value = handleLong(rs, colIndex); + break; + case Types.DECIMAL: + case Types.DOUBLE: + case Types.FLOAT: + case Types.REAL: + case Types.NUMERIC: + value = handleBigDecimal(rs.getBigDecimal(colIndex)); + break; + case Types.INTEGER: + case Types.TINYINT: + case Types.SMALLINT: + value = handleInteger(rs, colIndex); + break; + case Types.DATE: + value = handleDate(rs, colIndex); + break; + case Types.TIME: + value = handleTime(rs.getTime(colIndex)); + break; + case Types.TIMESTAMP: + value = handleTimestamp(rs.getTimestamp(colIndex)); + break; + case NVARCHAR: // todo : use rs.getNString + case NCHAR: // todo : use rs.getNString + case LONGNVARCHAR: // todo : use rs.getNString + case Types.LONGVARCHAR: + case Types.VARCHAR: + case Types.CHAR: + value = rs.getString(colIndex); + break; + default: + value = ""; + } + if (value == null) value = ""; + return value; + + } + + private static String read(Clob c) throws SQLException, IOException { + StringBuilder sb = new StringBuilder((int)c.length()); + Reader r = c.getCharacterStream(); + char[] cbuf = new char[CLOBBUFFERSIZE]; + int n; + while ((n = r.read(cbuf, 0, cbuf.length)) != -1) { + sb.append(cbuf, 0, n); + } + return sb.toString(); + } + } + + public static class CSVWriter implements Closeable { + public static final int INITIAL_STRING_SIZE = 128; + + public static final char DEFAULT_ESCAPE_CHARACTER = '"'; + + public static final char DEFAULT_SEPARATOR = ','; + + public static final char DEFAULT_QUOTE_CHARACTER = '"'; + + public static final char NO_QUOTE_CHARACTER = '\u0000'; + + public static final char NO_ESCAPE_CHARACTER = '\u0000'; + + public static final String DEFAULT_LINE_END = "\n"; + + public static final StringBuilder quoteCsv(String s, char escapechar, char quotechar, + StringBuilder sb) { + if (sb == null) sb = new StringBuilder(INITIAL_STRING_SIZE); + for (int j = 0; j < s.length(); j++) { + char nextChar = s.charAt(j); + if (escapechar != NO_ESCAPE_CHARACTER && nextChar == quotechar) { + sb.append(escapechar).append(nextChar); + } else if (escapechar != NO_ESCAPE_CHARACTER && nextChar == escapechar) { + sb.append(escapechar).append(nextChar); + } else { + sb.append(nextChar); + } + } + return sb; + } + + public static final String quoteCsv(String s) { + StringBuilder sb = new StringBuilder(INITIAL_STRING_SIZE); + sb.append(DEFAULT_QUOTE_CHARACTER); + quoteCsv(s, DEFAULT_ESCAPE_CHARACTER, DEFAULT_QUOTE_CHARACTER, sb); + sb.append(DEFAULT_QUOTE_CHARACTER); + return sb.toString(); + } + + public Writer out; + + protected char separator; + + protected char quotechar; + + protected char escapechar; + + protected String lineEnd; + + private ResultSetHelper resultSetHelper = new ResultSetHelper(); + + public CSVWriter(Writer writer) { + this(writer, DEFAULT_SEPARATOR); + } + + public CSVWriter(Writer writer, char separator) { + this(writer, separator, DEFAULT_QUOTE_CHARACTER); + } + + public CSVWriter(Writer writer, char separator, char quotechar) { + this(writer, separator, quotechar, DEFAULT_ESCAPE_CHARACTER); + } + + public CSVWriter(Writer writer, char separator, char quotechar, char escapechar) { + this(writer, separator, quotechar, escapechar, DEFAULT_LINE_END); + } + + public CSVWriter(Writer writer, char separator, char quotechar, String lineEnd) { + this(writer, separator, quotechar, DEFAULT_ESCAPE_CHARACTER, lineEnd); + } + + public CSVWriter(Writer writer, char separator, char quotechar, char escapechar, + String lineEnd) { + this.out = writer; + this.separator = separator; + this.quotechar = quotechar; + this.escapechar = escapechar; + this.lineEnd = lineEnd; + } + + public void writeAll(List allLines) throws IOException { + for (String[] line : allLines) { + writeNext(line); + } + } + + protected void writeColumnNames(ResultSet rs) throws SQLException, IOException { + writeNext(ResultSetHelper.getColumnNames(rs)); + } + + public void writeAll(java.sql.ResultSet rs, boolean includeColumnNames) + throws SQLException, IOException { + if (includeColumnNames) writeColumnNames(rs); + while (rs.next()) { + writeNext(resultSetHelper.getColumnValues(rs)); + } + } + + public void writeNext(String[] nextLine) throws IOException { + if (nextLine == null) return; + StringBuilder sb = new StringBuilder(INITIAL_STRING_SIZE); + for (int i = 0; i < nextLine.length; i++) { + if (i != 0) sb.append(separator); + String nextElement = nextLine[i]; + if (nextElement == null) continue; + if (quotechar != NO_QUOTE_CHARACTER) sb.append(quotechar); + sb.append(stringContainsSpecialCharacters(nextElement)? processLine(nextElement) + : nextElement); + if (quotechar != NO_QUOTE_CHARACTER) sb.append(quotechar); + } + sb.append(lineEnd); + out.write(sb.toString()); + } + + protected boolean stringContainsSpecialCharacters(String line) { + return line.indexOf(quotechar) != -1 || line.indexOf(escapechar) != -1; + } + + protected StringBuilder processLine(String nextElement) { + StringBuilder sb = new StringBuilder(INITIAL_STRING_SIZE); + for (int j = 0; j < nextElement.length(); j++) { + char nextChar = nextElement.charAt(j); + if (escapechar != NO_ESCAPE_CHARACTER && nextChar == quotechar) { + sb.append(escapechar).append(nextChar); + } else if (escapechar != NO_ESCAPE_CHARACTER && nextChar == escapechar) { + sb.append(escapechar).append(nextChar); + } else { + sb.append(nextChar); + } + } + return sb; + } + + public void flush() throws IOException { + out.flush(); + } + + public void close() throws IOException { + flush(); + out.close(); + } + + public void setResultSetHelper(ResultSetHelper resultSetHelper) { + this.resultSetHelper = resultSetHelper; + } + } + + public static class SqlResultSetHelper extends ResultSetHelper { + @Override + public String[] getColumnValues(ResultSet rs) throws SQLException, IOException { + List values = new ArrayList(); + ResultSetMetaData metadata = rs.getMetaData(); + int columnCount = metadata.getColumnCount(); + for (int i = 0; i < columnCount; i++) { + values.add(getColumnValue(rs, metadata.getColumnType(i + 1), i + 1)); + } + String[] valueArray = new String[values.size()]; + return values.toArray(valueArray); + } + + private String getColumnValue(ResultSet rs, int colType, int colIndex) throws SQLException, + IOException { + String value = null; + switch (colType) { + case Types.BIT: + case Types.JAVA_OBJECT: + Object ov = rs.getObject(colIndex); + if (!rs.wasNull() && ov != null) value = String.valueOf(ov); + break; + case Types.BOOLEAN: + Boolean bv = rs.getBoolean(colIndex); + if (!rs.wasNull() && bv != null) value = Boolean.toString(bv); + break; + case NCLOB: // todo : use rs.getNClob + case Types.CLOB: + Clob cv = rs.getClob(colIndex); + if (!rs.wasNull() && cv != null) value = read(cv); + break; + case Types.BIGINT: + Long lv = rs.getLong(colIndex); + if (!rs.wasNull() && lv != null) value = Long.toString(lv); + break; + case Types.DECIMAL: + case Types.DOUBLE: + case Types.FLOAT: + case Types.REAL: + case Types.NUMERIC: + BigDecimal bdv = rs.getBigDecimal(colIndex); + if (!rs.wasNull() && bdv != null) value = bdv.toString(); + break; + case Types.INTEGER: + case Types.TINYINT: + case Types.SMALLINT: + Integer iv = rs.getInt(colIndex); + if (!rs.wasNull() && iv != null) value = Integer.toString(iv); + break; + case Types.DATE: + Date dv = rs.getDate(colIndex); + if (!rs.wasNull() && dv != null) { + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd"); + value = dateFormat.format(dv); + } + break; + case Types.TIME: + Time tv = rs.getTime(colIndex); + if (!rs.wasNull() && tv != null) value = tv.toString(); + break; + case Types.TIMESTAMP: + Timestamp tsv = rs.getTimestamp(colIndex); + if (!rs.wasNull() && tsv != null) { + SimpleDateFormat timestampFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + value = timestampFormat.format(tsv); + } + break; + case NVARCHAR: // todo : use rs.getNString + case NCHAR: // todo : use rs.getNString + case LONGNVARCHAR: // todo : use rs.getNString + case Types.LONGVARCHAR: + case Types.VARCHAR: + case Types.CHAR: + value = rs.getString(colIndex); + break; + default: + value = ""; + } + + return value; + } + + private static final String read(Clob c) throws SQLException, IOException { + StringBuilder sb = new StringBuilder((int)c.length()); + Reader r = c.getCharacterStream(); + char[] cbuf = new char[CLOBBUFFERSIZE]; + int n; + while ((n = r.read(cbuf, 0, cbuf.length)) != -1) { + sb.append(cbuf, 0, n); + } + return sb.toString(); + } + } + + public static class SqlCSVWriter extends CSVWriter { + public SqlCSVWriter(Writer writer) { + this(writer, DEFAULT_SEPARATOR); + } + + public SqlCSVWriter(Writer writer, char separator) { + this(writer, separator, DEFAULT_QUOTE_CHARACTER); + } + + public SqlCSVWriter(Writer writer, char separator, char quotechar) { + this(writer, separator, quotechar, DEFAULT_ESCAPE_CHARACTER); + } + + public SqlCSVWriter(Writer writer, char separator, char quotechar, char escapechar) { + this(writer, separator, quotechar, escapechar, DEFAULT_LINE_END); + } + + public SqlCSVWriter(Writer writer, char separator, char quotechar, String lineEnd) { + this(writer, separator, quotechar, DEFAULT_ESCAPE_CHARACTER, lineEnd); + } + + public SqlCSVWriter(Writer writer, char separator, char quotechar, char escapechar, + String lineEnd) { + super(writer, separator, quotechar, escapechar, lineEnd); + setResultSetHelper(new SqlResultSetHelper()); + } + + @Override + public void writeNext(String[] nextLine) throws IOException { + if (nextLine == null) return; + StringBuilder sb = new StringBuilder(INITIAL_STRING_SIZE); + for (int i = 0; i < nextLine.length; i++) { + if (i != 0) sb.append(separator); + + String nextElement = nextLine[i]; + if (nextElement != null) { + if (quotechar != NO_QUOTE_CHARACTER) sb.append(quotechar); + if (stringContainsSpecialCharacters(nextElement)) { + sb.append(processLine(nextElement)); + } else { + sb.append(nextElement); + } + if (quotechar != NO_QUOTE_CHARACTER) sb.append(quotechar); + } else { + sb.append("NULL"); + } + } + sb.append(lineEnd); + out.write(sb.toString()); + } + } + + // ------------------------------------------------------------------------ + static class DelegateDriver implements Driver { + private Driver driver; + + DelegateDriver(Driver driver) { + this.driver = driver; + } + + public boolean acceptsURL(String u) throws SQLException { + return driver.acceptsURL(u); + } + + public Connection connect(String u, Properties p) throws SQLException { + return driver.connect(u, p); + } + + public int getMajorVersion() { + return driver.getMajorVersion(); + } + + public int getMinorVersion() { + return driver.getMinorVersion(); + } + + public DriverPropertyInfo[] getPropertyInfo(String u, Properties p) throws SQLException { + return driver.getPropertyInfo(u, p); + } + + public boolean jdbcCompliant() { + return driver.jdbcCompliant(); + } + + public Logger getParentLogger() { + try { + return (Logger)driver.getClass().getMethod("getParentLogger").invoke(driver); + } catch (Exception e) { + Throwable t = e; + if (t instanceof InvocationTargetException) t = t.getCause(); + throw new RuntimeException(t); + } + } + } + + static final String[] SUPPORTED_DRIVERS = new String[] { + "com.mysql.jdbc.Driver", + "oracle.jdbc.OracleDriver", + "org.hsqldb.jdbcDriver", + "org.postgresql.Driver"}; + + static final class Exit extends RuntimeException { + private static final long serialVersionUID = 1L; + + public Exit(int exitcode, String errorMsg) { + this.exitcode = exitcode; + this.errorMsg = errorMsg; + } + + public Exit(String errorMsg) { + this(1, errorMsg); + } + + public Exit(int exitcode) { + this(exitcode, null); + } + + public Exit() { + this(0, null); + } + + private int exitcode; + + public int getExitcode() { + return exitcode; + } + + private String errorMsg; + + public String getErrorMsg() { + return errorMsg; + } + + public void exit(String prefix) { + StringBuilder sb = new StringBuilder(); + if (prefix != null) sb.append(prefix); + if (errorMsg != null) sb.append(errorMsg); + if (sb.length() > 0) { + System.err.println(sb.toString()); + System.err.flush(); + } + System.exit(exitcode); + } + + public void exit() { + if (exitcode == 0) exit(null); + else exit("ERROR: "); + } + } + + static final String getArg(String[] args, int index, String errorMsg) { + if (args.length > index) return args[index]; + if (errorMsg == null) return null; + throw new Exit(1, errorMsg); + } + + static final String getExceptionAndMessage(String msg, Throwable t) { + if (t == null) return msg; + StringBuilder sb = new StringBuilder(); + if (msg != null) sb.append(msg); + sb.append(t.getClass().getName()); + sb.append(": "); + sb.append(t.getMessage()); + return sb.toString(); + } + + static final String getSummary(String msg, Throwable t) { + if (t == null) return msg; + StringWriter sw = new StringWriter(); + if (msg != null) sw.append(msg); + PrintWriter pw = new PrintWriter(sw); + t.printStackTrace(pw); + pw.flush(); + return sw.toString(); + } + + static final void logIgnoredError(Throwable t) { + log.info(getExceptionAndMessage("Erreur ignorée: ", t)); + log.finer(getSummary(null, t)); + } + + static final void println(String msg) { + System.out.println(msg); + System.out.flush(); + } + + static final void die(String msg, Throwable t) { + if (msg != null) System.err.println(msg); + if (t != null) t.printStackTrace(System.err); + System.err.flush(); + System.exit(1); + } + + static final boolean strIsempty(String str) { + return str == null || str.length() == 0; + } + + static final boolean strEquals(String s1, String s2, boolean ignoreCase) { + if (ignoreCase) return s1 == s2 || (s1 != null && s1.equalsIgnoreCase(s2)); + else return s1 == s2 || (s1 != null && s1.equals(s2)); + } + + static final boolean strEquals(String[] a1, String[] a2, boolean ignoreCase) { + if (a1 == null) return a2 == null; + else if (a2 == null) return false; + if (a1.length != a2.length) return false; + for (int i = 0; i < a1.length; i++) { + if (!strEquals(a1[i], a2[i], ignoreCase)) return false; + } + return true; + } + + static final boolean strEquals(String s1, String s2) { + return strEquals(s1, s2, false); + } + + static final String strNotnull(String str) { + return str == null? "": str; + } + + static final String strSubstr(String str, int start, int end) { + if (str == null) return null; + + int l = str.length(); + if (l > 0) { + while (start < 0) + start += l; + while (end < 0) + end += l; + } else { + if (start < 0) start = 0; + if (end < 0) end = 0; + } + if (start >= l || start >= end) return ""; + if (end >= l + 1) end = l; + + return str.substring(start, end); + } + + static final String strSubstr(String str, int start) { + if (str == null) return null; + return strSubstr(str, start, str.length()); + } + + static void close(InputStream is) { + try { + if (is != null) is.close(); + } catch (IOException e) { + } + } + + // static void close(OutputStream os) { + // try { + // if (os != null) os.close(); + // } catch (IOException e) { + // } + // } + + static void close(Reader r) { + try { + if (r != null) r.close(); + } catch (IOException e) { + } + } + + static void close(Writer w) { + try { + if (w != null) w.close(); + } catch (IOException e) { + } + } + + static void close(CSVWriter csv, String output) { + try { + if (csv != null && output != null) csv.close(); + } catch (IOException e) { + } + } + + static void close(Connection conn) { + try { + if (conn != null) conn.close(); + } catch (SQLException e) { + } + } + + static void close(PreparedStatement ps) { + try { + if (ps != null) ps.close(); + } catch (SQLException e) { + } + } + + static void close(ResultSet rs) { + try { + if (rs != null) rs.close(); + } catch (SQLException e) { + } + } + + static void rollback(Connection conn) { + try { + if (!conn.getAutoCommit()) conn.rollback(); + } catch (SQLException e) { + } + } + + static final Pattern RE_COMMENT = Pattern.compile("--|#|//"); + + static String nextLine(BufferedReader inf) throws IOException { + while (true) { + String line = inf.readLine(); + if (line == null) return null; + line = line.trim(); + if (!line.equals("") && !RE_COMMENT.matcher(line).lookingAt()) return line; + } + } + + static final Pattern RE_CREATE_STMT = Pattern + .compile("(?i)create\\s+(?:or\\s+replace)?(?:procedure|trigger|function|package)"); + + static final Pattern RE_SEPARATOR = Pattern.compile(";$"); + + static String nextStatement(BufferedReader inf) throws IOException { + String line = nextLine(inf); + if (line == null) return null; + + StringBuilder sb = new StringBuilder(); + boolean first = true; + if (RE_CREATE_STMT.matcher(line).lookingAt()) { + // Ici, lire jusqu'à une ligne contenant simplement '/' + do { + if (line.equals("/")) break; + if (first) first = false; + else sb.append("\n"); + sb.append(line); + line = nextLine(inf); + } while (line != null); + } else { + // Ici, lire jusqu'à une ligne se *terminant* par ';' + do { + if (first) first = false; + else sb.append(" "); + boolean lastLine = line.endsWith(";"); + sb.append(RE_SEPARATOR.matcher(line).replaceFirst("")); + if (lastLine) break; + line = nextLine(inf); + } while (line != null); + } + + return sb.toString(); + } + + static PreparedStatement getPreparedStatement(String query, Object[] args, Connection conn) + throws SQLException { + PreparedStatement ps = conn.prepareStatement(query); + if (args != null) { + for (int i = 0; i < args.length; i++) { + Object arg = args[i]; + if (arg instanceof java.util.Date && !(arg instanceof Date) + && !(arg instanceof Time) && !(arg instanceof Timestamp)) { + // corriger les instances de java.util.Date en java.sql.Timestamp + if (arg == null || arg instanceof Timestamp) arg = (Timestamp)arg; + else arg = new Timestamp(((java.util.Date)arg).getTime()); + } + ps.setObject(i + 1, arg); + } + } + return ps; + } + + SqlCSVWriter getCSVWriter(String output, boolean append) throws FileNotFoundException { + Writer outf; + if (output != null) { + outf = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(output, append), + UTF8)); + } else { + outf = new OutputStreamWriter(System.out); + } + return new SqlCSVWriter(outf); + } + + static class Options { + public Options(boolean ignoreIoError, boolean ignoreAnyError, boolean noHeaders, + boolean append, boolean sameOutput, boolean forceHeaders, boolean outputUc) { + this.ignoreIoErrors = ignoreIoError; + this.ignoreAnyErrors = ignoreIoError; + this.noHeaders = noHeaders; + this.append = append; + this.sameOutput = sameOutput; + this.forceHeaders = forceHeaders; + this.outputUc = outputUc; + } + + public Options() { + this(false, false, false, false, false, false, false); + } + + public boolean ignoreIoErrors; + + public boolean ignoreAnyErrors; + + public boolean noHeaders; + + public boolean append; + + public boolean sameOutput; + + public boolean forceHeaders; + + public boolean outputUc; + } + + static final String[] UC_FIELDS = new String[] {"updateCount"}; + + static final Pattern RE_NAME_EXT = Pattern.compile("(.+?)(?:(\\d+))?(?:(\\.[^.]+))?"); + + void executeQueries(Connection conn, Object[] args, ArrayList infs, + ArrayList outputs, Options o) throws IOException, SQLException { + int stcount = 0; + Iterator outputIt = outputs.iterator(); + String output = null, prevOutput = null, actualOutput = null; + String[] fields = null, prevFields = null; + boolean firstWrite = true; + for (BufferedReader inf : infs) { + try { + while (true) { + String statement = nextStatement(inf); + if (statement == null) break; + stcount++; + + log.finer(statement); + try { + PreparedStatement ps = getPreparedStatement(statement, args, conn); + try { + boolean rt = ps.execute(); + while (true) { + int uc = 0; + boolean writeOutput; + if (rt) { + writeOutput = true; + } else { + uc = ps.getUpdateCount(); + if (uc == -1) break; + writeOutput = o.outputUc; + } + + SqlCSVWriter csv = null; + prevOutput = output; + prevFields = fields; + boolean sameOutput = false, append = false; + if (writeOutput) { + if (outputIt.hasNext()) output = outputIt.next(); + if (!strEquals(output, prevOutput)) { + sameOutput = false; + firstWrite = true; + actualOutput = output; + } else if (o.sameOutput) { + sameOutput = true; + append = true; + } else if (output != null) { + String dir, basename, sindex, ext; + int index = 0; + sameOutput = true; + File file = new File(actualOutput); + dir = file.getParent(); + Matcher m = RE_NAME_EXT.matcher(file.getName()); + if (m.matches()) { + basename = m.group(1); + sindex = m.group(2); + ext = m.group(3); + if (sindex != null) index = Integer.valueOf(sindex); + index++; + + StringBuilder sb = new StringBuilder(); + if (dir != null) { + sb.append(dir); + sb.append("/"); + } + sb.append(basename); + sb.append(index); + if (ext != null) sb.append(ext); + actualOutput = sb.toString(); + firstWrite = true; + sameOutput = false; + } + } + } + + log.fine("stmt#" + stcount); + ResultSet rs = null; + if (rt) { + rs = ps.getResultSet(); + fields = ResultSetHelper.getColumnNames(rs); + } else if (o.outputUc) { + fields = UC_FIELDS; + } + boolean sameFields = strEquals(fields, prevFields, true); + boolean writeHeaders = !o.noHeaders + && !(sameFields && sameOutput && !o.forceHeaders); + if (writeOutput) { + csv = getCSVWriter(actualOutput, append | o.append); + if (sameOutput && !firstWrite + && (!sameFields || o.forceHeaders)) { + csv.out.write("\n"); + } + try { + if (rt) { + csv.writeAll(rs, writeHeaders); + } else { + if (writeHeaders) csv.writeNext(UC_FIELDS); + csv.writeNext(new String[] {String.valueOf(uc)}); + } + csv.flush(); + } finally { + close(csv, actualOutput); + close(rs); + } + } + firstWrite = false; + rt = ps.getMoreResults(); + } + } finally { + close(ps); + } + } catch (SQLException e) { + if (o.ignoreAnyErrors) { + logIgnoredError(e); + } else { + rollback(conn); + throw e; + } + } + } + } catch (IOException e) { + if (o.ignoreIoErrors) { + logIgnoredError(e); + } else { + rollback(conn); + throw e; + } + } + } + if (!conn.getAutoCommit()) conn.commit(); + } + + static final Pattern RE_URL_PROP = Pattern.compile("(.+)\\.url$"); + + void run(String[] args) throws Exception { + if (args.length == 1 && strEquals(args[0], "--help")) { + println("USAGE:" // + + "\n " + + CLASS_NAME + + " [query]" + + "\n\nquery est la requête SQL à exécuter. Si query n'est pas spécifiée ou si elle" + + "\n vaut '-', la requête SQL est lue sur l'entrée standard, ou depuis un" + + "\n fichier si l'option -f est spécifiée." + + "\n\nDEMARRAGE" + + "\n\nAu démarrage, les répertoires de configuration (utilisateur ~/.sqlcsv et système" + + "\n/etc/sqlcsv) sont analysés. Les fichiers *jar situés dans ces répertoires sont" + + "\najoutés au CLASSPATH. La présence de certains fichiers est testée pour activer" + + "\néventuellement les logs détaillés." + + "\n\nOPTIONS" + + "\n -C, --config CONFIG" + + "\n Prendre les informations de connexion depuis le fichier de propriété" + + "\n spécifié. Pour l'identifiant CONN, la propriété 'CONN.url' doit exister" + + "\n dans ce fichier avec la valeur de l'url jdbc de connexion. De plus, les" + + "\n propriétés 'CONN.user' et 'CONN.password' contiennent respectivement si" + + "\n nécessaire le nom et le mot de passe de connexion. La propriété" + + "\n 'loglevel', si elle existe, est utilisée pour configurer le niveau" + + "\n d'affichage des logs, comme avec l'option --loglevel" + + "\n Si cette option n'est pas spécifiée, un fichier nommé sqlcsv.properties" + + "\n est recherché dans l'ordre: dans le répertoire courant, dans le" + + "\n répertoire de configuration utilisateur, puis dans le répertoire de" + + "\n configuration système. Si le fichier est trouvé, il est chargé" + + "\n automatiquement." + + "\n -l, --conn CONN" + + "\n Spécifier l'identifiant (ou l'url) de connexion. Cette information est" + + "\n obligatoire. Si cette option n'est pas fournie, il faut spécifier un" + + "\n fichier de configuration avec l'option -C dans lequel *une seule*" + + "\n propriété 'CONN.url' est définie." + + "\n -u, --user USER" + + "\n -p, --password PASSWORD" + + "\n Spécifier un nom de connexion et un mot de passe si l'url ne le fournit" + + "\n pas. Ces valeurs ont la priorité sur les valeurs éventuellement déjà" + + "\n présentes dans le fichier de propriété." + + "\n -f, --input INPUT" + + "\n Lire la requête depuis le fichier INPUT au lieu de la lire depuis la" + + "\n ligne de commande ou l'entrée standard. Ne pas spécifier cette option ou" + + "\n utiliser '-' pour lire depuis l'entrée standard. Cette option est" + + "\n ignorée si la requête est fournie sur la ligne de commande." + + "\n -o, --output OUTPUT" + + "\n Ecrire le résultat dans le fichier OUTPUT. Utiliser '-' pour spécifier" + + "\n la sortie standard (c'est la valeur par défaut). S'il y a plusieurs" + + "\n requêtes et que le fichier de sortie n'est pas la sortie standard," + + "\n ajouter un numéro incrémental au nom du fichier en sortie pour chaque" + + "\n requête. Sinon, il est possible de spécifier plusieurs fois cette option" + + "\n pour nommer les fichiers correspondant à chaque requête." + + "\n -t, --autocommit" + + "\n Activer le mode autocommit" + + "\n -c, --ignore-io-error" + + "\n Continuer le traitement même en cas d'erreur du système de fichiers." + + "\n Cependant le traitement s'arrête et la transaction est annulée si une" + + "\n autre erreur se produit." + + "\n -y, --ignore-any-error" + + "\n Continuer le traitement même en cas d'erreur quelconque." + + "\n -n, --no-headers" + + "\n Ne JAMAIS inclure les en-têtes dans la sortie, même avec l'option -h" + + "\n -a, --append" + + "\n Ajouter le résultat au fichier OUTPUT au lieu de l'écraser." + + "\n -A, --auto-na" + + "\n Activer les option -n -a si le fichier OUTPUT existe et qu'il est non" + + "\n vide. Le test n'est effectué que pour le premier fichier spécifié." + + "\n -s, --same-output" + + "\n Utiliser le même fichier pour écrire le résultat de toutes les requêtes." + + "\n Normalement, un numéro incrémental est ajouté au fichier en sortie si" + + "\n plusieurs requêtes sont spécifiées. Si les en-têtes sont les mêmes," + + "\n ajouter le résultat au fichier directement à la suite. Sinon, sauter une" + + "\n ligne blanche et afficher les nouveaux en-têtes." + + "\n -h, --force-headers" + + "\n En cas d'écriture du résultat de plusieurs requêtes dans un même" + + "\n fichier, ne pas tenter de concaténer les résultats même si les en-têtes" + + "\n sont les mêmes." + + "\n --uc-output" + + "\n Ajouter dans la sortie les résultat de toutes les requêtes, pas" + + "\n seulement celles de type DQML" + + "\n --loglevel LOGLEVEL" + + "\n Spécifier le niveau de logs à afficher. Les valeurs valides sont à" + + "\n choisir parmi ALL, FINEST, FINER, FINE, CONFIG, INFO, WARNING, ERROR" + + "\n La présence de certains fichiers dans les répertoires de configuration" + + "\n utilisateur ou système configure les logs avant que les options de la" + + "\n ligne de commande ne soient analysés: un fichier DEBUG fait démarrer" + + "\n l'application avec le niveau de log ALL ce qui permet de voir les logs" + + "\n concernant le chargement des jar. Un fichier SQL_DEBUG permet d'activer" + + "\n la trace de DriverManager. Exemple:" + + "\n mkdir -p ~/.sqlcsv && touch ~/.sqlcsv/{DEBUG,SQL_DEBUG}"); + return; + } + + String config = null, connid = null, user = null, password = null; + Options o = new Options(); + boolean autocommit = false, autoNa = false; + String loglevel = null; + ArrayList inputs = new ArrayList(); + ArrayList outputs = new ArrayList(); + ArrayList remainArgs = new ArrayList(); + int i = 0, max = args.length; + while (i < max) { + String arg = args[i]; + if (arg.equals("-C") || arg.equals("--config")) { + config = getArg(args, ++i, "Vous devez spécifier le fichier de configuration"); + } else if (arg.equals("-l") || arg.equals("--conn")) { + connid = getArg(args, ++i, "Vous devez spécifier l'identifiant de connexion"); + } else if (arg.equals("-u") || arg.equals("--user")) { + user = getArg(args, ++i, "Vous devez spécifier l'utilisateur de connexion"); + } else if (arg.equals("-p") || arg.equals("--password")) { + password = getArg(args, ++i, "Vous devez spécifier l'utilisateur de connexion"); + } else if (arg.equals("-f") || arg.equals("--input")) { + String input = getArg(args, ++i, "Vous devez spécifier le fichier en entrée"); + if (strEquals(input, "-") || strEquals(input, "/dev/stdin")) input = null; + inputs.add(input); + } else if (arg.equals("-o") || arg.equals("--output")) { + String output = getArg(args, ++i, "Vous devez spécifier le fichier en sortie"); + if (strEquals(output, "-") || strEquals(output, "/dev/stdout")) output = null; + outputs.add(output); + } else if (arg.equals("-t") || arg.equals("--autocommit")) { + autocommit = true; + } else if (arg.equals("-c") || arg.equals("--ignore-io-error")) { + o.ignoreIoErrors = true; + } else if (arg.equals("-y") || arg.equals("--ignore-any-error")) { + o.ignoreAnyErrors = true; + } else if (arg.equals("-n") || arg.equals("--no-headers")) { + o.noHeaders = true; + } else if (arg.equals("-a") || arg.equals("--append")) { + o.append = true; + } else if (arg.equals("-A") || arg.equals("--auto-na")) { + autoNa = true; + } else if (arg.equals("-s") || arg.equals("--same-output")) { + o.sameOutput = true; + } else if (arg.equals("-h") || arg.equals("--force-headers")) { + o.forceHeaders = true; + } else if (arg.equals("--uc-output")) { + o.outputUc = true; + } else if (arg.equals("--loglevel")) { + loglevel = getArg(args, ++i, "Vous devez spécifier le niveau de logs"); + } else if (arg.equals("--")) { + i++; + break; + } else { + remainArgs.add(arg); + } + i++; + } + args = remainArgs.toArray(new String[0]); + + if (loglevel != null) { + log.setLevel(Level.parse(loglevel.toUpperCase())); + } + + // Charger les propriétés + Properties props = null; + if (config == null && new File(DEFAULT_CONFIG).exists()) { + // essayer depuis le répertoire courant + config = DEFAULT_CONFIG; + } + if (config == null && new File(USER_CONFIG).exists()) { + // puis dans le répertoire de configuration utilisateur + config = USER_CONFIG; + } + if (config == null && new File(SYSTEM_CONFIG).exists()) { + // puis dans le répertoire de configuration système + config = SYSTEM_CONFIG; + } + if (config != null) { + log.config("Chargement des propriétés de " + config); + props = new Properties(); + FileInputStream inf = null; + try { + inf = new FileInputStream(config); + props.load(inf); + } catch (FileNotFoundException e) { + throw new Exit(config + ": Fichier introuvable"); + } finally { + close(inf); + } + } + if (props != null) { + String loglevelKey = "loglevel"; + if (props.containsKey(loglevelKey)) { + loglevel = props.getProperty(loglevelKey); + log.setLevel(Level.parse(loglevel.toUpperCase())); + } + } + + String jdbcUrl = null; + if (connid == null && props != null) { + // Essayer de deviner connid en parcourant les propriétés de props + @SuppressWarnings("unchecked") + Enumeration en = (Enumeration)props.propertyNames(); + int count = 0; + while (en.hasMoreElements()) { + String propertyName = en.nextElement(); + Matcher m = RE_URL_PROP.matcher(propertyName); + if (m.matches()) { + connid = m.group(1); + count++; + } + } + if (count > 1) { + // Si plusieurs configurations sont trouvées, ne pas en sélectionner une par + // défaut + connid = null; + } + } + if (connid != null) { + if (props != null) { + String jdbcUrlKey = connid + ".url"; + if (props.containsKey(jdbcUrlKey)) jdbcUrl = props.getProperty(jdbcUrlKey); + String userKey = connid + ".user"; + if (props.containsKey(userKey)) user = props.getProperty(userKey); + String passwordKey = connid + ".password"; + if (props.containsKey(passwordKey)) password = props.getProperty(passwordKey); + } else { + jdbcUrl = connid; + } + } + if (jdbcUrl == null) throw new Exit("Vous devez spécifier l'url de connexion"); + + // Ouvrir les fichiers en entrée + ArrayList infs = new ArrayList(); + boolean parseInputs = true; + if (args.length > 0) { + String query = args[0]; + if (!strEquals(query, "-")) { + infs.add(new BufferedReader(new StringReader(query))); + parseInputs = false; + } + // XXX analyser les arguments à transmettre à la requête + } + if (parseInputs) { + if (inputs.size() == 0) inputs.add(null); + BufferedReader inf; + for (String input : inputs) { + if (input == null) { + inf = new BufferedReader(new InputStreamReader(System.in, UTF8)); + } else { + inf = new BufferedReader( + new InputStreamReader(new FileInputStream(input), UTF8)); + } + infs.add(inf); + } + } + + // Préparer la liste des fichiers en sortie + if (outputs.size() == 0) outputs.add(null); + String output = outputs.get(0); + if (autoNa && output != null) { + if (new File(output).exists()) { + o.noHeaders = true; + o.append = true; + } + } + + // Ouvrir la connexion + log.config("Connexion à " + jdbcUrl); + Connection conn; + if (user == null && password == null) conn = DriverManager.getConnection(jdbcUrl); + else conn = DriverManager.getConnection(jdbcUrl, user, password); + conn.setAutoCommit(autocommit); + + // Exécuter les requêtes + try { + executeQueries(conn, null, infs, outputs, o); + } finally { + close(conn); + for (BufferedReader inf : infs) { + close(inf); + } + } + } + + static final FilenameFilter JAR_FILTER = new FilenameFilter() { + public boolean accept(File dir, String name) { + return name.endsWith(".jar"); + } + }; + + static final boolean findJars(File confdir, ArrayList urls) { + if (!confdir.isDirectory()) return false; + log.config("Analyse des jars de " + confdir); + boolean foundJar = false; + for (File file : confdir.listFiles(JAR_FILTER)) { + try { + log.config("Ajout de " + file); + urls.add(file.toURI().toURL()); + foundJar = true; + } catch (MalformedURLException e) { + } + } + return foundJar; + } + + public static void main(String[] args) throws Exception { + if (new File(USER_CONFDIR + "/DEBUG").exists() + || new File(SYSTEM_CONFDIR + "/DEBUG").exists()) { + log.setLevel(Level.ALL); + } + if (new File(USER_CONFDIR + "/SQL_DEBUG").exists() + || new File(SYSTEM_CONFDIR + "/SQL_DEBUG").exists()) { + DriverManager.setLogWriter(new PrintWriter(System.err)); + } + boolean shouldUpdateClasspath = false; + ArrayList urls = new ArrayList(); + shouldUpdateClasspath |= findJars(new File(USER_CONFDIR), urls); + shouldUpdateClasspath |= findJars(new File(SYSTEM_CONFDIR), urls); + if (shouldUpdateClasspath) { + ClassLoader parentLoader = Thread.currentThread().getContextClassLoader(); + ClassLoader loader = new URLClassLoader(urls.toArray(new URL[0]), parentLoader); + Thread.currentThread().setContextClassLoader(loader); + for (String driverName : SUPPORTED_DRIVERS) { + try { + @SuppressWarnings("unchecked") + Class driverClass = (Class)loader.loadClass(driverName); + DriverManager.registerDriver(new DelegateDriver(driverClass.newInstance())); + } catch (Exception e) { + } + } + } + + try { + new sqlcsv().run(args); + System.exit(0); + } catch (Throwable t) { + if (t instanceof InvocationTargetException) t = t.getCause(); + if (t instanceof Exit) ((Exit)t).exit(); + die(null, t); + } + } +}