diff --git a/driverlist.csv b/driverlist.csv index cb67a151f..64b37ebb5 100644 --- a/driverlist.csv +++ b/driverlist.csv @@ -1,11 +1,11 @@ PostgreSQL;jdbc:postgresql://[:]/[];org.postgresql.Driver;lib/postgresql-42.2.16.jar -Oracle Thin;jdbc:oracle:thin:@::;oracle.jdbc.driver.OracleDriver -Oracle OCI;jdbc:oracle:oci:@;oracle.jdbc.driver.OracleDriver +Oracle Thin;jdbc:oracle:thin:@::;oracle.jdbc.driver.OracleDriver;;https://repo1.maven.org/maven2/com/oracle/database/jdbc/ojdbc8/19.7.0.0/ojdbc8-19.7.0.0.jar https://repo1.maven.org/maven2/com/oracle/database/xml/xdb/19.7.0.0/xdb-19.7.0.0.jar https://repo1.maven.org/maven2/com/oracle/database/xml/xmlparserv2/19.7.0.0/xmlparserv2-19.7.0.0.jar +Oracle OCI;jdbc:oracle:oci:@;oracle.jdbc.driver.OracleDriver;;https://repo1.maven.org/maven2/com/oracle/database/jdbc/ojdbc8/19.7.0.0/ojdbc8-19.7.0.0.jar https://repo1.maven.org/maven2/com/oracle/database/xml/xdb/19.7.0.0/xdb-19.7.0.0.jar https://repo1.maven.org/maven2/com/oracle/database/xml/xmlparserv2/19.7.0.0/xmlparserv2-19.7.0.0.jar MySQL 8;jdbc:mysql://:[/];com.mysql.jdbc.Driver;lib/mysql-connector-java-8.0.21.jar MySQL 5;jdbc:mysql://:[/];org.gjt.mm.mysql.Driver;lib/mysql-connector-java-5.1.5-bin.jar MariaDB;jdbc:mariadb://:[/];org.mariadb.jdbc.Driver;lib/mariadb-java-client-2.4.4.jar Microsoft SQL Server;jdbc:sqlserver://[\\][:];com.microsoft.sqlserver.jdbc.SQLServerDriver;lib/mssql-jdbc-7.2.1.jre8.jar -IBM Db2;jdbc:db2://:/;COM.ibm.db2.jdbc.app.DB2Driver +IBM Db2;jdbc:db2://:/;com.ibm.db2.jcc.DB2Driver;;https://repo1.maven.org/maven2/com/ibm/db2/jcc/11.5.4.0/jcc-11.5.4.0.jar SQLite;jdbc:sqlite:;org.sqlite.JDBC;lib/sqlite-jdbc-3.28.0.jar Sybase;jdbc:sybase:Tds:[:];com.sybase.jdbc3.jdbc.SybDriver Firebird;jdbc:firebirdsql://[:]/;org.firebirdsql.jdbc.FBDriver diff --git a/releasenotes.txt b/releasenotes.txt index bfc66c10d..d14e06be2 100644 --- a/releasenotes.txt +++ b/releasenotes.txt @@ -1,3 +1,10 @@ +10.1 + - Automatic download of JDBC drivers. + - Workaround for bug "JDK-8215200 : IllegalArgumentException in sun.lwawt.macosx.CPlatformWindow" + https://github.com/AdoptOpenJDK/openjdk-jdk11/issues/10 + https://bugs.java.com/bugdatabase/view_bug.do?bug_id=8215200 + https://bugs.openjdk.java.net/browse/JDK-8215200 + 10.0 - Java 7 support was dropped in favor of reduced development overheads. - Third party libraries and JDBC drivers have been upgraded accordingly. diff --git a/src/main/engine/net/sf/jailer/database/BasicDataSource.java b/src/main/engine/net/sf/jailer/database/BasicDataSource.java index 1c85d27ba..4b23bdcfe 100644 --- a/src/main/engine/net/sf/jailer/database/BasicDataSource.java +++ b/src/main/engine/net/sf/jailer/database/BasicDataSource.java @@ -21,6 +21,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; +import java.util.IdentityHashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; @@ -202,14 +203,24 @@ public class BasicDataSource implements DataSource { private static Set registeredDriverClassNames = new HashSet(); + private DriverShim currentDriver; + private static Map, DriverShim> drivers = new IdentityHashMap, BasicDataSource.DriverShim>(); + private void loadDriver(URL[] jdbcDriverURL) { ClassLoader classLoaderForJdbcDriver = addJarToClasspath(jdbcDriverURL); try { if (classLoaderForJdbcDriver != null) { - Driver d; try { - d = (Driver) Class.forName(driverClassName, true, classLoaderForJdbcDriver).newInstance(); - DriverManager.registerDriver(new DriverShim(d)); + @SuppressWarnings("unchecked") + Class driverClass = (Class) Class.forName(driverClassName, true, classLoaderForJdbcDriver); + synchronized (drivers) { + currentDriver = drivers.get(driverClass); + if (currentDriver == null) { + currentDriver = new DriverShim((Driver) driverClass.newInstance()); + drivers.put(driverClass, currentDriver); + } + } + DriverManager.registerDriver(currentDriver); registeredDriverClassNames.add(driverClassName); } catch (InstantiationException e) { throw new RuntimeException(e); @@ -349,14 +360,38 @@ public class BasicDataSource implements DataSource { for (Map.Entry entry: jdbcProperties.entrySet()) { info.put(entry.getKey(), entry.getValue()); } - con = DriverManager.getConnection(dbUrl, info); + try { + con = DriverManager.getConnection(dbUrl, info); + } catch (SQLException e3) { + if (currentDriver != null && currentDriver.acceptsURL(dbUrl)) { + con = currentDriver.connect(dbUrl, info); + } + } } catch (SQLException e2) { // ignore } } if (con == null) { - con = DriverManager.getConnection(dbUrl, dbUser, dbPassword); + try { + con = DriverManager.getConnection(dbUrl, dbUser, dbPassword); + } catch (SQLException e) { + try { + if (currentDriver != null && currentDriver.acceptsURL(dbUrl)) { + java.util.Properties info = new java.util.Properties(); + + if (dbUser != null) { + info.put("user", dbUser); + } + if (dbPassword != null) { + info.put("password", dbPassword); + } + con = currentDriver.connect(dbUrl, info); + } + } catch (SQLException e1) { + throw e; + } + } } if (maxPoolSize == 0) { diff --git a/src/main/gui/net/sf/jailer/ui/DbConnectionDetailsEditor.form b/src/main/gui/net/sf/jailer/ui/DbConnectionDetailsEditor.form index 6d37fbe90..e904ca3e0 100644 --- a/src/main/gui/net/sf/jailer/ui/DbConnectionDetailsEditor.form +++ b/src/main/gui/net/sf/jailer/ui/DbConnectionDetailsEditor.form @@ -73,16 +73,6 @@ - - - - - - - - - - @@ -395,7 +385,7 @@ - + @@ -409,7 +399,7 @@ - + @@ -459,7 +449,14 @@ - + + + + + + + + diff --git a/src/main/gui/net/sf/jailer/ui/DbConnectionDetailsEditor.java b/src/main/gui/net/sf/jailer/ui/DbConnectionDetailsEditor.java index 23bf41ffd..e70f12381 100644 --- a/src/main/gui/net/sf/jailer/ui/DbConnectionDetailsEditor.java +++ b/src/main/gui/net/sf/jailer/ui/DbConnectionDetailsEditor.java @@ -23,6 +23,7 @@ import java.awt.datatransfer.DataFlavor; import java.awt.datatransfer.StringSelection; import java.awt.datatransfer.Transferable; import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; import java.io.File; @@ -32,12 +33,15 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Function; import java.util.regex.Pattern; import java.util.stream.Collectors; import javax.swing.Icon; +import javax.swing.JButton; import javax.swing.JOptionPane; import javax.swing.JTextField; +import javax.swing.SwingUtilities; import javax.swing.Timer; import javax.swing.event.DocumentEvent; import javax.swing.event.DocumentListener; @@ -163,6 +167,9 @@ public class DbConnectionDetailsEditor extends javax.swing.JDialog { List driverURLs = retrieveDriverURLs(driverlist); downloadButton.setEnabled(driverURLs != null && Arrays.asList(jar1, jar2, jar3, jar4).stream().allMatch(f -> f.getText().trim().isEmpty())); + downloadButton.setToolTipText(driverURLs == null? null : + driverURLs.stream().collect(Collectors.joining("
", "", "")) + ); } })); if (needsTest) { @@ -270,16 +277,16 @@ public class DbConnectionDetailsEditor extends javax.swing.JDialog { } }).collect(Collectors.toList()); if (files.size() > 0) { - jar1.setText(files.get(0)); + jar1.setText(toRelFileName(files.get(0))); } if (files.size() > 1) { - jar2.setText(files.get(1)); + jar2.setText(toRelFileName(files.get(1))); } if (files.size() > 2) { - jar3.setText(files.get(2)); + jar3.setText(toRelFileName(files.get(2))); } if (files.size() > 3) { - jar4.setText(files.get(3)); + jar4.setText(toRelFileName(files.get(3))); } }); } @@ -326,7 +333,6 @@ public class DbConnectionDetailsEditor extends javax.swing.JDialog { jLabel2 = new javax.swing.JLabel(); jLabel3 = new javax.swing.JLabel(); jLabel4 = new javax.swing.JLabel(); - jLabel5 = new javax.swing.JLabel(); jLabel6 = new javax.swing.JLabel(); jLabel7 = new javax.swing.JLabel(); jLabel8 = new javax.swing.JLabel(); @@ -362,6 +368,7 @@ public class DbConnectionDetailsEditor extends javax.swing.JDialog { feedbackLabel = new javax.swing.JLabel(); jSeparator1 = new javax.swing.JSeparator(); downloadButton = new javax.swing.JButton(); + jSeparator2 = new javax.swing.JSeparator(); setDefaultCloseOperation(javax.swing.WindowConstants.DISPOSE_ON_CLOSE); setTitle("Database Connection"); @@ -395,12 +402,6 @@ public class DbConnectionDetailsEditor extends javax.swing.JDialog { gridBagConstraints.gridy = 15; jPanel1.add(jLabel4, gridBagConstraints); - jLabel5.setText(" "); - gridBagConstraints = new java.awt.GridBagConstraints(); - gridBagConstraints.gridx = 0; - gridBagConstraints.gridy = 45; - jPanel1.add(jLabel5, gridBagConstraints); - jLabel6.setText(" Driver-Class"); gridBagConstraints = new java.awt.GridBagConstraints(); gridBagConstraints.gridx = 0; @@ -640,7 +641,7 @@ public class DbConnectionDetailsEditor extends javax.swing.JDialog { jPanel5.setLayout(new java.awt.GridBagLayout()); - exportCBButton.setText("Copy credentials"); + exportCBButton.setText("Copy Credentials"); exportCBButton.setToolTipText("Copy credentials to clipboard"); exportCBButton.addActionListener(new java.awt.event.ActionListener() { public void actionPerformed(java.awt.event.ActionEvent evt) { @@ -654,7 +655,7 @@ public class DbConnectionDetailsEditor extends javax.swing.JDialog { gridBagConstraints.weightx = 1.0; jPanel5.add(exportCBButton, gridBagConstraints); - importCBButton.setText("Paste credentials"); + importCBButton.setText("Paste Credentials"); importCBButton.setToolTipText("Paste credentials from clipboard"); importCBButton.addActionListener(new java.awt.event.ActionListener() { public void actionPerformed(java.awt.event.ActionEvent evt) { @@ -718,7 +719,15 @@ public class DbConnectionDetailsEditor extends javax.swing.JDialog { gridBagConstraints.gridy = 44; gridBagConstraints.gridwidth = 3; gridBagConstraints.anchor = java.awt.GridBagConstraints.EAST; + gridBagConstraints.insets = new java.awt.Insets(4, 0, 0, 0); jPanel1.add(downloadButton, gridBagConstraints); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 46; + gridBagConstraints.gridwidth = 4; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.insets = new java.awt.Insets(4, 0, 16, 0); + jPanel1.add(jSeparator2, gridBagConstraints); getContentPane().add(jPanel1, "card2"); @@ -728,7 +737,7 @@ public class DbConnectionDetailsEditor extends javax.swing.JDialog { private void okButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_okButtonActionPerformed if (fillConnectionInfo()) { if (needsTest) { - if (!DbConnectionDialog.testConnection(isVisible()? this : parent, ci)) { + if (!DbConnectionDialog.testConnection(isVisible()? this : parent, ci, createDownloadButton())) { return; } } @@ -779,13 +788,33 @@ public class DbConnectionDetailsEditor extends javax.swing.JDialog { ConnectionInfo oldCi = new ConnectionInfo(); oldCi.assign(ci); if (fillConnectionInfo()) { - if (DbConnectionDialog.testConnection(isVisible()? this : parent, ci)) { + if (DbConnectionDialog.testConnection(isVisible()? this : parent, ci, createDownloadButton())) { JOptionPane.showMessageDialog(isVisible()? this : parent, "Successfully established connection.", "Connected", JOptionPane.INFORMATION_MESSAGE); } ci.assign(oldCi); } }//GEN-LAST:event_testConnectionButtonActionPerformed + private JButton createDownloadButton() { + JButton button = null; + if (downloadButton.isEnabled()) { + JButton finalButton = new JButton("Download Driver"); + button = finalButton; + button.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + Window window = SwingUtilities.getWindowAncestor(finalButton); + if (window != null) { + window.setVisible(false); + window.dispose(); + } + downloadButton.doClick(); + } + }); + } + return button; + } + private void loadButton1ActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_loadButton1ActionPerformed }//GEN-LAST:event_loadButton1ActionPerformed @@ -859,10 +888,15 @@ public class DbConnectionDetailsEditor extends javax.swing.JDialog { } AtomicBoolean ok = new AtomicBoolean(true); + Function updateInfo = total -> { + String text = "Downloading... " + (total > 0? "(" + total / 1024 + "K)" : "") + "
" + + driverURLs.stream().collect(Collectors.joining("
")) + ""; + return text; + }; @SuppressWarnings("serial") final ConcurrentTaskControl concurrentTaskControl = new ConcurrentTaskControl( - this, "Downloading Driver") { + this, updateInfo.apply(0L)) { @Override protected void onError(Throwable error) { @@ -877,7 +911,7 @@ public class DbConnectionDetailsEditor extends javax.swing.JDialog { closeWindow(); } }; - + ConcurrentTaskControl.openInModalDialog(this, concurrentTaskControl, new ConcurrentTaskControl.Task() { @Override @@ -886,9 +920,9 @@ public class DbConnectionDetailsEditor extends javax.swing.JDialog { driverURLs.forEach(url -> { try { String result = HttpDownload.get(url, vol -> { + String text = updateInfo.apply(total[0] += vol); UIUtil.invokeLater(() -> { - total[0] += vol; - concurrentTaskControl.master.infoLabel.setText("Downloading... (" + total[0] / 1024 + "k)"); + concurrentTaskControl.master.infoLabel.setText(text); }); }); if (result.length() == 0) { @@ -908,16 +942,16 @@ public class DbConnectionDetailsEditor extends javax.swing.JDialog { if (ok.get()) { synchronized (LOCK) { if (files.size() > 0) { - jar1.setText(files.get(0)); + jar1.setText(toRelFileName(files.get(0))); } if (files.size() > 1) { - jar2.setText(files.get(1)); + jar2.setText(toRelFileName(files.get(1))); } if (files.size() > 2) { - jar3.setText(files.get(2)); + jar3.setText(toRelFileName(files.get(2))); } if (files.size() > 3) { - jar4.setText(files.get(3)); + jar4.setText(toRelFileName(files.get(3))); } } } @@ -929,6 +963,26 @@ public class DbConnectionDetailsEditor extends javax.swing.JDialog { } }//GEN-LAST:event_downloadButtonActionPerformed + private String toRelFileName(final String fileName) { + try { + File f = new File(fileName); + String work = new File(".").getCanonicalPath(); + if (f.getCanonicalPath().startsWith(work)) { + String fn = f.getName(); + f = f.getParentFile(); + while (f != null && !f.getCanonicalPath().equals(work)) { + fn = f.getName() + File.separator + fn; + f = f.getParentFile(); + } + return fn; + } else { + return fileName; + } + } catch (Throwable t) { + return fileName; + } + } + protected void onSelect() { } @@ -950,7 +1004,6 @@ public class DbConnectionDetailsEditor extends javax.swing.JDialog { private javax.swing.JLabel jLabel2; private javax.swing.JLabel jLabel3; private javax.swing.JLabel jLabel4; - private javax.swing.JLabel jLabel5; private javax.swing.JLabel jLabel6; private javax.swing.JLabel jLabel7; private javax.swing.JLabel jLabel8; @@ -961,6 +1014,7 @@ public class DbConnectionDetailsEditor extends javax.swing.JDialog { private javax.swing.JPanel jPanel4; private javax.swing.JPanel jPanel5; private javax.swing.JSeparator jSeparator1; + private javax.swing.JSeparator jSeparator2; private javax.swing.JTextField jar1; private javax.swing.JTextField jar2; private javax.swing.JTextField jar3; diff --git a/src/main/gui/net/sf/jailer/ui/DbConnectionDialog.java b/src/main/gui/net/sf/jailer/ui/DbConnectionDialog.java index bbee3e71a..f2ca762d3 100644 --- a/src/main/gui/net/sf/jailer/ui/DbConnectionDialog.java +++ b/src/main/gui/net/sf/jailer/ui/DbConnectionDialog.java @@ -41,6 +41,7 @@ import java.util.List; import java.util.Map; import javax.swing.ImageIcon; +import javax.swing.JButton; import javax.swing.JLabel; import javax.swing.JOptionPane; import javax.swing.JTable; @@ -1067,7 +1068,7 @@ public class DbConnectionDialog extends javax.swing.JDialog { } try { UIUtil.setWaitCursor(root); - if (testConnection(mainPanel, currentConnection)) { + if (testConnection(mainPanel, currentConnection, null)) { isConnected = true; executionContext.setCurrentConnectionAlias(currentConnection.alias); onConnect(currentConnection); @@ -1088,7 +1089,7 @@ public class DbConnectionDialog extends javax.swing.JDialog { connect(); }// GEN-LAST:event_jButton1ActionPerformed - public static boolean testConnection(Component parent, ConnectionInfo ci) { + public static boolean testConnection(Component parent, ConnectionInfo ci, JButton downloadButton) { String d1 = ci.jar1.trim(); String d2 = ci.jar2.trim(); String d3 = ci.jar3.trim(); @@ -1154,9 +1155,9 @@ public class DbConnectionDialog extends javax.swing.JDialog { return false; } catch (Throwable e) { if (e.getCause() instanceof ClassNotFoundException) { - UIUtil.showException(parent, "Could not connect to DB", new ClassNotFoundException("JDBC driver class not found: '" + e.getMessage() + "'", e.getCause()), UIUtil.EXCEPTION_CONTEXT_MB_USER_ERROR); + UIUtil.showException(parent, "Could not connect", new ClassNotFoundException("JDBC driver class not found: '" + e.getMessage() + "'" + (downloadButton == null? "" : ".\nTry to download the driver."), e.getCause()), UIUtil.EXCEPTION_CONTEXT_MB_USER_ERROR, downloadButton); } else { - UIUtil.showException(parent, "Could not connect to DB (" + (e.getClass().getSimpleName()) + ")", e, UIUtil.EXCEPTION_CONTEXT_MB_USER_ERROR); + UIUtil.showException(parent, "Could not connect (" + (e.getClass().getSimpleName()) + ")", e, UIUtil.EXCEPTION_CONTEXT_MB_USER_ERROR); } return false; } @@ -1341,7 +1342,7 @@ public class DbConnectionDialog extends javax.swing.JDialog { if (ci.driverClass != null && ci.url != null && ci.user != null) { - if (testConnection(parent, ci)) { + if (testConnection(parent, ci, null)) { currentConnection = ci; executionContext.setCurrentConnectionAlias(currentConnection.alias); isConnected = true; diff --git a/src/main/gui/net/sf/jailer/ui/util/ConcurrentTaskControl.java b/src/main/gui/net/sf/jailer/ui/util/ConcurrentTaskControl.java index b4848da4c..2c2c3e822 100644 --- a/src/main/gui/net/sf/jailer/ui/util/ConcurrentTaskControl.java +++ b/src/main/gui/net/sf/jailer/ui/util/ConcurrentTaskControl.java @@ -259,7 +259,7 @@ public abstract class ConcurrentTaskControl extends javax.swing.JPanel { result.set(call.call()); done.set(true); } finally { - UIUtil.invokeLater(new Runnable() { + UIUtil.invokeLater(100, new Runnable() { @Override public void run() { concurrentTaskControl.closeWindow(); diff --git a/src/main/gui/net/sf/jailer/ui/util/HttpDownload.java b/src/main/gui/net/sf/jailer/ui/util/HttpDownload.java index a707c0eac..8d4d95eee 100644 --- a/src/main/gui/net/sf/jailer/ui/util/HttpDownload.java +++ b/src/main/gui/net/sf/jailer/ui/util/HttpDownload.java @@ -32,15 +32,14 @@ import net.sf.jailer.ui.Environment; public class HttpDownload { - public static final String DOWNLOADFOLDER = "download"; + public static final String DOWNLOADFOLDER = "downloads"; public static String get(final String url, Consumer volConsumer) throws Throwable { Throwable t = null; StringBuilder result = new StringBuilder(); try { - if (get(url, result, volConsumer) == 200) { - return result.toString(); - } + get(url, result, volConsumer); + return result.toString(); } catch (Throwable err) { t = err; } @@ -61,24 +60,28 @@ public class HttpDownload { throw t; } } + if (result.length() == 0 && t != null) { + throw t; + } return result.toString(); } - public static int get(final String url, final StringBuilder result, Consumer volConsumer) throws MalformedURLException, IOException { + public static void get(final String url, final StringBuilder result, Consumer volConsumer) throws MalformedURLException, IOException { URL theUrl; theUrl = new URL(url); long t0 = System.currentTimeMillis(); - URLConnection con = theUrl.openConnection(); - ((HttpURLConnection) con).setInstanceFollowRedirects(true); - InputStream in = con.getInputStream(); - int rc = ((HttpURLConnection) con).getResponseCode(); - if (rc == 200) { - File dir = Environment.newFile(DOWNLOADFOLDER); - dir.mkdir(); - String name = toFileName(theUrl); - File tmpFile = new File(dir, name + "." + System.currentTimeMillis()); - File file = new File(dir, name); - if (!file.exists()) { + String name = toFileName(theUrl); + File dir = Environment.newFile(DOWNLOADFOLDER); + dir.mkdir(); + File file = new File(dir, name); + int rc = 200; + if (!file.exists()) { + URLConnection con = theUrl.openConnection(); + ((HttpURLConnection) con).setInstanceFollowRedirects(true); + InputStream in = con.getInputStream(); + rc = ((HttpURLConnection) con).getResponseCode(); + if (rc == 200) { + File tmpFile = new File(dir, name + "." + System.currentTimeMillis()); OutputStream out = new FileOutputStream(tmpFile); int c; long total = 0; @@ -98,16 +101,18 @@ public class HttpDownload { } out.close(); if (!tmpFile.renameTo(file)) { - throw new RuntimeException("can't rename \"" + tmpFile.getAbsolutePath() + "\""); + throw new HttpException("can't rename \"" + tmpFile.getAbsolutePath() + "\""); }; if (total < 1024 * 100L) { - return 0; + throw new HttpException("download failed"); } } in.close(); - result.append(new File(DOWNLOADFOLDER, name).getAbsolutePath()); } - return rc; + result.append(new File(DOWNLOADFOLDER, name).getAbsolutePath()); + if (rc != 200) { + throw new HttpException("Response code " + rc + " received"); + } } public static String toFileName(URL theUrl) { @@ -121,8 +126,14 @@ public class HttpDownload { get(args[0], result, null); System.out.println(result); } catch (Throwable e) { - e.printStackTrace(); + // ignore } } + @SuppressWarnings("serial") + public static class HttpException extends RuntimeException { + public HttpException(String message) { + super(message); + } + } }