This is an automated email from the ASF dual-hosted git repository.
bamaer pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/hop.git
The following commit(s) were added to refs/heads/main by this push:
new 1e674f68df Issue #9098 : Keep hop-ui and SWT out of the Oracle, MS SQL
Server Native and Redshift database metas (#9138)
1e674f68df is described below
commit 1e674f68dffd1cd8539390f7c7c12de4795444ee
Author: vbhanuchander-lang <[email protected]>
AuthorDate: Fri Oct 30 05:08:25 2026 -0400
Issue #9098 : Keep hop-ui and SWT out of the Oracle, MS SQL Server Native
and Redshift database metas (#9138)
The three metas implemented IGuiPluginCompositeWidgetsListener themselves,
so loading them needed hop-ui and SWT, which breaks using Hop as a library
without the GUI.
The listener code moves unchanged into a separate class in each plugin.
IDatabase gets getGuiWidgetsListenerClassName(), which names that class,
and DatabaseMetaEditor creates it from the plugin's class loader. A meta
that implements the listener itself still works.
---
.../org/apache/hop/core/database/IDatabase.java | 12 ++
.../mssqlnative/MsSqlServerNativeDatabaseMeta.java | 60 +--------
.../MsSqlServerNativeWidgetsListener.java | 93 +++++++++++++
.../MsSqlServerNativeDatabaseMetaHeadlessTest.java | 82 +++++++++++
.../hop/databases/oracle/OracleDatabaseMeta.java | 110 +--------------
.../databases/oracle/OracleWidgetsListener.java | 150 +++++++++++++++++++++
.../oracle/OracleDatabaseMetaHeadlessTest.java | 82 +++++++++++
.../oracle/OracleWidgetVisibilityTest.java | 24 ++--
.../databases/redshift/RedshiftDatabaseMeta.java | 92 +------------
.../redshift/RedshiftWidgetsListener.java | 130 ++++++++++++++++++
.../redshift/RedshiftDatabaseMetaHeadlessTest.java | 82 +++++++++++
.../hop/ui/core/database/DatabaseMetaEditor.java | 37 ++++-
.../hop/ui/testing/DisabledGuiWidgetsTestBase.java | 25 +++-
13 files changed, 711 insertions(+), 268 deletions(-)
diff --git a/core/src/main/java/org/apache/hop/core/database/IDatabase.java
b/core/src/main/java/org/apache/hop/core/database/IDatabase.java
index 814e908510..3b561a40fe 100644
--- a/core/src/main/java/org/apache/hop/core/database/IDatabase.java
+++ b/core/src/main/java/org/apache/hop/core/database/IDatabase.java
@@ -1325,6 +1325,18 @@ public interface IDatabase extends Cloneable {
return true;
}
+ /**
+ * Names a class in the same plugin that adjusts this database's widgets in
the connection dialog,
+ * for example to hide options that do not apply. The class implements {@code
+ * IGuiPluginCompositeWidgetsListener} and has a public constructor taking
this {@link IDatabase}.
+ * It is given as a name so that the database can be used without the GUI on
the classpath.
+ *
+ * @return the class name, or null when the widgets need no adjusting
+ */
+ default String getGuiWidgetsListenerClassName() {
+ return null;
+ }
+
/**
* @return the unsupported message if database does not support standard
table output transform
*/
diff --git
a/plugins/databases/mssqlnative/src/main/java/org/apache/hop/databases/mssqlnative/MsSqlServerNativeDatabaseMeta.java
b/plugins/databases/mssqlnative/src/main/java/org/apache/hop/databases/mssqlnative/MsSqlServerNativeDatabaseMeta.java
index 0ac37c60f7..fee6b95533 100644
---
a/plugins/databases/mssqlnative/src/main/java/org/apache/hop/databases/mssqlnative/MsSqlServerNativeDatabaseMeta.java
+++
b/plugins/databases/mssqlnative/src/main/java/org/apache/hop/databases/mssqlnative/MsSqlServerNativeDatabaseMeta.java
@@ -17,10 +17,7 @@
package org.apache.hop.databases.mssqlnative;
-import java.util.ArrayList;
-import java.util.List;
import org.apache.hop.core.Const;
-import org.apache.hop.core.database.BaseDatabaseMeta;
import org.apache.hop.core.database.DatabaseMeta;
import org.apache.hop.core.database.DatabaseMetaPlugin;
import org.apache.hop.core.database.DriverDownload;
@@ -30,10 +27,6 @@ import org.apache.hop.core.gui.plugin.GuiWidgetElement;
import org.apache.hop.core.util.Utils;
import org.apache.hop.databases.mssql.MsSqlServerDatabaseMeta;
import org.apache.hop.metadata.api.HopMetadataProperty;
-import org.apache.hop.ui.core.gui.GuiCompositeWidgets;
-import org.apache.hop.ui.core.gui.IGuiPluginCompositeWidgetsListener;
-import org.eclipse.swt.widgets.Button;
-import org.eclipse.swt.widgets.Control;
@DatabaseMetaPlugin(
type = "MSSQLNATIVE",
@@ -42,8 +35,7 @@ import org.eclipse.swt.widgets.Control;
documentationUrl = "/database/databases/mssqlnative.html",
classLoaderGroup = "mssqlnative-db")
@GuiPlugin(id = "GUI-MSSQLServerNativeDatabaseMeta")
-public class MsSqlServerNativeDatabaseMeta extends MsSqlServerDatabaseMeta
- implements IGuiPluginCompositeWidgetsListener {
+public class MsSqlServerNativeDatabaseMeta extends MsSqlServerDatabaseMeta {
public static final String ID_INTEGRATED_SECURITY_WIDGET =
"usingIntegratedSecurity";
@@ -57,54 +49,8 @@ public class MsSqlServerNativeDatabaseMeta extends
MsSqlServerDatabaseMeta
private boolean usingIntegratedSecurity;
@Override
- public void widgetsCreated(GuiCompositeWidgets compositeWidgets) {
- // not needed
- }
-
- @Override
- public void widgetsPopulated(GuiCompositeWidgets compositeWidgets) {
- enableField(compositeWidgets);
- }
-
- @Override
- public void widgetModified(
- GuiCompositeWidgets compositeWidgets, Control changedWidget, String
widgetId) {
- enableField(compositeWidgets);
- }
-
- @Override
- public void persistContents(GuiCompositeWidgets compositeWidgets) {
- // not needed
- }
-
- private void enableField(GuiCompositeWidgets compositeWidgets) {
- List<Control> controls = new ArrayList<>();
- String[] ids =
- new String[] {
- BaseDatabaseMeta.ID_USERNAME_LABEL,
- BaseDatabaseMeta.ID_USERNAME_WIDGET,
- BaseDatabaseMeta.ID_PASSWORD_LABEL,
- BaseDatabaseMeta.ID_PASSWORD_WIDGET,
- };
- for (String id : ids) {
- Control control = compositeWidgets.getWidgetsMap().get(id);
- if (control != null) {
- controls.add(control);
- }
- }
- Button wIntegratedSecurity =
- (Button)
compositeWidgets.getWidgetsMap().get(ID_INTEGRATED_SECURITY_WIDGET);
-
- // The checkbox is absent when it is switched off in
disabledGuiElements.xml. There is nothing
- // to read the setting from then, so we go by what is on the metadata
itself.
- //
- boolean enable =
- wIntegratedSecurity == null
- ? !isUsingIntegratedSecurity()
- : !wIntegratedSecurity.getSelection();
- for (Control control : controls) {
- control.setEnabled(enable);
- }
+ public String getGuiWidgetsListenerClassName() {
+ return
"org.apache.hop.databases.mssqlnative.MsSqlServerNativeWidgetsListener";
}
/**
diff --git
a/plugins/databases/mssqlnative/src/main/java/org/apache/hop/databases/mssqlnative/MsSqlServerNativeWidgetsListener.java
b/plugins/databases/mssqlnative/src/main/java/org/apache/hop/databases/mssqlnative/MsSqlServerNativeWidgetsListener.java
new file mode 100644
index 0000000000..802581b823
--- /dev/null
+++
b/plugins/databases/mssqlnative/src/main/java/org/apache/hop/databases/mssqlnative/MsSqlServerNativeWidgetsListener.java
@@ -0,0 +1,93 @@
+/*
+ * 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.
+ */
+
+package org.apache.hop.databases.mssqlnative;
+
+import java.util.ArrayList;
+import java.util.List;
+import org.apache.hop.core.database.BaseDatabaseMeta;
+import org.apache.hop.core.database.IDatabase;
+import org.apache.hop.ui.core.gui.GuiCompositeWidgets;
+import org.apache.hop.ui.core.gui.IGuiPluginCompositeWidgetsListener;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Control;
+
+/**
+ * Disables the user name and password when integrated security is used, in
the connection dialog.
+ */
+public class MsSqlServerNativeWidgetsListener implements
IGuiPluginCompositeWidgetsListener {
+
+ private final MsSqlServerNativeDatabaseMeta database;
+
+ public MsSqlServerNativeWidgetsListener(IDatabase database) {
+ this.database = (MsSqlServerNativeDatabaseMeta) database;
+ }
+
+ @Override
+ public void widgetsCreated(GuiCompositeWidgets compositeWidgets) {
+ // not needed
+ }
+
+ @Override
+ public void widgetsPopulated(GuiCompositeWidgets compositeWidgets) {
+ enableField(compositeWidgets);
+ }
+
+ @Override
+ public void widgetModified(
+ GuiCompositeWidgets compositeWidgets, Control changedWidget, String
widgetId) {
+ enableField(compositeWidgets);
+ }
+
+ @Override
+ public void persistContents(GuiCompositeWidgets compositeWidgets) {
+ // not needed
+ }
+
+ private void enableField(GuiCompositeWidgets compositeWidgets) {
+ List<Control> controls = new ArrayList<>();
+ String[] ids =
+ new String[] {
+ BaseDatabaseMeta.ID_USERNAME_LABEL,
+ BaseDatabaseMeta.ID_USERNAME_WIDGET,
+ BaseDatabaseMeta.ID_PASSWORD_LABEL,
+ BaseDatabaseMeta.ID_PASSWORD_WIDGET,
+ };
+ for (String id : ids) {
+ Control control = compositeWidgets.getWidgetsMap().get(id);
+ if (control != null) {
+ controls.add(control);
+ }
+ }
+ Button wIntegratedSecurity =
+ (Button)
+ compositeWidgets
+ .getWidgetsMap()
+
.get(MsSqlServerNativeDatabaseMeta.ID_INTEGRATED_SECURITY_WIDGET);
+
+ // The checkbox is absent when it is switched off in
disabledGuiElements.xml. There is nothing
+ // to read the setting from then, so we go by what is on the metadata
itself.
+ //
+ boolean enable =
+ wIntegratedSecurity == null
+ ? !database.isUsingIntegratedSecurity()
+ : !wIntegratedSecurity.getSelection();
+ for (Control control : controls) {
+ control.setEnabled(enable);
+ }
+ }
+}
diff --git
a/plugins/databases/mssqlnative/src/test/java/org/apache/hop/databases/mssqlnative/MsSqlServerNativeDatabaseMetaHeadlessTest.java
b/plugins/databases/mssqlnative/src/test/java/org/apache/hop/databases/mssqlnative/MsSqlServerNativeDatabaseMetaHeadlessTest.java
new file mode 100644
index 0000000000..8bc1e04dae
--- /dev/null
+++
b/plugins/databases/mssqlnative/src/test/java/org/apache/hop/databases/mssqlnative/MsSqlServerNativeDatabaseMetaHeadlessTest.java
@@ -0,0 +1,82 @@
+/*
+ * 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.
+ */
+
+package org.apache.hop.databases.mssqlnative;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import java.io.File;
+import java.net.URL;
+import java.net.URLClassLoader;
+import java.util.ArrayList;
+import java.util.List;
+import org.apache.hop.core.database.IDatabase;
+import org.apache.hop.ui.core.gui.IGuiPluginCompositeWidgetsListener;
+import org.junit.jupiter.api.Test;
+
+/** The database must be usable as a library, without hop-ui or SWT on the
classpath. */
+class MsSqlServerNativeDatabaseMetaHeadlessTest {
+
+ @Test
+ void databaseLoadsWithoutTheGui() throws Exception {
+ try (URLClassLoader withoutGui = new ClassLoaderWithoutGui()) {
+ Object database =
+ Class.forName(MsSqlServerNativeDatabaseMeta.class.getName(), true,
withoutGui)
+ .getConstructor()
+ .newInstance();
+
+ assertEquals(
+ MsSqlServerNativeWidgetsListener.class.getName(),
+
database.getClass().getMethod("getGuiWidgetsListenerClassName").invoke(database));
+ }
+ }
+
+ @Test
+ void namedListenerIsCreatedForTheDatabase() throws Exception {
+ IDatabase database = new MsSqlServerNativeDatabaseMeta();
+
+ Object listener =
+ Class.forName(database.getGuiWidgetsListenerClassName())
+ .getConstructor(IDatabase.class)
+ .newInstance(database);
+
+ assertEquals(true, listener instanceof IGuiPluginCompositeWidgetsListener);
+ }
+
+ /** Loads everything from the test classpath itself, except the GUI classes.
*/
+ private static class ClassLoaderWithoutGui extends URLClassLoader {
+ ClassLoaderWithoutGui() throws Exception {
+ super(classpath(), ClassLoader.getPlatformClassLoader());
+ }
+
+ @Override
+ protected Class<?> loadClass(String name, boolean resolve) throws
ClassNotFoundException {
+ if (name.startsWith("org.apache.hop.ui.") ||
name.startsWith("org.eclipse.swt.")) {
+ throw new ClassNotFoundException(name);
+ }
+ return super.loadClass(name, resolve);
+ }
+
+ private static URL[] classpath() throws Exception {
+ List<URL> urls = new ArrayList<>();
+ for (String entry :
System.getProperty("java.class.path").split(File.pathSeparator)) {
+ urls.add(new File(entry).toURI().toURL());
+ }
+ return urls.toArray(new URL[0]);
+ }
+ }
+}
diff --git
a/plugins/databases/oracle/src/main/java/org/apache/hop/databases/oracle/OracleDatabaseMeta.java
b/plugins/databases/oracle/src/main/java/org/apache/hop/databases/oracle/OracleDatabaseMeta.java
index 0d884801f9..c94403d395 100644
---
a/plugins/databases/oracle/src/main/java/org/apache/hop/databases/oracle/OracleDatabaseMeta.java
+++
b/plugins/databases/oracle/src/main/java/org/apache/hop/databases/oracle/OracleDatabaseMeta.java
@@ -20,10 +20,8 @@ package org.apache.hop.databases.oracle;
import java.sql.ResultSet;
import java.sql.Types;
import java.util.ArrayList;
-import java.util.HashSet;
import java.util.List;
import java.util.Properties;
-import java.util.Set;
import lombok.Getter;
import lombok.Setter;
import org.apache.commons.lang3.StringUtils;
@@ -49,11 +47,6 @@ import org.apache.hop.core.row.IValueMeta;
import org.apache.hop.core.util.Utils;
import org.apache.hop.core.variables.IVariables;
import org.apache.hop.metadata.api.HopMetadataProperty;
-import org.apache.hop.ui.core.gui.GuiCompositeWidgets;
-import org.apache.hop.ui.core.gui.IGuiPluginCompositeWidgetsListener;
-import org.eclipse.swt.widgets.Button;
-import org.eclipse.swt.widgets.Combo;
-import org.eclipse.swt.widgets.Control;
/** Contains Oracle specific information through static final members */
@DatabaseMetaPlugin(
@@ -63,8 +56,7 @@ import org.eclipse.swt.widgets.Control;
documentationUrl = "/database/databases/oracle.html",
classLoaderGroup = "oracle-db")
@GuiPlugin(id = "GUI-OracleDatabaseMeta")
-public class OracleDatabaseMeta extends BaseDatabaseMeta
- implements IDatabase, IGuiPluginCompositeWidgetsListener {
+public class OracleDatabaseMeta extends BaseDatabaseMeta implements IDatabase {
/** RAW and LONG RAW arrive as variable length binary but are really
strings. */
private static final List<IDatabaseTypeRule> RAW_RULES =
@@ -696,104 +688,8 @@ public class OracleDatabaseMeta extends BaseDatabaseMeta
}
@Override
- public void widgetsCreated(GuiCompositeWidgets compositeWidgets) {
- // Nothing to do, the values aren't set yet at this point.
- }
-
- @Override
- public void widgetsPopulated(GuiCompositeWidgets compositeWidgets) {
- hideFieldsThatDoNotApply(compositeWidgets);
- }
-
- @Override
- public void widgetModified(
- GuiCompositeWidgets compositeWidgets, Control changedWidget, String
widgetId) {
- hideFieldsThatDoNotApply(compositeWidgets);
- }
-
- @Override
- public void persistContents(GuiCompositeWidgets compositeWidgets) {
- // Not needed, the dialog reads the widgets back itself.
- }
-
- /**
- * Keeps the dialog down to the fields that can actually do something for
the connection being
- * edited. Most Oracle connections are a plain host and port, and a wall of
TLS and wallet fields
- * that will only ever stay empty makes the ones that matter harder to find.
- */
- private void hideFieldsThatDoNotApply(GuiCompositeWidgets compositeWidgets) {
- OracleConnectionType type = readConnectionType(compositeWidgets);
-
- // A TNS alias and a hand written descriptor say where and how to connect
all by themselves, so
- // neither the address nor the TCPS checkbox has anything left to decide.
Their certificates
- // are still ours to pass on, which is why the credentials below stay put.
- //
- boolean addressApplies =
- type != OracleConnectionType.TNS_ALIAS && type !=
OracleConnectionType.DESCRIPTOR;
- boolean tlsApplies =
- !addressApplies || readCheckbox(compositeWidgets, ID_USE_TCPS,
isUseTcps());
- OracleTlsCredentialType credentials = readCredentialType(compositeWidgets);
-
- Set<String> hidden = new HashSet<>();
- hideUnless(hidden, addressApplies, ID_HOSTNAME, ID_PORT, ID_USE_TCPS);
- hideUnless(hidden, tlsApplies, ID_TLS_CREDENTIAL_TYPE);
- hideUnless(hidden, tlsApplies, ID_SSL_SERVER_DN_MATCH,
ID_SSL_SERVER_CERT_DN);
- hideUnless(
- hidden,
- tlsApplies && credentials == OracleTlsCredentialType.WALLET,
- ID_WALLET_DIRECTORY,
- ID_WALLET_PASSWORD);
- hideUnless(
- hidden,
- tlsApplies && credentials == OracleTlsCredentialType.JKS,
- ID_TRUST_STORE_FILE,
- ID_TRUST_STORE_PASSWORD,
- ID_TRUST_STORE_TYPE,
- ID_KEY_STORE_FILE,
- ID_KEY_STORE_PASSWORD,
- ID_KEY_STORE_TYPE);
-
- compositeWidgets.setWidgetsHidden(this, hidden);
- }
-
- private void hideUnless(Set<String> hidden, boolean applies, String...
widgetIds) {
- if (!applies) {
- hidden.addAll(List.of(widgetIds));
- }
- }
-
- private OracleConnectionType readConnectionType(GuiCompositeWidgets
compositeWidgets) {
- Control control = compositeWidgets.getWidgetsMap().get(ID_CONNECTION_TYPE);
- if (control instanceof Combo combo) {
- try {
- return OracleConnectionType.valueOf(combo.getText());
- } catch (IllegalArgumentException e) {
- // Nothing selected yet, the metadata is the better answer.
- }
- }
- return getConnectionType();
- }
-
- /**
- * Widgets can be switched off through disabledGuiElements.xml, in which
case there is nothing to
- * read the setting from and we fall back to what is on the metadata itself.
- */
- private boolean readCheckbox(
- GuiCompositeWidgets compositeWidgets, String widgetId, boolean fallback)
{
- Control control = compositeWidgets.getWidgetsMap().get(widgetId);
- return control instanceof Button button ? button.getSelection() : fallback;
- }
-
- private OracleTlsCredentialType readCredentialType(GuiCompositeWidgets
compositeWidgets) {
- Control control =
compositeWidgets.getWidgetsMap().get(ID_TLS_CREDENTIAL_TYPE);
- if (control instanceof Combo combo) {
- try {
- return OracleTlsCredentialType.valueOf(combo.getText());
- } catch (IllegalArgumentException e) {
- // Nothing selected yet, the metadata is the better answer.
- }
- }
- return getTlsCredentialType();
+ public String getGuiWidgetsListenerClassName() {
+ return "org.apache.hop.databases.oracle.OracleWidgetsListener";
}
/**
diff --git
a/plugins/databases/oracle/src/main/java/org/apache/hop/databases/oracle/OracleWidgetsListener.java
b/plugins/databases/oracle/src/main/java/org/apache/hop/databases/oracle/OracleWidgetsListener.java
new file mode 100644
index 0000000000..e3498e2c49
--- /dev/null
+++
b/plugins/databases/oracle/src/main/java/org/apache/hop/databases/oracle/OracleWidgetsListener.java
@@ -0,0 +1,150 @@
+/*
+ * 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.
+ */
+
+package org.apache.hop.databases.oracle;
+
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import org.apache.hop.core.database.IDatabase;
+import org.apache.hop.ui.core.gui.GuiCompositeWidgets;
+import org.apache.hop.ui.core.gui.IGuiPluginCompositeWidgetsListener;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Combo;
+import org.eclipse.swt.widgets.Control;
+
+/** Hides the Oracle connection options that do not apply, in the connection
dialog. */
+public class OracleWidgetsListener implements
IGuiPluginCompositeWidgetsListener {
+
+ private final OracleDatabaseMeta database;
+
+ public OracleWidgetsListener(IDatabase database) {
+ this.database = (OracleDatabaseMeta) database;
+ }
+
+ @Override
+ public void widgetsCreated(GuiCompositeWidgets compositeWidgets) {
+ // Nothing to do, the values aren't set yet at this point.
+ }
+
+ @Override
+ public void widgetsPopulated(GuiCompositeWidgets compositeWidgets) {
+ hideFieldsThatDoNotApply(compositeWidgets);
+ }
+
+ @Override
+ public void widgetModified(
+ GuiCompositeWidgets compositeWidgets, Control changedWidget, String
widgetId) {
+ hideFieldsThatDoNotApply(compositeWidgets);
+ }
+
+ @Override
+ public void persistContents(GuiCompositeWidgets compositeWidgets) {
+ // Not needed, the dialog reads the widgets back itself.
+ }
+
+ /**
+ * Keeps the dialog down to the fields that can actually do something for
the connection being
+ * edited. Most Oracle connections are a plain host and port, and a wall of
TLS and wallet fields
+ * that will only ever stay empty makes the ones that matter harder to find.
+ */
+ private void hideFieldsThatDoNotApply(GuiCompositeWidgets compositeWidgets) {
+ OracleConnectionType type = readConnectionType(compositeWidgets);
+
+ // A TNS alias and a hand written descriptor say where and how to connect
all by themselves, so
+ // neither the address nor the TCPS checkbox has anything left to decide.
Their certificates
+ // are still ours to pass on, which is why the credentials below stay put.
+ //
+ boolean addressApplies =
+ type != OracleConnectionType.TNS_ALIAS && type !=
OracleConnectionType.DESCRIPTOR;
+ boolean tlsApplies =
+ !addressApplies
+ || readCheckbox(compositeWidgets, OracleDatabaseMeta.ID_USE_TCPS,
database.isUseTcps());
+ OracleTlsCredentialType credentials = readCredentialType(compositeWidgets);
+
+ Set<String> hidden = new HashSet<>();
+ hideUnless(
+ hidden,
+ addressApplies,
+ OracleDatabaseMeta.ID_HOSTNAME,
+ OracleDatabaseMeta.ID_PORT,
+ OracleDatabaseMeta.ID_USE_TCPS);
+ hideUnless(hidden, tlsApplies, OracleDatabaseMeta.ID_TLS_CREDENTIAL_TYPE);
+ hideUnless(
+ hidden,
+ tlsApplies,
+ OracleDatabaseMeta.ID_SSL_SERVER_DN_MATCH,
+ OracleDatabaseMeta.ID_SSL_SERVER_CERT_DN);
+ hideUnless(
+ hidden,
+ tlsApplies && credentials == OracleTlsCredentialType.WALLET,
+ OracleDatabaseMeta.ID_WALLET_DIRECTORY,
+ OracleDatabaseMeta.ID_WALLET_PASSWORD);
+ hideUnless(
+ hidden,
+ tlsApplies && credentials == OracleTlsCredentialType.JKS,
+ OracleDatabaseMeta.ID_TRUST_STORE_FILE,
+ OracleDatabaseMeta.ID_TRUST_STORE_PASSWORD,
+ OracleDatabaseMeta.ID_TRUST_STORE_TYPE,
+ OracleDatabaseMeta.ID_KEY_STORE_FILE,
+ OracleDatabaseMeta.ID_KEY_STORE_PASSWORD,
+ OracleDatabaseMeta.ID_KEY_STORE_TYPE);
+
+ compositeWidgets.setWidgetsHidden(database, hidden);
+ }
+
+ private void hideUnless(Set<String> hidden, boolean applies, String...
widgetIds) {
+ if (!applies) {
+ hidden.addAll(List.of(widgetIds));
+ }
+ }
+
+ private OracleConnectionType readConnectionType(GuiCompositeWidgets
compositeWidgets) {
+ Control control =
compositeWidgets.getWidgetsMap().get(OracleDatabaseMeta.ID_CONNECTION_TYPE);
+ if (control instanceof Combo combo) {
+ try {
+ return OracleConnectionType.valueOf(combo.getText());
+ } catch (IllegalArgumentException e) {
+ // Nothing selected yet, the metadata is the better answer.
+ }
+ }
+ return database.getConnectionType();
+ }
+
+ /**
+ * Widgets can be switched off through disabledGuiElements.xml, in which
case there is nothing to
+ * read the setting from and we fall back to what is on the metadata itself.
+ */
+ private boolean readCheckbox(
+ GuiCompositeWidgets compositeWidgets, String widgetId, boolean fallback)
{
+ Control control = compositeWidgets.getWidgetsMap().get(widgetId);
+ return control instanceof Button button ? button.getSelection() : fallback;
+ }
+
+ private OracleTlsCredentialType readCredentialType(GuiCompositeWidgets
compositeWidgets) {
+ Control control =
+
compositeWidgets.getWidgetsMap().get(OracleDatabaseMeta.ID_TLS_CREDENTIAL_TYPE);
+ if (control instanceof Combo combo) {
+ try {
+ return OracleTlsCredentialType.valueOf(combo.getText());
+ } catch (IllegalArgumentException e) {
+ // Nothing selected yet, the metadata is the better answer.
+ }
+ }
+ return database.getTlsCredentialType();
+ }
+}
diff --git
a/plugins/databases/oracle/src/test/java/org/apache/hop/databases/oracle/OracleDatabaseMetaHeadlessTest.java
b/plugins/databases/oracle/src/test/java/org/apache/hop/databases/oracle/OracleDatabaseMetaHeadlessTest.java
new file mode 100644
index 0000000000..9d7758922a
--- /dev/null
+++
b/plugins/databases/oracle/src/test/java/org/apache/hop/databases/oracle/OracleDatabaseMetaHeadlessTest.java
@@ -0,0 +1,82 @@
+/*
+ * 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.
+ */
+
+package org.apache.hop.databases.oracle;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import java.io.File;
+import java.net.URL;
+import java.net.URLClassLoader;
+import java.util.ArrayList;
+import java.util.List;
+import org.apache.hop.core.database.IDatabase;
+import org.apache.hop.ui.core.gui.IGuiPluginCompositeWidgetsListener;
+import org.junit.jupiter.api.Test;
+
+/** The database must be usable as a library, without hop-ui or SWT on the
classpath. */
+class OracleDatabaseMetaHeadlessTest {
+
+ @Test
+ void databaseLoadsWithoutTheGui() throws Exception {
+ try (URLClassLoader withoutGui = new ClassLoaderWithoutGui()) {
+ Object database =
+ Class.forName(OracleDatabaseMeta.class.getName(), true, withoutGui)
+ .getConstructor()
+ .newInstance();
+
+ assertEquals(
+ OracleWidgetsListener.class.getName(),
+
database.getClass().getMethod("getGuiWidgetsListenerClassName").invoke(database));
+ }
+ }
+
+ @Test
+ void namedListenerIsCreatedForTheDatabase() throws Exception {
+ IDatabase database = new OracleDatabaseMeta();
+
+ Object listener =
+ Class.forName(database.getGuiWidgetsListenerClassName())
+ .getConstructor(IDatabase.class)
+ .newInstance(database);
+
+ assertEquals(true, listener instanceof IGuiPluginCompositeWidgetsListener);
+ }
+
+ /** Loads everything from the test classpath itself, except the GUI classes.
*/
+ private static class ClassLoaderWithoutGui extends URLClassLoader {
+ ClassLoaderWithoutGui() throws Exception {
+ super(classpath(), ClassLoader.getPlatformClassLoader());
+ }
+
+ @Override
+ protected Class<?> loadClass(String name, boolean resolve) throws
ClassNotFoundException {
+ if (name.startsWith("org.apache.hop.ui.") ||
name.startsWith("org.eclipse.swt.")) {
+ throw new ClassNotFoundException(name);
+ }
+ return super.loadClass(name, resolve);
+ }
+
+ private static URL[] classpath() throws Exception {
+ List<URL> urls = new ArrayList<>();
+ for (String entry :
System.getProperty("java.class.path").split(File.pathSeparator)) {
+ urls.add(new File(entry).toURI().toURL());
+ }
+ return urls.toArray(new URL[0]);
+ }
+ }
+}
diff --git
a/plugins/databases/oracle/src/test/java/org/apache/hop/databases/oracle/OracleWidgetVisibilityTest.java
b/plugins/databases/oracle/src/test/java/org/apache/hop/databases/oracle/OracleWidgetVisibilityTest.java
index 57e0d846ad..b02e5ee044 100644
---
a/plugins/databases/oracle/src/test/java/org/apache/hop/databases/oracle/OracleWidgetVisibilityTest.java
+++
b/plugins/databases/oracle/src/test/java/org/apache/hop/databases/oracle/OracleWidgetVisibilityTest.java
@@ -202,7 +202,8 @@ class OracleWidgetVisibilityTest extends SwtBotTestBase {
Combo credentials =
(Combo)
widgets.getWidgetsMap().get(OracleDatabaseMeta.ID_TLS_CREDENTIAL_TYPE);
credentials.setText(OracleTlsCredentialType.JKS.name());
- meta.widgetModified(widgets, credentials,
OracleDatabaseMeta.ID_TLS_CREDENTIAL_TYPE);
+ new OracleWidgetsListener(meta)
+ .widgetModified(widgets, credentials,
OracleDatabaseMeta.ID_TLS_CREDENTIAL_TYPE);
assertHangsBelow(
widgets,
OracleDatabaseMeta.ID_SSL_SERVER_DN_MATCH,
@@ -262,8 +263,9 @@ class OracleWidgetVisibilityTest extends SwtBotTestBase {
widgets.createCompositeWidgets(
meta, null, inner, DatabaseMeta.GUI_PLUGIN_ELEMENT_PARENT_ID, null);
widgets.setWidgetsContents(meta, inner,
DatabaseMeta.GUI_PLUGIN_ELEMENT_PARENT_ID);
- widgets.setWidgetsListener(meta);
- meta.widgetsPopulated(widgets);
+ OracleWidgetsListener listener = new OracleWidgetsListener(meta);
+ widgets.setWidgetsListener(listener);
+ listener.widgetsPopulated(widgets);
shell.layout(true, true);
shell.pack();
@@ -331,12 +333,14 @@ class OracleWidgetVisibilityTest extends SwtBotTestBase {
Button useTcps = (Button)
widgets.getWidgetsMap().get(OracleDatabaseMeta.ID_USE_TCPS);
useTcps.setSelection(false);
- meta.widgetModified(widgets, useTcps,
OracleDatabaseMeta.ID_USE_TCPS);
+ new OracleWidgetsListener(meta)
+ .widgetModified(widgets, useTcps,
OracleDatabaseMeta.ID_USE_TCPS);
assertHidden(widgets, OracleDatabaseMeta.ID_WALLET_DIRECTORY);
assertHidden(widgets, OracleDatabaseMeta.ID_TLS_CREDENTIAL_TYPE);
useTcps.setSelection(true);
- meta.widgetModified(widgets, useTcps,
OracleDatabaseMeta.ID_USE_TCPS);
+ new OracleWidgetsListener(meta)
+ .widgetModified(widgets, useTcps,
OracleDatabaseMeta.ID_USE_TCPS);
assertVisible(widgets, OracleDatabaseMeta.ID_WALLET_DIRECTORY);
assertVisible(widgets, OracleDatabaseMeta.ID_TLS_CREDENTIAL_TYPE);
});
@@ -362,8 +366,9 @@ class OracleWidgetVisibilityTest extends SwtBotTestBase {
widgets.createCompositeWidgets(
meta, null, shell, DatabaseMeta.GUI_PLUGIN_ELEMENT_PARENT_ID, null);
widgets.setWidgetsContents(meta, shell,
DatabaseMeta.GUI_PLUGIN_ELEMENT_PARENT_ID);
- widgets.setWidgetsListener(meta);
- meta.widgetsPopulated(widgets);
+ OracleWidgetsListener listener = new OracleWidgetsListener(meta);
+ widgets.setWidgetsListener(listener);
+ listener.widgetsPopulated(widgets);
shell.layout(true, true);
shell.pack();
@@ -439,7 +444,7 @@ class OracleWidgetVisibilityTest extends SwtBotTestBase {
GuiCompositeWidgets widgets = new GuiCompositeWidgets(new Variables());
widgets.createCompositeWidgets(
meta, null, shell, DatabaseMeta.GUI_PLUGIN_ELEMENT_PARENT_ID, null);
- widgets.setWidgetsListener(meta);
+ widgets.setWidgetsListener(new OracleWidgetsListener(meta));
// setWidgetsContents() calls Combo.setText(), which fires a Modify
event, which sends the
// editor straight back into getWidgetsContents() while the rest of the
widgets are still
@@ -500,7 +505,8 @@ class OracleWidgetVisibilityTest extends SwtBotTestBase {
(meta, widgets) -> {
Combo combo = (Combo)
widgets.getWidgetsMap().get(OracleDatabaseMeta.ID_CONNECTION_TYPE);
combo.setText(OracleConnectionType.SERVICE_NAME.name());
- meta.widgetModified(widgets, combo,
OracleDatabaseMeta.ID_CONNECTION_TYPE);
+ new OracleWidgetsListener(meta)
+ .widgetModified(widgets, combo,
OracleDatabaseMeta.ID_CONNECTION_TYPE);
// getWidgetsContents() is what the Test button runs before it
builds the URL, so this is
// the path between the combo and what actually gets connected to.
diff --git
a/plugins/databases/redshift/src/main/java/org/apache/hop/databases/redshift/RedshiftDatabaseMeta.java
b/plugins/databases/redshift/src/main/java/org/apache/hop/databases/redshift/RedshiftDatabaseMeta.java
index 9dcc006a25..cf3af3b578 100644
---
a/plugins/databases/redshift/src/main/java/org/apache/hop/databases/redshift/RedshiftDatabaseMeta.java
+++
b/plugins/databases/redshift/src/main/java/org/apache/hop/databases/redshift/RedshiftDatabaseMeta.java
@@ -16,10 +16,8 @@
*/
package org.apache.hop.databases.redshift;
-import java.util.HashSet;
import java.util.List;
import java.util.Properties;
-import java.util.Set;
import lombok.Getter;
import lombok.Setter;
import org.apache.commons.lang3.StringUtils;
@@ -39,10 +37,6 @@ import org.apache.hop.core.row.IValueMeta;
import org.apache.hop.core.variables.IVariables;
import org.apache.hop.databases.postgresql.PostgreSqlDatabaseMeta;
import org.apache.hop.metadata.api.HopMetadataProperty;
-import org.apache.hop.ui.core.gui.GuiCompositeWidgets;
-import org.apache.hop.ui.core.gui.IGuiPluginCompositeWidgetsListener;
-import org.eclipse.swt.widgets.Combo;
-import org.eclipse.swt.widgets.Control;
@DatabaseMetaPlugin(
type = "REDSHIFT",
@@ -51,8 +45,7 @@ import org.eclipse.swt.widgets.Control;
documentationUrl = "/database/databases/redshift.html",
classLoaderGroup = "redshift-db")
@GuiPlugin(id = "GUI-RedshiftDatabaseMeta")
-public class RedshiftDatabaseMeta extends PostgreSqlDatabaseMeta
- implements IGuiPluginCompositeWidgetsListener {
+public class RedshiftDatabaseMeta extends PostgreSqlDatabaseMeta {
/** The host name of every serverless workgroup ends in this. */
public static final String SERVERLESS_HOST_SUFFIX =
".redshift-serverless.amazonaws.com";
@@ -458,86 +451,7 @@ public class RedshiftDatabaseMeta extends
PostgreSqlDatabaseMeta
}
@Override
- public void widgetsCreated(GuiCompositeWidgets compositeWidgets) {
- // Nothing to do, the values aren't set yet at this point.
- }
-
- @Override
- public void widgetsPopulated(GuiCompositeWidgets compositeWidgets) {
- hideFieldsThatDoNotApply(compositeWidgets);
- }
-
- @Override
- public void widgetModified(
- GuiCompositeWidgets compositeWidgets, Control changedWidget, String
widgetId) {
- hideFieldsThatDoNotApply(compositeWidgets);
- }
-
- @Override
- public void persistContents(GuiCompositeWidgets compositeWidgets) {
- // Not needed, the dialog reads the widgets back itself.
- }
-
- /**
- * Keeps the dialog down to the fields that can actually do something for
the connection being
- * edited. Between two deployments and four ways to authenticate, showing
everything at once would
- * leave most of the tab permanently empty and the fields that matter hard
to pick out.
- */
- private void hideFieldsThatDoNotApply(GuiCompositeWidgets compositeWidgets) {
- RedshiftDeploymentType deployment = readDeploymentType(compositeWidgets);
- RedshiftAuthenticationType authentication =
readAuthenticationType(compositeWidgets);
- boolean serverless = deployment == RedshiftDeploymentType.SERVERLESS;
-
- Set<String> hidden = new HashSet<>();
- // The host name stays on offer for a serverless workgroup: left empty Hop
builds the endpoint,
- // filled in it wins, which is how a custom domain name or a load balancer
is reached.
- hideUnless(hidden, serverless, ID_WORKGROUP, ID_ACCOUNT_ID);
- // A cluster only has to name itself when IAM has to find it.
- hideUnless(hidden, !serverless && authentication.isIam(), ID_CLUSTER_ID);
- // The region builds the host name, and IAM needs it to know which AWS to
ask.
- hideUnless(hidden, serverless || authentication.isIam(), ID_AWS_REGION);
-
- hideUnless(
- hidden,
- authentication == RedshiftAuthenticationType.IAM_CREDENTIALS,
- ID_AWS_ACCESS_KEY_ID,
- ID_AWS_SECRET_ACCESS_KEY,
- ID_AWS_SESSION_TOKEN);
- hideUnless(hidden, authentication ==
RedshiftAuthenticationType.IAM_PROFILE, ID_AWS_PROFILE);
- // Only a provisioned cluster maps IAM onto a named database user.
- hideUnless(
- hidden, authentication.isIam() && !serverless, ID_DB_USER,
ID_DB_GROUPS, ID_AUTO_CREATE);
-
- compositeWidgets.setWidgetsHidden(this, hidden);
- }
-
- private void hideUnless(Set<String> hidden, boolean applies, String...
widgetIds) {
- if (!applies) {
- hidden.addAll(List.of(widgetIds));
- }
- }
-
- private RedshiftDeploymentType readDeploymentType(GuiCompositeWidgets
compositeWidgets) {
- Control control = compositeWidgets.getWidgetsMap().get(ID_DEPLOYMENT_TYPE);
- if (control instanceof Combo combo) {
- try {
- return RedshiftDeploymentType.valueOf(combo.getText());
- } catch (IllegalArgumentException e) {
- // Nothing selected yet, the metadata is the better answer.
- }
- }
- return getDeploymentType();
- }
-
- private RedshiftAuthenticationType
readAuthenticationType(GuiCompositeWidgets compositeWidgets) {
- Control control =
compositeWidgets.getWidgetsMap().get(ID_AUTHENTICATION_TYPE);
- if (control instanceof Combo combo) {
- try {
- return RedshiftAuthenticationType.valueOf(combo.getText());
- } catch (IllegalArgumentException e) {
- // Nothing selected yet, the metadata is the better answer.
- }
- }
- return getAuthenticationType();
+ public String getGuiWidgetsListenerClassName() {
+ return "org.apache.hop.databases.redshift.RedshiftWidgetsListener";
}
}
diff --git
a/plugins/databases/redshift/src/main/java/org/apache/hop/databases/redshift/RedshiftWidgetsListener.java
b/plugins/databases/redshift/src/main/java/org/apache/hop/databases/redshift/RedshiftWidgetsListener.java
new file mode 100644
index 0000000000..53e82dbaea
--- /dev/null
+++
b/plugins/databases/redshift/src/main/java/org/apache/hop/databases/redshift/RedshiftWidgetsListener.java
@@ -0,0 +1,130 @@
+/*
+ * 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.
+ */
+
+package org.apache.hop.databases.redshift;
+
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import org.apache.hop.core.database.IDatabase;
+import org.apache.hop.ui.core.gui.GuiCompositeWidgets;
+import org.apache.hop.ui.core.gui.IGuiPluginCompositeWidgetsListener;
+import org.eclipse.swt.widgets.Combo;
+import org.eclipse.swt.widgets.Control;
+
+/** Hides the Redshift connection options that do not apply, in the connection
dialog. */
+public class RedshiftWidgetsListener implements
IGuiPluginCompositeWidgetsListener {
+
+ private final RedshiftDatabaseMeta database;
+
+ public RedshiftWidgetsListener(IDatabase database) {
+ this.database = (RedshiftDatabaseMeta) database;
+ }
+
+ @Override
+ public void widgetsCreated(GuiCompositeWidgets compositeWidgets) {
+ // Nothing to do, the values aren't set yet at this point.
+ }
+
+ @Override
+ public void widgetsPopulated(GuiCompositeWidgets compositeWidgets) {
+ hideFieldsThatDoNotApply(compositeWidgets);
+ }
+
+ @Override
+ public void widgetModified(
+ GuiCompositeWidgets compositeWidgets, Control changedWidget, String
widgetId) {
+ hideFieldsThatDoNotApply(compositeWidgets);
+ }
+
+ @Override
+ public void persistContents(GuiCompositeWidgets compositeWidgets) {
+ // Not needed, the dialog reads the widgets back itself.
+ }
+
+ /**
+ * Keeps the dialog down to the fields that can actually do something for
the connection being
+ * edited. Between two deployments and four ways to authenticate, showing
everything at once would
+ * leave most of the tab permanently empty and the fields that matter hard
to pick out.
+ */
+ private void hideFieldsThatDoNotApply(GuiCompositeWidgets compositeWidgets) {
+ RedshiftDeploymentType deployment = readDeploymentType(compositeWidgets);
+ RedshiftAuthenticationType authentication =
readAuthenticationType(compositeWidgets);
+ boolean serverless = deployment == RedshiftDeploymentType.SERVERLESS;
+
+ Set<String> hidden = new HashSet<>();
+ // The host name stays on offer for a serverless workgroup: left empty Hop
builds the endpoint,
+ // filled in it wins, which is how a custom domain name or a load balancer
is reached.
+ hideUnless(
+ hidden, serverless, RedshiftDatabaseMeta.ID_WORKGROUP,
RedshiftDatabaseMeta.ID_ACCOUNT_ID);
+ // A cluster only has to name itself when IAM has to find it.
+ hideUnless(hidden, !serverless && authentication.isIam(),
RedshiftDatabaseMeta.ID_CLUSTER_ID);
+ // The region builds the host name, and IAM needs it to know which AWS to
ask.
+ hideUnless(hidden, serverless || authentication.isIam(),
RedshiftDatabaseMeta.ID_AWS_REGION);
+
+ hideUnless(
+ hidden,
+ authentication == RedshiftAuthenticationType.IAM_CREDENTIALS,
+ RedshiftDatabaseMeta.ID_AWS_ACCESS_KEY_ID,
+ RedshiftDatabaseMeta.ID_AWS_SECRET_ACCESS_KEY,
+ RedshiftDatabaseMeta.ID_AWS_SESSION_TOKEN);
+ hideUnless(
+ hidden,
+ authentication == RedshiftAuthenticationType.IAM_PROFILE,
+ RedshiftDatabaseMeta.ID_AWS_PROFILE);
+ // Only a provisioned cluster maps IAM onto a named database user.
+ hideUnless(
+ hidden,
+ authentication.isIam() && !serverless,
+ RedshiftDatabaseMeta.ID_DB_USER,
+ RedshiftDatabaseMeta.ID_DB_GROUPS,
+ RedshiftDatabaseMeta.ID_AUTO_CREATE);
+
+ compositeWidgets.setWidgetsHidden(database, hidden);
+ }
+
+ private void hideUnless(Set<String> hidden, boolean applies, String...
widgetIds) {
+ if (!applies) {
+ hidden.addAll(List.of(widgetIds));
+ }
+ }
+
+ private RedshiftDeploymentType readDeploymentType(GuiCompositeWidgets
compositeWidgets) {
+ Control control =
compositeWidgets.getWidgetsMap().get(RedshiftDatabaseMeta.ID_DEPLOYMENT_TYPE);
+ if (control instanceof Combo combo) {
+ try {
+ return RedshiftDeploymentType.valueOf(combo.getText());
+ } catch (IllegalArgumentException e) {
+ // Nothing selected yet, the metadata is the better answer.
+ }
+ }
+ return database.getDeploymentType();
+ }
+
+ private RedshiftAuthenticationType
readAuthenticationType(GuiCompositeWidgets compositeWidgets) {
+ Control control =
+
compositeWidgets.getWidgetsMap().get(RedshiftDatabaseMeta.ID_AUTHENTICATION_TYPE);
+ if (control instanceof Combo combo) {
+ try {
+ return RedshiftAuthenticationType.valueOf(combo.getText());
+ } catch (IllegalArgumentException e) {
+ // Nothing selected yet, the metadata is the better answer.
+ }
+ }
+ return database.getAuthenticationType();
+ }
+}
diff --git
a/plugins/databases/redshift/src/test/java/org/apache/hop/databases/redshift/RedshiftDatabaseMetaHeadlessTest.java
b/plugins/databases/redshift/src/test/java/org/apache/hop/databases/redshift/RedshiftDatabaseMetaHeadlessTest.java
new file mode 100644
index 0000000000..f50e117bcf
--- /dev/null
+++
b/plugins/databases/redshift/src/test/java/org/apache/hop/databases/redshift/RedshiftDatabaseMetaHeadlessTest.java
@@ -0,0 +1,82 @@
+/*
+ * 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.
+ */
+
+package org.apache.hop.databases.redshift;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import java.io.File;
+import java.net.URL;
+import java.net.URLClassLoader;
+import java.util.ArrayList;
+import java.util.List;
+import org.apache.hop.core.database.IDatabase;
+import org.apache.hop.ui.core.gui.IGuiPluginCompositeWidgetsListener;
+import org.junit.jupiter.api.Test;
+
+/** The database must be usable as a library, without hop-ui or SWT on the
classpath. */
+class RedshiftDatabaseMetaHeadlessTest {
+
+ @Test
+ void databaseLoadsWithoutTheGui() throws Exception {
+ try (URLClassLoader withoutGui = new ClassLoaderWithoutGui()) {
+ Object database =
+ Class.forName(RedshiftDatabaseMeta.class.getName(), true, withoutGui)
+ .getConstructor()
+ .newInstance();
+
+ assertEquals(
+ RedshiftWidgetsListener.class.getName(),
+
database.getClass().getMethod("getGuiWidgetsListenerClassName").invoke(database));
+ }
+ }
+
+ @Test
+ void namedListenerIsCreatedForTheDatabase() throws Exception {
+ IDatabase database = new RedshiftDatabaseMeta();
+
+ Object listener =
+ Class.forName(database.getGuiWidgetsListenerClassName())
+ .getConstructor(IDatabase.class)
+ .newInstance(database);
+
+ assertEquals(true, listener instanceof IGuiPluginCompositeWidgetsListener);
+ }
+
+ /** Loads everything from the test classpath itself, except the GUI classes.
*/
+ private static class ClassLoaderWithoutGui extends URLClassLoader {
+ ClassLoaderWithoutGui() throws Exception {
+ super(classpath(), ClassLoader.getPlatformClassLoader());
+ }
+
+ @Override
+ protected Class<?> loadClass(String name, boolean resolve) throws
ClassNotFoundException {
+ if (name.startsWith("org.apache.hop.ui.") ||
name.startsWith("org.eclipse.swt.")) {
+ throw new ClassNotFoundException(name);
+ }
+ return super.loadClass(name, resolve);
+ }
+
+ private static URL[] classpath() throws Exception {
+ List<URL> urls = new ArrayList<>();
+ for (String entry :
System.getProperty("java.class.path").split(File.pathSeparator)) {
+ urls.add(new File(entry).toURI().toURL());
+ }
+ return urls.toArray(new URL[0]);
+ }
+ }
+}
diff --git
a/ui/src/main/java/org/apache/hop/ui/core/database/DatabaseMetaEditor.java
b/ui/src/main/java/org/apache/hop/ui/core/database/DatabaseMetaEditor.java
index c3da2ee969..4e590a0193 100644
--- a/ui/src/main/java/org/apache/hop/ui/core/database/DatabaseMetaEditor.java
+++ b/ui/src/main/java/org/apache/hop/ui/core/database/DatabaseMetaEditor.java
@@ -108,6 +108,8 @@ public class DatabaseMetaEditor extends
MetadataEditor<DatabaseMeta> {
private Composite wDatabaseSpecificComp;
private GuiCompositeWidgets guiCompositeWidgets;
+ private IDatabase pluginListenerDatabase;
+ private IGuiPluginCompositeWidgetsListener pluginListener;
private Button wSupportsBoolean;
private Button wSupportsTimestamp;
@@ -486,8 +488,9 @@ public class DatabaseMetaEditor extends
MetadataEditor<DatabaseMeta> {
/**
* The composite has a single listener slot, which this editor needs for its
own change tracking.
- * Database plugins that implement {@link
IGuiPluginCompositeWidgetsListener} -- to enable, hide
- * or otherwise adjust their own widgets -- are forwarded to from here;
without this their
+ * Database plugins that adjust their own widgets -- to enable, hide or
otherwise change them --
+ * through {@link IDatabase#getGuiWidgetsListenerClassName()}, or by
implementing {@link
+ * IGuiPluginCompositeWidgetsListener} themselves, are forwarded to from
here; without this their
* callbacks would never fire.
*/
private IGuiPluginCompositeWidgetsListener createWidgetsListener() {
@@ -524,9 +527,33 @@ public class DatabaseMetaEditor extends
MetadataEditor<DatabaseMeta> {
if (databaseMeta == null) {
return Optional.empty();
}
- return databaseMeta.getIDatabase() instanceof
IGuiPluginCompositeWidgetsListener listener
- ? Optional.of(listener)
- : Optional.empty();
+ IDatabase iDatabase = databaseMeta.getIDatabase();
+ if (iDatabase instanceof IGuiPluginCompositeWidgetsListener listener) {
+ return Optional.of(listener);
+ }
+ // The listener belongs to one database object: make a new one when the
type is changed.
+ if (iDatabase != pluginListenerDatabase) {
+ pluginListenerDatabase = iDatabase;
+ pluginListener = createPluginWidgetsListener(iDatabase);
+ }
+ return Optional.ofNullable(pluginListener);
+ }
+
+ private IGuiPluginCompositeWidgetsListener
createPluginWidgetsListener(IDatabase iDatabase) {
+ String className = iDatabase == null ? null :
iDatabase.getGuiWidgetsListenerClassName();
+ if (Utils.isEmpty(className)) {
+ return null;
+ }
+ try {
+ Class<?> listenerClass =
iDatabase.getClass().getClassLoader().loadClass(className);
+ return (IGuiPluginCompositeWidgetsListener)
+ listenerClass.getConstructor(IDatabase.class).newInstance(iDatabase);
+ } catch (Exception e) {
+ HopGui.getInstance()
+ .getLog()
+ .logError("Error creating the widgets listener " + className + " for
the database", e);
+ return null;
+ }
}
private void addDefaultPortButton() {
diff --git
a/ui/src/test/java/org/apache/hop/ui/testing/DisabledGuiWidgetsTestBase.java
b/ui/src/test/java/org/apache/hop/ui/testing/DisabledGuiWidgetsTestBase.java
index 1a4cb7e82e..34f25a1f56 100644
--- a/ui/src/test/java/org/apache/hop/ui/testing/DisabledGuiWidgetsTestBase.java
+++ b/ui/src/test/java/org/apache/hop/ui/testing/DisabledGuiWidgetsTestBase.java
@@ -29,10 +29,12 @@ import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.Stream;
+import org.apache.hop.core.database.IDatabase;
import org.apache.hop.core.gui.plugin.GuiElements;
import org.apache.hop.core.gui.plugin.GuiRegistry;
import org.apache.hop.core.gui.plugin.menu.GuiMenuItem;
import org.apache.hop.core.gui.plugin.toolbar.GuiToolbarItem;
+import org.apache.hop.core.util.Utils;
import org.apache.hop.core.variables.Variables;
import org.apache.hop.ui.core.gui.GuiCompositeWidgets;
import org.apache.hop.ui.core.gui.GuiMenuWidgets;
@@ -324,7 +326,7 @@ public abstract class DisabledGuiWidgetsTestBase extends
SwtBotTestBase {
widgets.createCompositeWidgets(source, null, shell, parentId, null);
widgets.setWidgetsContents(source, shell, parentId);
- if (source instanceof IGuiPluginCompositeWidgetsListener listener) {
+ if (listenerFor(source) instanceof IGuiPluginCompositeWidgetsListener
listener) {
widgets.setWidgetsListener(listener);
listener.widgetsCreated(widgets);
listener.widgetsPopulated(widgets);
@@ -347,6 +349,27 @@ public abstract class DisabledGuiWidgetsTestBase extends
SwtBotTestBase {
}
}
+ /**
+ * The object that reacts to the widgets: the source object itself, or for a
database the listener
+ * it names, created the way {@code DatabaseMetaEditor} does it.
+ */
+ private static Object listenerFor(Object source) {
+ if (source instanceof IDatabase database
+ && !Utils.isEmpty(database.getGuiWidgetsListenerClassName())) {
+ try {
+ return database
+ .getClass()
+ .getClassLoader()
+ .loadClass(database.getGuiWidgetsListenerClassName())
+ .getConstructor(IDatabase.class)
+ .newInstance(database);
+ } catch (ReflectiveOperationException e) {
+ throw new IllegalStateException(e);
+ }
+ }
+ return source;
+ }
+
/**
* Builds the toolbar the way a perspective or dialog does, minus the
disabled item.
*