From 0a4463781418a066e58b097935d6be41a3419737 Mon Sep 17 00:00:00 2001 From: Eirik Bakke Date: Sat, 11 Jan 2025 22:59:11 -0500 Subject: [PATCH] Add a 'File' field to the Add Database Connection dialog. Use it for SQLite and DuckDB URL templates. Also validate the selected file in the SQLite and DuckDB cases. --- ide/db/nbproject/project.properties | 58 -------- .../db/explorer/action/ConnectAction.java | 22 ++- .../modules/db/explorer/dlg/Bundle.properties | 5 + .../db/explorer/dlg/NewConnectionPanel.form | 39 +++++- .../db/explorer/dlg/NewConnectionPanel.java | 126 ++++++++++++++++-- .../modules/db/util/DriverListUtil.java | 105 ++++++++++++++- .../org/netbeans/modules/db/util/JdbcUrl.java | 49 ++++++- .../modules/db/util/DriverListUtilTest.java | 28 ++++ 8 files changed, 342 insertions(+), 90 deletions(-) delete mode 100644 ide/db/nbproject/project.properties diff --git a/ide/db/nbproject/project.properties b/ide/db/nbproject/project.properties deleted file mode 100644 index 43740ef5f650..000000000000 --- a/ide/db/nbproject/project.properties +++ /dev/null @@ -1,58 +0,0 @@ -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, -# software distributed under the License is distributed on an -# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the License for the -# specific language governing permissions and limitations -# under the License. - -javac.compilerargs=-Xlint -Xlint:-serial -javac.source=1.8 -javadoc.arch=${basedir}/arch.xml -javadoc.apichanges=${basedir}/apichanges.xml - -spec.version.base=1.97.0 - -extra.module.files=modules/ext/ddl.jar - -fake-jdbc-40=${basedir}/build/fake-jdbc-40 -fake-jdbc-40.src=${fake-jdbc-40}/src -fake-jdbc-40.build=${fake-jdbc-40}/build -lib.cp=\ - ${fake-jdbc-40.build}:\ - ${openide.util.lookup.dir}/lib/org-openide-util-lookup.jar:\ - ${openide.util.dir}/lib/org-openide-util.jar:\ - ${openide.util.ui.dir}/lib/org-openide-util-ui.jar:\ - ${openide.dialogs.dir}/modules/org-openide-dialogs.jar:\ - ${openide.io.dir}/modules/org-openide-io.jar - - - -test.unit.cp.extra=\ - ${nb_all}/ide/db/external/derby-10.14.2.0.jar - -test.config.stableBTD.includes=**/*Test.class -test.config.stableBTD.excludes=\ - **/explorer/dlg/*,\ - **/explorer/node/ColumnNodeTest.class,\ - **/explorer/node/DDLHelperTest.class,\ - **/explorer/node/TableNodeTest.class,\ - **/explorer/node/ViewNodeTest.class,\ - **/DatabaseConnectionConvertor2Test.class,\ - **/DatabaseConnectionConvertorTest.class,\ - **/DatabaseConnectionTest.class,\ - **/DatabaseExplorerUIsTest.class,\ - **/GrabTableHelperTest.class,\ - **/JDBCDriverManagerTest.class,\ - **/JDBCDriverManager2Test.class,\ - **/QuoterTest.class - diff --git a/ide/db/src/org/netbeans/modules/db/explorer/action/ConnectAction.java b/ide/db/src/org/netbeans/modules/db/explorer/action/ConnectAction.java index 53ea11739095..263842929f2b 100644 --- a/ide/db/src/org/netbeans/modules/db/explorer/action/ConnectAction.java +++ b/ide/db/src/org/netbeans/modules/db/explorer/action/ConnectAction.java @@ -28,7 +28,6 @@ import java.awt.event.ActionListener; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; -import java.lang.reflect.InvocationTargetException; import java.sql.DatabaseMetaData; import java.sql.ResultSet; import java.sql.SQLException; @@ -41,6 +40,7 @@ import javax.swing.Action; import javax.swing.JComponent; import javax.swing.SwingUtilities; +import org.netbeans.api.db.explorer.JDBCDriver; import org.netbeans.api.progress.ProgressHandle; import org.netbeans.api.progress.ProgressHandleFactory; import org.netbeans.lib.ddl.DDLException; @@ -54,6 +54,8 @@ import org.netbeans.modules.db.explorer.dlg.ConnectionDialog; import org.netbeans.modules.db.explorer.dlg.ConnectionDialogMediator; import org.netbeans.modules.db.explorer.dlg.SchemaPanel; +import org.netbeans.modules.db.util.DriverListUtil; +import org.netbeans.modules.db.util.JdbcUrl; import org.openide.DialogDescriptor; import org.openide.DialogDisplayer; import org.openide.NotifyDescriptor; @@ -62,7 +64,6 @@ import org.openide.awt.ActionRegistration; import org.openide.awt.ActionState; import org.openide.util.ContextAwareAction; -import org.openide.util.Exceptions; import org.openide.util.HelpCtx; import org.openide.util.Lookup; import org.openide.util.Mutex; @@ -370,13 +371,20 @@ protected boolean retrieveSchemas(SchemaPanel schemaPanel, DatabaseConnection db } private boolean supportsConnectWithoutUsername(DatabaseConnection dc) { - try { - return dc.findJDBCDriver().getClassName().equals("org.sqlite.JDBC") //NOI18N - || dc.findJDBCDriver().getClassName().equals("org.h2.Driver"); //NOI18N - } catch (NullPointerException ex) { - // Most probably findJDBCDriver failed to find a driver + JDBCDriver driver = dc.findJDBCDriver(); + if (driver == null) { + return false; + } + List urls = DriverListUtil.getJdbcUrls(driver); + if (urls.isEmpty()) { return false; } + for (JdbcUrl url : urls) { + if (url.isUsernamePasswordDisplayed()) { + return false; + } + } + return true; } private void connectWithNewInfo(DatabaseConnection dbcon, Credentials input) { diff --git a/ide/db/src/org/netbeans/modules/db/explorer/dlg/Bundle.properties b/ide/db/src/org/netbeans/modules/db/explorer/dlg/Bundle.properties index 82fb3d443866..c7f15485eff3 100644 --- a/ide/db/src/org/netbeans/modules/db/explorer/dlg/Bundle.properties +++ b/ide/db/src/org/netbeans/modules/db/explorer/dlg/Bundle.properties @@ -49,6 +49,8 @@ ACS_AddDriverProgressBarA11yDesc=Progress bar showing the progress of searching # Select driver file chooser AddDriver_Chooser_Title=Select Driver AddDriver_Chooser_Filter=Archive Files (*.jar, *.zip) +# Select database file chooser +NewConnectionFile_Chooser_Title=Select Database File NewConnectionDialogTitle=New Database Connection NewConnectionDriverName=Driver &Name: @@ -66,6 +68,7 @@ NewConnectionSID=Service ID (SID): NewConnectionServiceName=Service: NewConnectionTNSName=TNS Name: NewConnectionDSN=DSN: +NewConnectionFile=&File: NewConnectionInstanceName=Instance Name: NewCOnnectionInputMode=Data Input &Mode: NewConnectionFieldEntryMode=&Field Entry @@ -105,6 +108,8 @@ ACS_NewConnectionTNSNameA11yDesc=The TNS name for this connection ACS_NewConnectionTNSNameTextFieldA11yName=Database server TNS text field ACS_NewConnectionDSNA11yDesc=The data source name for this connection ACS_NewConnectionDSNTextFieldA11yName=Database server data source name (DSN) text field +ACS_NewConnectionFileA11yDesc=The database file for this connection +ACS_NewConnectionFileTextFieldA11yName=Database file name text field ACS_NewConnectionInstanceNameA11yDesc=The instance name for this connection ACS_NewConnectionInstanceNameTextFieldA11yName=Database server instance name text field ACS_NewConnectionFieldEntryModeA11yDesc=Select this mode to enter individual field values. diff --git a/ide/db/src/org/netbeans/modules/db/explorer/dlg/NewConnectionPanel.form b/ide/db/src/org/netbeans/modules/db/explorer/dlg/NewConnectionPanel.form index 5f37405671a4..410572e842e4 100644 --- a/ide/db/src/org/netbeans/modules/db/explorer/dlg/NewConnectionPanel.form +++ b/ide/db/src/org/netbeans/modules/db/explorer/dlg/NewConnectionPanel.form @@ -56,6 +56,7 @@ + @@ -73,7 +74,7 @@ - + @@ -97,6 +98,11 @@ + + + + + @@ -149,6 +155,12 @@ + + + + + + @@ -400,6 +412,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ide/db/src/org/netbeans/modules/db/explorer/dlg/NewConnectionPanel.java b/ide/db/src/org/netbeans/modules/db/explorer/dlg/NewConnectionPanel.java index 15a383f35524..a1606ed1e223 100644 --- a/ide/db/src/org/netbeans/modules/db/explorer/dlg/NewConnectionPanel.java +++ b/ide/db/src/org/netbeans/modules/db/explorer/dlg/NewConnectionPanel.java @@ -24,8 +24,14 @@ import java.awt.event.FocusEvent; import java.awt.event.FocusListener; import java.awt.event.ItemEvent; +import java.io.File; +import java.io.IOException; import java.net.MalformedURLException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; import java.util.LinkedHashMap; +import java.util.List; import java.util.Map.Entry; import java.util.Properties; import java.util.logging.Level; @@ -33,7 +39,9 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.swing.JComboBox; +import javax.swing.JComponent; import javax.swing.JLabel; +import javax.swing.JOptionPane; import javax.swing.JTextArea; import javax.swing.JTextField; import javax.swing.SwingUtilities; @@ -55,6 +63,7 @@ import org.openide.DialogDisplayer; import org.openide.NotifyDescriptor; import org.openide.WizardValidationException; +import org.openide.filesystems.FileChooserBuilder; import org.openide.util.NbBundle; import org.openide.util.RequestProcessor; @@ -84,6 +93,7 @@ private void initFieldMap() { urlFields.put(JdbcUrl.TOKEN_SERVICENAME, new UrlField(serviceField, serviceLabel)); urlFields.put(JdbcUrl.TOKEN_TNSNAME, new UrlField(tnsField, tnsLabel)); urlFields.put(JdbcUrl.TOKEN_DSN, new UrlField(dsnField, dsnLabel)); + urlFields.put(JdbcUrl.TOKEN_FILE, new UrlField(fileField, fileLabel, fileBrowseButton)); urlFields.put(JdbcUrl.TOKEN_SERVERNAME, new UrlField(serverNameField, serverNameLabel)); urlFields.put(JdbcUrl.TOKEN_INSTANCE, new UrlField(instanceField, instanceLabel)); } @@ -259,6 +269,8 @@ private void initAccessibility() { tnsLabel.getAccessibleContext().setAccessibleDescription(NbBundle.getMessage(NewConnectionPanel.class, "ACS_NewConnectionTNSNameA11yDesc")); //NOI18N dsnField.getAccessibleContext().setAccessibleName(NbBundle.getMessage(NewConnectionPanel.class, "ACS_NewConnectionDSNTextFieldA11yName")); //NOI18N dsnLabel.getAccessibleContext().setAccessibleDescription(NbBundle.getMessage(NewConnectionPanel.class, "ACS_NewConnectionDSNA11yDesc")); //NOI18N + fileField.getAccessibleContext().setAccessibleName(NbBundle.getMessage(NewConnectionPanel.class, "ACS_NewConnectionFileTextFieldA11yName")); //NOI18N + fileLabel.getAccessibleContext().setAccessibleDescription(NbBundle.getMessage(NewConnectionPanel.class, "ACS_NewConnectionFileA11yDesc")); //NOI18N instanceField.getAccessibleContext().setAccessibleName(NbBundle.getMessage(NewConnectionPanel.class, "ACS_NewConnectionInstanceNameTextFieldA11yName")); //NOI18N instanceLabel.getAccessibleContext().setAccessibleDescription(NbBundle.getMessage(NewConnectionPanel.class, "ACS_NewConnectionInstanceNameA11yDesc")); //NOI18N } @@ -301,6 +313,9 @@ private void initComponents() { passwordField = new javax.swing.JPasswordField(); dsnLabel = new javax.swing.JLabel(); dsnField = new javax.swing.JTextField(); + fileLabel = new javax.swing.JLabel(); + fileField = new javax.swing.JTextField(); + fileBrowseButton = new javax.swing.JButton(); urlField = new javax.swing.JTextField(); passwordCheckBox = new javax.swing.JCheckBox(); directUrlLabel = new javax.swing.JLabel(); @@ -371,6 +386,14 @@ private void initComponents() { dsnField.setToolTipText(org.openide.util.NbBundle.getMessage(NewConnectionPanel.class, "ACS_NewConnectionDSNA11yDesc")); // NOI18N + fileLabel.setLabelFor(fileField); + org.openide.awt.Mnemonics.setLocalizedText(fileLabel, org.openide.util.NbBundle.getMessage(NewConnectionPanel.class, "NewConnectionFile")); // NOI18N + + fileField.setToolTipText(org.openide.util.NbBundle.getMessage(NewConnectionPanel.class, "ACS_NewConnectionFileA11yDesc")); // NOI18N + + org.openide.awt.Mnemonics.setLocalizedText(fileBrowseButton, "&Browse..."); + fileBrowseButton.addActionListener(formListener); + urlField.setToolTipText(org.openide.util.NbBundle.getMessage(NewConnectionPanel.class, "ACS_NewConnectionJDBCURLA11yDesc")); // NOI18N urlField.addActionListener(formListener); urlField.addFocusListener(formListener); @@ -404,6 +427,7 @@ private void initComponents() { .addComponent(instanceLabel, javax.swing.GroupLayout.Alignment.LEADING, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) .addComponent(serverNameLabel, javax.swing.GroupLayout.Alignment.LEADING, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) .addComponent(dsnLabel, javax.swing.GroupLayout.Alignment.LEADING, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(fileLabel, javax.swing.GroupLayout.Alignment.LEADING, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) .addComponent(tnsLabel, javax.swing.GroupLayout.Alignment.LEADING, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) .addComponent(serviceLabel, javax.swing.GroupLayout.Alignment.LEADING, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) .addComponent(sidLabel, javax.swing.GroupLayout.Alignment.LEADING, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) @@ -418,7 +442,7 @@ private void initComponents() { .addComponent(bConnectionProperties) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) .addComponent(bTestConnection) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, 100, Short.MAX_VALUE)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) .addComponent(userField, javax.swing.GroupLayout.Alignment.TRAILING) .addComponent(sidField) .addComponent(serviceField) @@ -438,7 +462,11 @@ private void initComponents() { .addComponent(templateComboBox, javax.swing.GroupLayout.Alignment.TRAILING, 0, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) .addGroup(layout.createSequentialGroup() .addComponent(passwordCheckBox, javax.swing.GroupLayout.PREFERRED_SIZE, 256, javax.swing.GroupLayout.PREFERRED_SIZE) - .addGap(0, 0, Short.MAX_VALUE))))) + .addGap(0, 0, Short.MAX_VALUE)) + .addGroup(layout.createSequentialGroup() + .addComponent(fileField) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(fileBrowseButton))))) .addContainerGap()) ); layout.setVerticalGroup( @@ -477,6 +505,11 @@ private void initComponents() { .addComponent(dsnLabel) .addComponent(dsnField, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(fileLabel) + .addComponent(fileField, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(fileBrowseButton)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) .addComponent(serverNameLabel) .addComponent(serverNameField, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)) @@ -527,6 +560,9 @@ else if (evt.getSource() == bTestConnection) { else if (evt.getSource() == bConnectionProperties) { NewConnectionPanel.this.bConnectionPropertiesActionPerformed(evt); } + else if (evt.getSource() == fileBrowseButton) { + NewConnectionPanel.this.fileBrowseButtonActionPerformed(evt); + } } public void focusGained(java.awt.event.FocusEvent evt) { @@ -618,6 +654,48 @@ private void bConnectionPropertiesActionPerformed(java.awt.event.ActionEvent evt } }//GEN-LAST:event_bConnectionPropertiesActionPerformed + private void fileBrowseButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_fileBrowseButtonActionPerformed + FileChooserBuilder fileChooserBuilder = new FileChooserBuilder(NewConnectionPanel.class); + fileChooserBuilder.setTitle(NbBundle.getMessage(AddDriverDialog.class, "NewConnectionFile_Chooser_Title")); //NOI18N + fileChooserBuilder.setFilesOnly(true); + File existingFile = new File(fileField.getText()); + if (existingFile.exists() && existingFile.isDirectory()) { + fileChooserBuilder.setDefaultWorkingDirectory(existingFile); + } else { + File parentFile = existingFile.getParentFile(); + if (parentFile != null && parentFile.exists()) { + fileChooserBuilder.setDefaultWorkingDirectory(existingFile.getParentFile()); + } + } + File file = fileChooserBuilder.showOpenDialog(); + if (file != null) { + JdbcUrl url = getSelectedJdbcUrl(); + JdbcUrl.DatabaseFileValidator validator = (url == null) ? null : url.getDatabaseFileValidator(); + String validationErrorMessage = null; + if (validator != null) { + try { + validationErrorMessage = validator.getValidationErrorMessage(file); + } catch (IOException e) { + LOGGER.log(Level.INFO, "Problem while attempting to validate database file", e); + } + } + if (validationErrorMessage != null) { + String validationErrorMessageFinal = validationErrorMessage; + /* Use invokeLater to give the file browser dialog time to disappear before the new + dialog is shown. Otherwise DialogDisplayer can make it appear behind the + New Connection dialog. */ + SwingUtilities.invokeLater(() -> { + NotifyDescriptor msgDesc = + new NotifyDescriptor.Message(validationErrorMessageFinal, JOptionPane.WARNING_MESSAGE); + DialogDisplayer.getDefault().notify(msgDesc); + }); + /* Still allow the file to be selected. Maybe there's a newer JDBC driver that + supports other formats etc. */ + } + fileField.setText(file.getAbsolutePath()); + } + }//GEN-LAST:event_fileBrowseButtonActionPerformed + // Variables declaration - do not modify//GEN-BEGIN:variables private javax.swing.JButton bConnectionProperties; private javax.swing.JButton bTestConnection; @@ -626,6 +704,9 @@ private void bConnectionPropertiesActionPerformed(java.awt.event.ActionEvent evt private javax.swing.JLabel directUrlLabel; private javax.swing.JTextField dsnField; private javax.swing.JLabel dsnLabel; + private javax.swing.JButton fileBrowseButton; + private javax.swing.JTextField fileField; + private javax.swing.JLabel fileLabel; private javax.swing.JTextField hostField; private javax.swing.JLabel hostLabel; private javax.swing.ButtonGroup inputModeButtonGroup; @@ -697,8 +778,9 @@ private void setUpFields() { if (jdbcurl == null) { for (Entry entry : urlFields.entrySet()) { - entry.getValue().getField().setVisible(false); - entry.getValue().getLabel().setVisible(false); + for (JComponent c : entry.getValue().getComponents()) { + c.setVisible(false); + } } checkValid(); @@ -706,19 +788,24 @@ private void setUpFields() { return; } - userField.setVisible(true); - userLabel.setVisible(true); - - passwordField.setVisible(true); - passwordLabel.setVisible(true); - - passwordCheckBox.setVisible(true); + boolean showUsernamePassword = jdbcurl.isUsernamePasswordDisplayed(); + userField.setVisible(showUsernamePassword); + userLabel.setVisible(showUsernamePassword); + passwordField.setVisible(showUsernamePassword); + passwordLabel.setVisible(showUsernamePassword); + passwordCheckBox.setVisible(showUsernamePassword); + if (!showUsernamePassword) { + userField.setText(""); + passwordField.setText(""); + passwordCheckBox.setSelected(false); + } directUrlLabel.setVisible(true); for (Entry entry : urlFields.entrySet()) { - entry.getValue().getField().setVisible(jdbcurl.supportsToken(entry.getKey())); - entry.getValue().getLabel().setVisible(jdbcurl.supportsToken(entry.getKey())); + for (JComponent c : entry.getValue().getComponents()) { + c.setVisible(jdbcurl.supportsToken(entry.getKey())); + } } if (!jdbcurl.isParseUrl()) { @@ -1020,10 +1107,16 @@ private class UrlField { private final JTextField field; private final JLabel label; + private final List components; - public UrlField(JTextField field, JLabel label) { + public UrlField(JTextField field, JLabel label, JComponent ... otherComponents) { this.field = field; this.label = label; + List toComponents = new ArrayList<>(); + toComponents.add(field); + toComponents.add(label); + toComponents.addAll(Arrays.asList(otherComponents)); + components = Collections.unmodifiableList(toComponents); } public JTextField getField() { @@ -1033,6 +1126,10 @@ public JTextField getField() { public JLabel getLabel() { return label; } + + public List getComponents() { + return components; + } } /** @@ -1063,6 +1160,7 @@ public void focusLost(FocusEvent e) { case JdbcUrl.TOKEN_SERVICENAME: case JdbcUrl.TOKEN_TNSNAME: case JdbcUrl.TOKEN_DSN: + case JdbcUrl.TOKEN_FILE: case JdbcUrl.TOKEN_SERVERNAME: case JdbcUrl.TOKEN_INSTANCE: case USERINPUT_FIELD: diff --git a/ide/db/src/org/netbeans/modules/db/util/DriverListUtil.java b/ide/db/src/org/netbeans/modules/db/util/DriverListUtil.java index 12813638cd13..1cfdc746508f 100644 --- a/ide/db/src/org/netbeans/modules/db/util/DriverListUtil.java +++ b/ide/db/src/org/netbeans/modules/db/util/DriverListUtil.java @@ -19,6 +19,11 @@ package org.netbeans.modules.db.util; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; import java.util.Set; @@ -376,15 +381,25 @@ following fully-qualified class names (FQCNs) that are independent of the JDBC v add("Sybase Driver", "com.sun.sql.jdbc.sybase.SybaseDriver", - "jdbc:sun:sybase://[:[:]"); - add("SQLite", - "org.sqlite.JDBC", - "jdbc:sqlite:"); - - add("H2 Database Engine", + url = add("SQLite", null, "org.sqlite.JDBC", "jdbc:sqlite:", true); + url.setSampleUrl("jdbc:sqlite:"); + url.setUsernamePasswordDisplayed(false); + url.setDatabaseFileValidator(new SQLiteDatabaseFileValidator()); + + // The FILE field is optional for DuckDB. If omitted, an in-memory database will be opened. + url = add("DuckDB", null, "org.duckdb.DuckDBDriver", "jdbc:duckdb:[]", true); + url.setSampleUrl("jdbc:duckdb:"); + url.setUsernamePasswordDisplayed(false); + url.setDatabaseFileValidator(new DuckDBDatabaseFileValidator()); + + url = add("H2 Database Engine", "org.h2.Driver", - "jdbc:h2:"); + "jdbc:h2:", true); + /* I think H2 can sometimes use a password with the database, even though it's a file-based + database. So keep the username/password displayed in this case. */ + // url.setUsernamePasswordDisplayed(false); } public static Set getDrivers() { @@ -404,6 +419,8 @@ public static List getJdbcUrls(JDBCDriver driver) { for (JdbcUrl url : templateUrls) { if (url.getClassName().equals(driver.getClassName())) { JdbcUrl newurl = new JdbcUrl(url, driver); + newurl.setUsernamePasswordDisplayed(url.isUsernamePasswordDisplayed()); + newurl.setDatabaseFileValidator(url.getDatabaseFileValidator()); driverUrls.add(newurl); } } @@ -449,4 +466,78 @@ public static String findFreeName(String name) { return name; } } + + private static class SQLiteDatabaseFileValidator implements JdbcUrl.DatabaseFileValidator { + /* From zMagicHeader in src/btree.c in + https://github.com/sqlite/sqlite/blob/cc83b6e071ba69943f175a038d2625ae3d6abf47/src/btree.c */ + private static final byte[] SQLITE2_MAGIC_HEADER = + "** This file contains an SQLite ".getBytes(StandardCharsets.US_ASCII); + // From https://www.sqlite.org/fileformat.html + private static final byte[] SQLITE3_MAGIC_HEADER = + "SQLite format 3\u0000".getBytes(StandardCharsets.US_ASCII); + + private static Integer getSQLiteVersion(File file) throws IOException { + final byte[] actual; + try (InputStream is = new FileInputStream(file)) { + actual = is.readNBytes(Math.max(SQLITE2_MAGIC_HEADER.length, SQLITE3_MAGIC_HEADER.length)); + } + if (startsWith(actual, SQLITE3_MAGIC_HEADER)) { + return 3; + } else if (startsWith(actual, SQLITE2_MAGIC_HEADER)) { + return 2; + } else { + return null; + } + } + + private static boolean startsWith(byte[] arr, byte[] prefix) { + if (arr.length < prefix.length) { + return false; + } + for (int i = 0; i < prefix.length; i++) { + if (arr[i] != prefix[i]) { + return false; + } + } + return true; + } + + @Override + public String getValidationErrorMessage(File file) throws IOException { + Integer ret = getSQLiteVersion(file); + if (ret == null) { + return "This file is not a SQLite 3 database."; + } else if (ret == 2) { + return "This file may be an older SQLite 2 database, which is not supported."; + } else { + return null; + } + } + } + + private static class DuckDBDatabaseFileValidator implements JdbcUrl.DatabaseFileValidator { + // From https://duckdb.org/docs/internals/storage.html + private static final byte[] DUCKDB_MAGIC_HEADER = + "DUCK".getBytes(StandardCharsets.US_ASCII); + + @Override + public String getValidationErrorMessage(File file) throws IOException { + final byte[] actual; + /* "DuckDB files start with a uint64_t which contains a checksum for the main header, + followed by four magic bytes (DUCK)" + https://duckdb.org/docs/internals/storage.html */ + try (InputStream is = new FileInputStream(file)) { + actual = is.readNBytes(8 + 4); + } + if (actual[8] == (byte) 'D' && + actual[9] == (byte) 'U' && + actual[10] == (byte) 'C' && + actual[11] == (byte) 'K') + { + return null; + } else { + return "This file is not a valid DuckDB database."; + } + } + } } diff --git a/ide/db/src/org/netbeans/modules/db/util/JdbcUrl.java b/ide/db/src/org/netbeans/modules/db/util/JdbcUrl.java index ea9e865bcc12..7e6e664c28b3 100644 --- a/ide/db/src/org/netbeans/modules/db/util/JdbcUrl.java +++ b/ide/db/src/org/netbeans/modules/db/util/JdbcUrl.java @@ -19,6 +19,8 @@ package org.netbeans.modules.db.util; +import java.io.File; +import java.io.IOException; import java.net.MalformedURLException; import java.util.ArrayList; import java.util.HashMap; @@ -58,6 +60,7 @@ public class JdbcUrl extends HashMap { public static final String TOKEN_SID = ""; public static final String TOKEN_SERVICENAME = ""; public static final String TOKEN_DSN = ""; + public static final String TOKEN_FILE = ""; public static final String TOKEN_INSTANCE = ""; private static final String OPTIONAL_START = "["; @@ -71,6 +74,8 @@ public class JdbcUrl extends HashMap { private String sampleUser; private String samplePassword; private String sampleUrl; + private boolean usernamePasswordDisplayed = true; + private DatabaseFileValidator databaseFileValidator = null; public JdbcUrl(String name, String displayName, String className, String type, String urlTemplate, boolean parseUrl) { this.name = name; @@ -139,11 +144,46 @@ public void setDriver(JDBCDriver driver) { this.name = driver.getName(); this.displayName = driver.getDisplayName(); } - + + public void setUsernamePasswordDisplayed(boolean usernamePasswordDisplayed) { + this.usernamePasswordDisplayed = usernamePasswordDisplayed; + } + + /** + * @return true if this driver always uses an empty username and password + */ + public boolean isUsernamePasswordDisplayed() { + return usernamePasswordDisplayed; + } + public JDBCDriver getDriver() { return driver; } + public void setDatabaseFileValidator(DatabaseFileValidator databaseFileValidator) { + this.databaseFileValidator = databaseFileValidator; + } + + /** + * @return may be null + */ + public DatabaseFileValidator getDatabaseFileValidator() { + return databaseFileValidator; + } + + public static interface DatabaseFileValidator { + /** + * Do a quick check of whether the given file is a valid database file for the relevant JDBC + * driver. This would typically be based on examination of the first few bytes in the file. + * Called after the user selects a file in the file browser, for JDBC drivers that expect a + * {@link JdbcUrl#TOKEN_FILE} field. + * + * @return null if the file was of a valid type for this JDBC driver, otherwise an error + * message that can be displayed to the user in a dialog box + */ + String getValidationErrorMessage(File file) throws IOException; + } + /** * Get display name with type and custom driver name, if available. */ @@ -229,6 +269,9 @@ public boolean equals(Object obj) { if (!Objects.equals(this.type, other.type)) { return false; } + if (!Objects.equals(this.usernamePasswordDisplayed, other.usernamePasswordDisplayed)) { + return false; + } return true; } @@ -737,8 +780,8 @@ public String toString() { "',className='" + className + // NOI18N "',type='" + type + // NOI18N "',urlTemplate='" + urlTemplate + // NOI18N - "',parseUrl,=" + parseUrl + // NOI18N - "',sampleUrl,=" + sampleUrl + "]"; // NOI18N + "',parseUrl=" + parseUrl + // NOI18N + "',sampleUrl=" + sampleUrl + "]"; // NOI18N } public String getSampleUser() { diff --git a/ide/db/test/unit/src/org/netbeans/modules/db/util/DriverListUtilTest.java b/ide/db/test/unit/src/org/netbeans/modules/db/util/DriverListUtilTest.java index e68f7a173774..38774d0c8734 100644 --- a/ide/db/test/unit/src/org/netbeans/modules/db/util/DriverListUtilTest.java +++ b/ide/db/test/unit/src/org/netbeans/modules/db/util/DriverListUtilTest.java @@ -44,6 +44,7 @@ public class DriverListUtilTest extends TestCase { private static final String INSTANCE = "instancename"; private static final String SID = "mysid"; private static final String DSN = "mydsn"; + private static final String FILE = "C:\\Users\\John\\foobar.db"; private static final String TNSNAME = "mytns"; private static final HashMap ALLPROPS = new HashMap<>(); @@ -57,6 +58,7 @@ public class DriverListUtilTest extends TestCase { ALLPROPS.put(JdbcUrl.TOKEN_SERVERNAME, SERVERNAME); ALLPROPS.put(JdbcUrl.TOKEN_ADDITIONAL, ADDITIONAL); ALLPROPS.put(JdbcUrl.TOKEN_DSN, DSN); + ALLPROPS.put(JdbcUrl.TOKEN_FILE, FILE); ALLPROPS.put(JdbcUrl.TOKEN_SERVICENAME, SERVICENAME); ALLPROPS.put(JdbcUrl.TOKEN_SID, SID); ALLPROPS.put(JdbcUrl.TOKEN_TNSNAME, TNSNAME); @@ -306,6 +308,31 @@ public void testAmazonRedshiftIAM() throws Exception { propValues.remove(JdbcUrl.TOKEN_HOST); testUrlString(url, propValues, "jdbc:redshift:iam:///" + DB); } + + public void testSQLite() throws Exception { + ArrayList supportedProps = new ArrayList<>(); + supportedProps.add(JdbcUrl.TOKEN_FILE); + ArrayList requiredProps = new ArrayList<>(); + requiredProps.add(JdbcUrl.TOKEN_FILE); + JdbcUrl url = checkUrl("SQLite", null, "org.sqlite.JDBC", + "jdbc:sqlite:", + supportedProps, requiredProps); + HashMap propValues = buildPropValues(supportedProps); + testUrlString(url, propValues, "jdbc:sqlite:" + FILE); + } + + public void testDuckDB() throws Exception { + ArrayList supportedProps = new ArrayList<>(); + supportedProps.add(JdbcUrl.TOKEN_FILE); + ArrayList requiredProps = new ArrayList<>(); + JdbcUrl url = checkUrl("DuckDB", null, "org.duckdb.DuckDBDriver", + "jdbc:duckdb:[]", + supportedProps, requiredProps); + HashMap propValues = buildPropValues(supportedProps); + testUrlString(url, propValues, "jdbc:duckdb:" + FILE); + propValues.remove(JdbcUrl.TOKEN_FILE); + testUrlString(url, propValues, "jdbc:duckdb:"); + } enum DB2Types { DB2, IDS, CLOUDSCAPE }; @@ -676,6 +703,7 @@ private JdbcUrl checkUrl(String name, String type, String className, JdbcUrl other = new JdbcUrl(url.getName(), url.getName(), url.getClassName(), url.getType(), url.getUrlTemplate(), url.isParseUrl()); + other.setUsernamePasswordDisplayed(url.isUsernamePasswordDisplayed()); assertEquals(url, other);