OSDN Git Service

Fix warnings found by FindBugs
[stew/Stew4.git] / src / net / argius / stew / ui / window / ResultSetTableModel.java
1 package net.argius.stew.ui.window;
2
3 import static java.awt.EventQueue.invokeLater;
4 import static java.sql.Types.*;
5 import static java.util.Collections.nCopies;
6 import static net.argius.stew.text.TextUtilities.join;
7
8 import java.math.*;
9 import java.sql.*;
10 import java.util.*;
11 import java.util.Map.Entry;
12 import java.util.concurrent.*;
13 import java.util.regex.*;
14
15 import javax.swing.table.*;
16
17 import net.argius.stew.*;
18
19 /**
20  * The TableModel for ResultSetTable.
21  * It mainly provides to synchronize with databases.
22  */
23 final class ResultSetTableModel extends DefaultTableModel {
24
25     private static final Logger log = Logger.getLogger(ResultSetTableModel.class);
26
27     private static final long serialVersionUID = -8861356207097438822L;
28     private static final String PTN1 = "\\s*SELECT\\s.+?\\sFROM\\s+([^\\s]+).*";
29
30     private final int[] types;
31     private final String commandString;
32
33     private Connection conn;
34     private Object tableName;
35     private String[] primaryKeys;
36     private boolean updatable;
37     private boolean linkable;
38
39     ResultSetTableModel(ResultSetReference ref) throws SQLException {
40         super(0, getColumnCount(ref));
41         ResultSet rs = ref.getResultSet();
42         ColumnOrder order = ref.getOrder();
43         final String cmd = ref.getCommandString();
44         final boolean orderIsEmpty = order.size() == 0;
45         ResultSetMetaData meta = rs.getMetaData();
46         final int columnCount = getColumnCount();
47         int[] types = new int[columnCount];
48         for (int i = 0; i < columnCount; i++) {
49             final int type;
50             final String name;
51             if (orderIsEmpty) {
52                 type = meta.getColumnType(i + 1);
53                 name = meta.getColumnName(i + 1);
54             } else {
55                 type = meta.getColumnType(order.getOrder(i));
56                 name = order.getName(i);
57             }
58             types[i] = type;
59             @SuppressWarnings({"unchecked", "unused"})
60             Object o = columnIdentifiers.set(i, name);
61         }
62         this.types = types;
63         this.commandString = cmd;
64         try {
65             analyzeForLinking(rs, cmd);
66         } catch (Exception ex) {
67             log.warn(ex);
68         }
69     }
70
71     private static final class UnlinkedRow extends Vector<Object> {
72
73         UnlinkedRow(Vector<?> rowData) {
74             super(rowData);
75         }
76
77         UnlinkedRow(Object[] rowData) {
78             super(rowData.length);
79             for (final Object o : rowData) {
80                 add(o);
81             }
82         }
83
84     }
85
86     private static int getColumnCount(ResultSetReference ref) throws SQLException {
87         final int size = ref.getOrder().size();
88         return (size == 0) ? ref.getResultSet().getMetaData().getColumnCount() : size;
89     }
90
91     @Override
92     public Class<?> getColumnClass(int columnIndex) {
93         switch (types[columnIndex]) {
94             case CHAR:
95             case VARCHAR:
96             case LONGVARCHAR:
97                 return String.class;
98             case BOOLEAN:
99             case BIT:
100                 return Boolean.class;
101             case TINYINT:
102                 return Byte.class;
103             case SMALLINT:
104                 return Short.class;
105             case INTEGER:
106                 return Integer.class;
107             case BIGINT:
108                 return Long.class;
109             case REAL:
110                 return Float.class;
111             case DOUBLE:
112             case FLOAT:
113                 return Double.class;
114             case DECIMAL:
115             case NUMERIC:
116                 return BigDecimal.class;
117             default:
118                 return Object.class;
119         }
120     }
121
122     @Override
123     public boolean isCellEditable(int row, int column) {
124         if (primaryKeys == null || primaryKeys.length == 0) {
125             return false;
126         }
127         return super.isCellEditable(row, column);
128     }
129
130     @Override
131     public void setValueAt(Object newValue, int row, int column) {
132         if (!linkable) {
133             return;
134         }
135         final Object oldValue = getValueAt(row, column);
136         final boolean changed;
137         if (newValue == null) {
138             changed = (newValue != oldValue);
139         } else {
140             changed = !newValue.equals(oldValue);
141         }
142         if (changed) {
143             if (isLinkedRow(row)) {
144                 Object[] keys = columnIdentifiers.toArray();
145                 try {
146                     executeUpdate(getRowData(keys, row), keys[column], newValue);
147                 } catch (Exception ex) {
148                     log.error(ex);
149                     throw new RuntimeException(ex);
150                 }
151             } else {
152                 if (log.isTraceEnabled()) {
153                     log.debug("update unlinked row");
154                 }
155             }
156         } else {
157             if (log.isDebugEnabled()) {
158                 log.debug("skip to update");
159             }
160         }
161         super.setValueAt(newValue, row, column);
162     }
163
164     void addUnlinkedRow(Object[] rowData) {
165         addUnlinkedRow(convertToVector(rowData));
166     }
167
168     void addUnlinkedRow(Vector<?> rowData) {
169         addRow(new UnlinkedRow(rowData));
170     }
171
172     void insertUnlinkedRow(int row, Object[] rowData) {
173         insertUnlinkedRow(row, new UnlinkedRow(rowData));
174     }
175
176     void insertUnlinkedRow(int row, Vector<?> rowData) {
177         insertRow(row, new UnlinkedRow(rowData));
178     }
179
180     /**
181      * Links a row with database.
182      * @param rowIndex
183      * @return true if it successed, false if already linked
184      * @throws SQLException failed to link by SQL error
185      */
186     boolean linkRow(int rowIndex) throws SQLException {
187         if (isLinkedRow(rowIndex)) {
188             return false;
189         }
190         executeInsert(getRowData(columnIdentifiers.toArray(), rowIndex));
191         @SuppressWarnings("unchecked")
192         Vector<Object> rows = getDataVector();
193         rows.set(rowIndex, new Vector<Object>((Vector<?>)rows.get(rowIndex)));
194         return true;
195     }
196
197     /**
198      * Removes a linked row.
199      * @param rowIndex
200      * @return true if it successed, false if already linked
201      * @throws SQLException failed to link by SQL error
202      */
203     boolean removeLinkedRow(int rowIndex) throws SQLException {
204         if (!isLinkedRow(rowIndex)) {
205             return false;
206         }
207         executeDelete(getRowData(columnIdentifiers.toArray(), rowIndex));
208         super.removeRow(rowIndex);
209         return true;
210     }
211
212     private Map<Object, Object> getRowData(Object[] keys, int rowIndex) {
213         Map<Object, Object> rowData = new LinkedHashMap<Object, Object>();
214         for (int columnIndex = 0, n = keys.length; columnIndex < n; columnIndex++) {
215             rowData.put(keys[columnIndex], getValueAt(rowIndex, columnIndex));
216         }
217         return rowData;
218     }
219
220     /**
221      * Sorts this table.
222      * @param columnIndex
223      * @param descending
224      */
225     void sort(final int columnIndex, boolean descending) {
226         final int f = (descending) ? -1 : 1;
227         @SuppressWarnings("unchecked")
228         List<List<Object>> dataVector = getDataVector();
229         Collections.sort(dataVector, new RowComparator(f, columnIndex));
230     }
231
232     private static final class RowComparator implements Comparator<List<Object>> {
233     
234         private final int f;
235         private final int columnIndex;
236     
237         RowComparator(int f, int columnIndex) {
238             this.f = f;
239             this.columnIndex = columnIndex;
240         }
241     
242         @Override
243         public int compare(List<Object> row1, List<Object> row2) {
244             return c(row1, row2) * f;
245         }
246     
247         private int c(List<Object> row1, List<Object> row2) {
248             if (row1 == null || row2 == null) {
249                 return row1 == null ? row2 == null ? 0 : -1 : 1;
250             }
251             final Object o1 = row1.get(columnIndex);
252             final Object o2 = row2.get(columnIndex);
253             if (o1 == null || o2 == null) {
254                 return o1 == null ? o2 == null ? 0 : -1 : 1;
255             }
256             if (o1 instanceof Comparable<?> && o1.getClass() == o2.getClass()) {
257                 @SuppressWarnings("unchecked")
258                 Comparable<Object> c1 = (Comparable<Object>)o1;
259                 @SuppressWarnings("unchecked")
260                 Comparable<Object> c2 = (Comparable<Object>)o2;
261                 return c1.compareTo(c2);
262             }
263             return o1.toString().compareTo(o2.toString());
264         }
265
266     }
267
268     /**
269      * Checks whether this table is updatable.
270      * @return
271      */
272     boolean isUpdatable() {
273         return updatable;
274     }
275
276     /**
277      * Checks whether this table is linkable. 
278      * @return
279      */
280     boolean isLinkable() {
281         return linkable;
282     }
283
284     /**
285      * Checks whether the specified row is linked.
286      * @param rowIndex
287      * @return
288      */
289     boolean isLinkedRow(int rowIndex) {
290         return !(getDataVector().get(rowIndex) instanceof UnlinkedRow);
291     }
292
293     /**
294      * Checks whether this table has unlinked rows.
295      * @return
296      */
297     boolean hasUnlinkedRows() {
298         for (final Object row : getDataVector()) {
299             if (row instanceof UnlinkedRow) {
300                 return true;
301             }
302         }
303         return false;
304     }
305
306     /**
307      * Checks whether specified connection is same as the connection it has.
308      * @return
309      */
310     boolean isSameConnection(Connection conn) {
311         return conn == this.conn;
312     }
313
314     /**
315      * Returns the command string that creates this.
316      * @return
317      */
318     String getCommandString() {
319         return commandString;
320     }
321
322     private void executeUpdate(Map<Object, Object> keyMap, Object targetKey, Object targetValue) throws SQLException {
323         final String sql = String.format("UPDATE %s SET %s=? WHERE %s",
324                                          tableName,
325                                          quoteIfNeeds(targetKey),
326                                          toKeyPhrase(primaryKeys));
327         List<Object> a = new ArrayList<Object>();
328         a.add(targetValue);
329         for (Object pk : primaryKeys) {
330             a.add(keyMap.get(pk));
331         }
332         executeSql(sql, a.toArray());
333     }
334
335     private void executeInsert(Map<Object, Object> rowData) throws SQLException {
336         final int dataSize = rowData.size();
337         List<Object> keys = new ArrayList<Object>(dataSize);
338         List<Object> values = new ArrayList<Object>(dataSize);
339         for (Entry<?, ?> entry : rowData.entrySet()) {
340             keys.add(quoteIfNeeds(String.valueOf(entry.getKey())));
341             values.add(entry.getValue());
342         }
343         final String sql = String.format("INSERT INTO %s (%s) VALUES (%s)",
344                                          tableName,
345                                          join(",", keys),
346                                          join(",", nCopies(dataSize, "?")));
347         executeSql(sql, values.toArray());
348     }
349
350     private void executeDelete(Map<Object, Object> keyMap) throws SQLException {
351         final String sql = String.format("DELETE FROM %s WHERE %s",
352                                          tableName,
353                                          toKeyPhrase(primaryKeys));
354         List<Object> a = new ArrayList<Object>();
355         for (Object pk : primaryKeys) {
356             a.add(keyMap.get(pk));
357         }
358         executeSql(sql, a.toArray());
359     }
360
361     private void executeSql(final String sql, final Object[] parameters) throws SQLException {
362         if (log.isDebugEnabled()) {
363             log.debug("SQL: " + sql);
364             log.debug("parameters: " + Arrays.asList(parameters));
365         }
366         final CountDownLatch latch = new CountDownLatch(1);
367         final List<SQLException> errors = new ArrayList<SQLException>();
368         // asynchronous execution
369         DaemonThreadFactory.execute(new Runnable() {
370             @SuppressWarnings("synthetic-access")
371             @Override
372             public void run() {
373                 try {
374                     if (conn.isClosed()) {
375                         throw new SQLException(ResourceManager.Default.get("e.not-connect"));
376                     }
377                     final PreparedStatement stmt = conn.prepareStatement(sql);
378                     try {
379                         ValueTransporter transporter = ValueTransporter.getInstance("");
380                         int index = 0;
381                         for (Object o : parameters) {
382                             boolean isNull = false;
383                             if (o == null || String.valueOf(o).length() == 0) {
384                                 if (getColumnClass(index) != String.class) {
385                                     isNull = true;
386                                 }
387                             }
388                             ++index;
389                             if (isNull) {
390                                 stmt.setNull(index, types[index - 1]);
391                             } else {
392                                 transporter.setObject(stmt, index, o);
393                             }
394                         }
395                         final int updatedCount = stmt.executeUpdate();
396                         if (updatedCount != 1) {
397                             throw new SQLException("updated count is not 1, but " + updatedCount);
398                         }
399                     } finally {
400                         stmt.close();
401                     }
402                 } catch (SQLException ex) {
403                     log.error(ex);
404                     errors.add(ex);
405                 } catch (Throwable th) {
406                     log.error(th);
407                     SQLException ex = new SQLException();
408                     ex.initCause(th);
409                     errors.add(ex);
410                 }
411                 latch.countDown();
412             }
413         });
414         try {
415             // waits for a task to stop
416             latch.await(3L, TimeUnit.SECONDS);
417         } catch (InterruptedException ex) {
418             throw new RuntimeException(ex);
419         }
420         if (latch.getCount() != 0) {
421             DaemonThreadFactory.execute(new Runnable() {
422                 @Override
423                 public void run() {
424                     try {
425                         latch.await();
426                     } catch (InterruptedException ex) {
427                         log.warn(ex);
428                     }
429                     if (!errors.isEmpty()) {
430                         invokeLater(new Runnable() {
431                             @Override
432                             public void run() {
433                                 WindowOutputProcessor.showErrorDialog(null, errors.get(0));
434                             }
435                         });
436                     }
437                 }
438             });
439         } else if (!errors.isEmpty()) {
440             if (log.isDebugEnabled()) {
441                 for (final Exception ex : errors) {
442                     log.debug("", ex);
443                 }
444             }
445             throw errors.get(0);
446         }
447     }
448
449     private static String toKeyPhrase(Object[] keys) {
450         List<String> a = new ArrayList<String>(keys.length);
451         for (final Object key : keys) {
452             a.add(String.format("%s=?", key));
453         }
454         return join(" AND ", a);
455     }
456
457     private static String quoteIfNeeds(Object o) {
458         final String s = String.valueOf(o);
459         if (s.matches(".*\\W.*")) {
460             return String.format("\"%s\"", s);
461         }
462         return s;
463     }
464
465     private void analyzeForLinking(ResultSet rs, String cmd) throws SQLException {
466         if (rs == null) {
467             return;
468         }
469         Statement stmt = rs.getStatement();
470         if (stmt == null) {
471             return;
472         }
473         Connection conn = stmt.getConnection();
474         if (conn == null) {
475             return;
476         }
477         this.conn = conn;
478         if (conn.isReadOnly()) {
479             return;
480         }
481         final String tableName = findTableName(cmd);
482         if (tableName.length() == 0) {
483             return;
484         }
485         this.tableName = tableName;
486         this.updatable = true;
487         List<String> pkList = findPrimaryKeys(conn, tableName);
488         if (pkList.isEmpty()) {
489             return;
490         }
491         @SuppressWarnings("unchecked")
492         final Collection<Object> columnIdentifiers = this.columnIdentifiers;
493         if (!columnIdentifiers.containsAll(pkList)) {
494             return;
495         }
496         if (findUnion(cmd)) {
497             return;
498         }
499         this.primaryKeys = pkList.toArray(new String[pkList.size()]);
500         this.linkable = true;
501     }
502
503     /**
504      * Finds a table name.
505      * @param cmd command string or SQL
506      * @return table name if it found only a table, or empty string
507      */
508     static String findTableName(String cmd) {
509         if (cmd != null) {
510             StringBuilder buffer = new StringBuilder();
511             Scanner scanner = new Scanner(cmd);
512             try {
513                 while (scanner.hasNextLine()) {
514                     final String line = scanner.nextLine();
515                     buffer.append(line.replaceAll("/\\*.*?\\*/|//.*", ""));
516                     buffer.append(' ');
517                 }
518             } finally {
519                 scanner.close();
520             }
521             Pattern p = Pattern.compile(PTN1, Pattern.CASE_INSENSITIVE);
522             Matcher m = p.matcher(buffer);
523             if (m.matches()) {
524                 String afterFrom = m.group(1);
525                 String[] words = afterFrom.split("\\s");
526                 boolean foundComma = false;
527                 for (int i = 0; i < 2 && i < words.length; i++) {
528                     String word = words[i];
529                     if (word.indexOf(',') >= 0) {
530                         foundComma = true;
531                     }
532                 }
533                 if (!foundComma) {
534                     String word = words[0];
535                     if (word.matches("[A-Za-z0-9_\\.]+")) {
536                         return word;
537                     }
538                 }
539             }
540         }
541         return "";
542     }
543
544     private static List<String> findPrimaryKeys(Connection conn, String tableName) throws SQLException {
545         DatabaseMetaData dbmeta = conn.getMetaData();
546         final String cp0;
547         final String sp0;
548         final String tp0;
549         if (tableName.contains(".")) {
550             String[] splitted = tableName.split("\\.");
551             if (splitted.length >= 3) {
552                 cp0 = splitted[0];
553                 sp0 = splitted[1];
554                 tp0 = splitted[2];
555             } else {
556                 cp0 = null;
557                 sp0 = splitted[0];
558                 tp0 = splitted[1];
559             }
560         } else {
561             cp0 = null;
562             sp0 = dbmeta.getUserName();
563             tp0 = tableName;
564         }
565         final String cp;
566         final String sp;
567         final String tp;
568         if (dbmeta.storesLowerCaseIdentifiers()) {
569             cp = (cp0 == null) ? null : cp0.toLowerCase();
570             sp = (sp0 == null) ? null : sp0.toLowerCase();
571             tp = tp0.toLowerCase();
572         } else if (dbmeta.storesUpperCaseIdentifiers()) {
573             cp = (cp0 == null) ? null : cp0.toUpperCase();
574             sp = (sp0 == null) ? null : sp0.toUpperCase();
575             tp = tp0.toUpperCase();
576         } else {
577             cp = cp0;
578             sp = sp0;
579             tp = tp0;
580         }
581         if (cp == null && sp == null) {
582             return getPrimaryKeys(dbmeta, null, null, tp);
583         }
584         List<String> a = getPrimaryKeys(dbmeta, cp, sp, tp);
585         if (a.isEmpty()) {
586             return getPrimaryKeys(dbmeta, null, null, tp);
587         }
588         return a;
589     }
590
591     private static List<String> getPrimaryKeys(DatabaseMetaData dbmeta,
592                                                String catalog,
593                                                String schema,
594                                                String table) throws SQLException {
595         ResultSet rs = dbmeta.getPrimaryKeys(catalog, schema, table);
596         try {
597             List<String> pkList = new ArrayList<String>();
598             Set<String> schemaSet = new HashSet<String>();
599             while (rs.next()) {
600                 pkList.add(rs.getString(4));
601                 schemaSet.add(rs.getString(2));
602             }
603             if (schemaSet.size() != 1) {
604                 return Collections.emptyList();
605             }
606             return pkList;
607         } finally {
608             rs.close();
609         }
610     }
611
612     private static boolean findUnion(String sql) {
613         String s = sql;
614         if (s.indexOf('\'') >= 0) {
615             if (s.indexOf("\\'") >= 0) {
616                 s = s.replaceAll("\\'", "");
617             }
618             s = s.replaceAll("'[^']+'", "''");
619         }
620         StringTokenizer tokenizer = new StringTokenizer(s);
621         while (tokenizer.hasMoreTokens()) {
622             if (tokenizer.nextToken().equalsIgnoreCase("UNION")) {
623                 return true;
624             }
625         }
626         return false;
627     }
628
629 }