This is an automated email from the ASF dual-hosted git repository.

asf-gitbox-commits pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/cayenne.git

commit dc096d569f994a408ae469ccc2ae85d4d2f089c1
Author: Andrus Adamchik <[email protected]>
AuthorDate: Sat May 23 17:08:59 2026 -0400

    CAY-2949 CayenneModeler MCP: dbimport_run tool
    
    preliminary refactoring - sharing preferences between MCP and the Modeler
    * cleanup
    * tests
---
 .../apache/cayenne/modeler/pref/PrefsAdapter.java  |   8 ++
 ...BConnectorPrefs.java => DBConnectorsPrefs.java} |   4 +-
 .../modeler/pref/dbconnector/DBConnector.java      |   5 +-
 .../modeler/pref/dbconnector/DBConnectors.java     |   4 +-
 .../cayenne/modeler/pref/PrefsLocatorTest.java     |  98 ++++++++++++++
 .../modeler/pref/adapters/ClasspathPrefsTest.java  |  98 ++++++++++++++
 .../pref/adapters/DBConnectorsPrefsTest.java       | 142 +++++++++++++++++++++
 .../modeler/pref/dbconnector/DBConnectorTest.java  | 118 +++++++++++++++++
 .../modeler/pref/dbconnector/DBConnectorsTest.java | 138 ++++++++++++++++++++
 .../org/apache/cayenne/modeler/Application.java    |   4 +-
 .../modeler/pref/adapters/DataMapPrefs.java        |  26 +---
 11 files changed, 617 insertions(+), 28 deletions(-)

diff --git 
a/modeler/cayenne-modeler-prefs/src/main/java/org/apache/cayenne/modeler/pref/PrefsAdapter.java
 
b/modeler/cayenne-modeler-prefs/src/main/java/org/apache/cayenne/modeler/pref/PrefsAdapter.java
index 3fb1d3952..e4f3f55f1 100644
--- 
a/modeler/cayenne-modeler-prefs/src/main/java/org/apache/cayenne/modeler/pref/PrefsAdapter.java
+++ 
b/modeler/cayenne-modeler-prefs/src/main/java/org/apache/cayenne/modeler/pref/PrefsAdapter.java
@@ -31,4 +31,12 @@ public abstract class PrefsAdapter {
     protected PrefsAdapter(Preferences prefs) {
         this.prefs = prefs;
     }
+
+    protected void putOrRemove(String key, String value) {
+        if (value == null) {
+            prefs.remove(key);
+        } else {
+            prefs.put(key, value);
+        }
+    }
 }
diff --git 
a/modeler/cayenne-modeler-prefs/src/main/java/org/apache/cayenne/modeler/pref/adapters/DBConnectorPrefs.java
 
b/modeler/cayenne-modeler-prefs/src/main/java/org/apache/cayenne/modeler/pref/adapters/DBConnectorsPrefs.java
similarity index 97%
rename from 
modeler/cayenne-modeler-prefs/src/main/java/org/apache/cayenne/modeler/pref/adapters/DBConnectorPrefs.java
rename to 
modeler/cayenne-modeler-prefs/src/main/java/org/apache/cayenne/modeler/pref/adapters/DBConnectorsPrefs.java
index 21c42bb34..d66eb52d5 100644
--- 
a/modeler/cayenne-modeler-prefs/src/main/java/org/apache/cayenne/modeler/pref/adapters/DBConnectorPrefs.java
+++ 
b/modeler/cayenne-modeler-prefs/src/main/java/org/apache/cayenne/modeler/pref/adapters/DBConnectorsPrefs.java
@@ -31,7 +31,7 @@ import java.util.prefs.Preferences;
  * Loads {@link DBConnectors} from the user prefs and binds the registry's
  * change events to the prefs node, so registry mutations are mirrored to disk.
  */
-public class DBConnectorPrefs extends PrefsAdapter {
+public class DBConnectorsPrefs extends PrefsAdapter {
 
     public static final String NODE = "dbConnectors";
 
@@ -41,7 +41,7 @@ public class DBConnectorPrefs extends PrefsAdapter {
     private static final String URL_PROPERTY = "url";
     private static final String USER_NAME_PROPERTY = "userName";
 
-    public DBConnectorPrefs(Preferences prefs) {
+    public DBConnectorsPrefs(Preferences prefs) {
         super(prefs);
     }
 
diff --git 
a/modeler/cayenne-modeler-prefs/src/main/java/org/apache/cayenne/modeler/pref/dbconnector/DBConnector.java
 
b/modeler/cayenne-modeler-prefs/src/main/java/org/apache/cayenne/modeler/pref/dbconnector/DBConnector.java
index b5db35869..b11b0eab7 100644
--- 
a/modeler/cayenne-modeler-prefs/src/main/java/org/apache/cayenne/modeler/pref/dbconnector/DBConnector.java
+++ 
b/modeler/cayenne-modeler-prefs/src/main/java/org/apache/cayenne/modeler/pref/dbconnector/DBConnector.java
@@ -30,7 +30,6 @@ import org.apache.cayenne.util.Util;
  */
 public class DBConnector {
 
-    private static final String EMPTY_STRING = "";
     public static final String DB_ADAPTER_PROPERTY = "dbAdapter";
     public static final String JDBC_DRIVER_PROPERTY = "jdbcDriver";
     public static final String PASSWORD_PROPERTY = "password";
@@ -60,7 +59,7 @@ public class DBConnector {
     }
 
     public String getPassword() {
-        return password == null ? EMPTY_STRING : password;
+        return password;
     }
 
     public void setPassword(final String password) {
@@ -76,7 +75,7 @@ public class DBConnector {
     }
 
     public String getUserName() {
-        return userName == null ? EMPTY_STRING : userName;
+        return userName;
     }
 
     public void setUserName(final String userName) {
diff --git 
a/modeler/cayenne-modeler-prefs/src/main/java/org/apache/cayenne/modeler/pref/dbconnector/DBConnectors.java
 
b/modeler/cayenne-modeler-prefs/src/main/java/org/apache/cayenne/modeler/pref/dbconnector/DBConnectors.java
index b694df69b..68b7bb602 100644
--- 
a/modeler/cayenne-modeler-prefs/src/main/java/org/apache/cayenne/modeler/pref/dbconnector/DBConnectors.java
+++ 
b/modeler/cayenne-modeler-prefs/src/main/java/org/apache/cayenne/modeler/pref/dbconnector/DBConnectors.java
@@ -19,7 +19,7 @@
 
 package org.apache.cayenne.modeler.pref.dbconnector;
 
-import org.apache.cayenne.modeler.pref.adapters.DBConnectorPrefs;
+import org.apache.cayenne.modeler.pref.adapters.DBConnectorsPrefs;
 
 import java.util.ArrayList;
 import java.util.Collections;
@@ -29,7 +29,7 @@ import java.util.Map;
 
 /**
  * App-wide registry of named local DB connection profiles. Source of truth at 
runtime;
- * fires events on add/remove so listeners (e.g. {@link DBConnectorPrefs}) can 
mirror
+ * fires events on add/remove so listeners (e.g. {@link DBConnectorsPrefs}) 
can mirror
  * state to persistent storage.
  */
 public class DBConnectors {
diff --git 
a/modeler/cayenne-modeler-prefs/src/test/java/org/apache/cayenne/modeler/pref/PrefsLocatorTest.java
 
b/modeler/cayenne-modeler-prefs/src/test/java/org/apache/cayenne/modeler/pref/PrefsLocatorTest.java
new file mode 100644
index 000000000..91588bc9e
--- /dev/null
+++ 
b/modeler/cayenne-modeler-prefs/src/test/java/org/apache/cayenne/modeler/pref/PrefsLocatorTest.java
@@ -0,0 +1,98 @@
+/*****************************************************************
+ *   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
+ *
+ *    https://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.cayenne.modeler.pref;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+
+import java.util.UUID;
+import java.util.prefs.BackingStoreException;
+import java.util.prefs.Preferences;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class PrefsLocatorTest {
+
+    private final Preferences root = Preferences.userRoot()
+            .node("cayenne-test/" + UUID.randomUUID().toString().replace("-", 
""));
+    private final PrefsLocator locator = new PrefsLocator(root);
+
+    @AfterEach
+    public void cleanup() throws BackingStoreException {
+        root.removeNode();
+    }
+
+    @Test
+    public void modelerRootIsUnderAllRoot() {
+        assertTrue(locator.modelerRoot().absolutePath().endsWith("/v5"));
+    }
+
+    @Test
+    public void appNodeNullReturnsAppRoot() {
+        assertTrue(locator.appNode(null).absolutePath().endsWith("/v5/app"));
+    }
+
+    @Test
+    public void appNodeEmptyReturnsAppRoot() {
+        assertTrue(locator.appNode("").absolutePath().endsWith("/v5/app"));
+    }
+
+    @Test
+    public void appNodeDescendsIntoSubtree() {
+        
assertTrue(locator.appNode("foo/bar").absolutePath().endsWith("/v5/app/foo/bar"));
+    }
+
+    @Test
+    public void projectNodeIncludesId() {
+        
assertTrue(locator.projectNode("abc123").absolutePath().endsWith("/v5/project/abc123"));
+    }
+
+    @Test
+    public void dataMapNodeIncludesId() {
+        
assertTrue(locator.dataMapNode("map42").absolutePath().endsWith("/v5/datamap/map42"));
+    }
+
+    @Test
+    public void handshakeNodeExistsFalseByDefault() {
+        assertFalse(locator.handshakeNodeExists("nonce-x"));
+    }
+
+    @Test
+    public void handshakeNodeExistsTrueAfterWrite() throws 
BackingStoreException {
+        locator.handshakeNode("nonce-y").put("pid", "123");
+        root.flush();
+
+        assertTrue(locator.handshakeNodeExists("nonce-y"));
+    }
+
+    @Test
+    public void handshakeRootNodeExistsFalseByDefault() {
+        assertFalse(locator.handshakeRootNodeExists());
+    }
+
+    @Test
+    public void handshakeRootNodeExistsTrueAfterWrite() throws 
BackingStoreException {
+        locator.handshakeNode("any-nonce").put("pid", "1");
+        root.flush();
+
+        assertTrue(locator.handshakeRootNodeExists());
+    }
+}
diff --git 
a/modeler/cayenne-modeler-prefs/src/test/java/org/apache/cayenne/modeler/pref/adapters/ClasspathPrefsTest.java
 
b/modeler/cayenne-modeler-prefs/src/test/java/org/apache/cayenne/modeler/pref/adapters/ClasspathPrefsTest.java
new file mode 100644
index 000000000..b9a80f15d
--- /dev/null
+++ 
b/modeler/cayenne-modeler-prefs/src/test/java/org/apache/cayenne/modeler/pref/adapters/ClasspathPrefsTest.java
@@ -0,0 +1,98 @@
+/*****************************************************************
+ *   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
+ *
+ *    https://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.cayenne.modeler.pref.adapters;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+
+import java.util.List;
+import java.util.UUID;
+import java.util.prefs.BackingStoreException;
+import java.util.prefs.Preferences;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class ClasspathPrefsTest {
+
+    private final Preferences root = Preferences.userRoot()
+            .node("cayenne-test/" + UUID.randomUUID().toString().replace("-", 
""));
+    private final ClasspathPrefs prefs = new ClasspathPrefs(root);
+
+    @AfterEach
+    public void cleanup() throws BackingStoreException {
+        root.removeNode();
+    }
+
+    @Test
+    public void emptyPrefsReturnsEmptyList() {
+        assertTrue(prefs.getEntries().isEmpty());
+    }
+
+    @Test
+    public void singleEntryRoundtrip() {
+        prefs.setEntries(List.of("/lib/foo.jar"));
+        assertEquals(List.of("/lib/foo.jar"), prefs.getEntries());
+    }
+
+    @Test
+    public void multipleEntriesPreserveOrder() {
+        List<String> entries = List.of("/a.jar", "/b.jar", "/c.jar");
+        prefs.setEntries(entries);
+        assertEquals(entries, prefs.getEntries());
+    }
+
+    @Test
+    public void setEntriesOverwritesClearsPreviousEntries() {
+        prefs.setEntries(List.of("/old.jar"));
+        prefs.setEntries(List.of("/new.jar"));
+        assertEquals(List.of("/new.jar"), prefs.getEntries());
+    }
+
+    @Test
+    public void numericKeysAreOrderedNumerically() throws 
BackingStoreException {
+        // Write keys manually in a non-lexicographic order: 10 before 2
+        root.put("1", "/first.jar");
+        root.put("10", "/tenth.jar");
+        root.put("2", "/second.jar");
+        root.flush();
+
+        // Numeric sort: 1 < 2 < 10, not lexicographic "1" < "10" < "2"
+        assertEquals(List.of("/first.jar", "/second.jar", "/tenth.jar"), 
prefs.getEntries());
+    }
+
+    @Test
+    public void nonNumericKeysAreIgnoredOnRead() throws BackingStoreException {
+        root.put("foo", "/should-be-ignored.jar");
+        root.put("1", "/valid.jar");
+        root.flush();
+
+        assertEquals(List.of("/valid.jar"), prefs.getEntries());
+    }
+
+    @Test
+    public void emptyValuesAreSkippedOnRead() throws BackingStoreException {
+        root.put("1", "");
+        root.put("2", "/valid.jar");
+        root.flush();
+
+        assertEquals(List.of("/valid.jar"), prefs.getEntries());
+    }
+}
diff --git 
a/modeler/cayenne-modeler-prefs/src/test/java/org/apache/cayenne/modeler/pref/adapters/DBConnectorsPrefsTest.java
 
b/modeler/cayenne-modeler-prefs/src/test/java/org/apache/cayenne/modeler/pref/adapters/DBConnectorsPrefsTest.java
new file mode 100644
index 000000000..557d687dd
--- /dev/null
+++ 
b/modeler/cayenne-modeler-prefs/src/test/java/org/apache/cayenne/modeler/pref/adapters/DBConnectorsPrefsTest.java
@@ -0,0 +1,142 @@
+/*****************************************************************
+ *   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
+ *
+ *    https://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.cayenne.modeler.pref.adapters;
+
+import org.apache.cayenne.modeler.pref.dbconnector.DBConnector;
+import org.apache.cayenne.modeler.pref.dbconnector.DBConnectors;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.UUID;
+import java.util.prefs.BackingStoreException;
+import java.util.prefs.Preferences;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class DBConnectorsPrefsTest {
+
+    private final Preferences root = Preferences.userRoot()
+            .node("cayenne-test/" + UUID.randomUUID().toString().replace("-", 
""));
+    private final DBConnectorsPrefs prefs = new DBConnectorsPrefs(root);
+
+    @AfterEach
+    public void cleanup() throws BackingStoreException {
+        root.removeNode();
+    }
+
+    @Test
+    public void emptyPrefsReturnsEmptyRegistry() {
+        assertTrue(prefs.getConnectors().getAll().isEmpty());
+    }
+
+    @Test
+    public void loadsConnectorFromPrefsNode() throws BackingStoreException {
+        Preferences n = root.node("myconn");
+        n.put("url", "jdbc:h2:mem");
+        n.put("userName", "sa");
+        n.put("password", "secret");
+        n.put("jdbcDriver", "org.h2.Driver");
+        n.put("dbAdapter", "org.apache.cayenne.dba.h2.H2Adapter");
+        root.flush();
+
+        DBConnectors registry = prefs.getConnectors();
+        DBConnector loaded = registry.get("myconn");
+
+        assertEquals("jdbc:h2:mem", loaded.getUrl());
+        assertEquals("sa", loaded.getUserName());
+        assertEquals("secret", loaded.getPassword());
+        assertEquals("org.h2.Driver", loaded.getJdbcDriver());
+        assertEquals("org.apache.cayenne.dba.h2.H2Adapter", 
loaded.getDbAdapter());
+    }
+
+    @Test
+    public void loadsMultipleConnectors() throws BackingStoreException {
+        root.node("conn1").put("url", "jdbc:h2:mem:db1");
+        root.node("conn2").put("url", "jdbc:h2:mem:db2");
+        root.flush();
+
+        DBConnectors registry = prefs.getConnectors();
+        assertEquals(2, registry.getAll().size());
+        assertEquals("jdbc:h2:mem:db1", registry.get("conn1").getUrl());
+        assertEquals("jdbc:h2:mem:db2", registry.get("conn2").getUrl());
+    }
+
+    @Test
+    public void missingPropertiesLoadAsNull() throws BackingStoreException {
+        root.node("sparse").put("url", "jdbc:h2:mem");
+        root.flush();
+
+        DBConnector loaded = prefs.getConnectors().get("sparse");
+        assertEquals("jdbc:h2:mem", loaded.getUrl());
+        assertNull(loaded.getUserName());
+        assertNull(loaded.getPassword());
+        assertNull(loaded.getJdbcDriver());
+        assertNull(loaded.getDbAdapter());
+    }
+
+    @Test
+    public void putMutationWritesToPrefs() throws BackingStoreException {
+        DBConnectors registry = prefs.getConnectors();
+
+        DBConnector connector = new DBConnector();
+        connector.setUrl("jdbc:pg://localhost/mydb");
+        connector.setUserName("admin");
+        registry.put("pgconn", connector);
+
+        Preferences stored = root.node("pgconn");
+        assertEquals("jdbc:pg://localhost/mydb", stored.get("url", null));
+        assertEquals("admin", stored.get("userName", null));
+    }
+
+    @Test
+    public void removeMutationDeletesPrefsNode() throws BackingStoreException {
+        root.node("toremove").put("url", "jdbc:h2:mem");
+        root.flush();
+
+        DBConnectors registry = prefs.getConnectors();
+        registry.remove("toremove");
+
+        assertFalse(root.nodeExists("toremove"));
+    }
+
+    @Test
+    public void initialLoadDoesNotFireWriteback() throws BackingStoreException 
{
+        // Write one connector to prefs before loading
+        root.node("existing").put("url", "jdbc:h2:mem");
+        root.flush();
+
+        List<String> writebackNames = new ArrayList<>();
+        DBConnectors registry = prefs.getConnectors();
+
+        // Attach a spy listener after loading to detect any late writeback
+        registry.addListener(new DBConnectors.Listener() {
+            public void connectionUpdated(String name) { 
writebackNames.add(name); }
+            public void connectionRemoved(String name) { 
writebackNames.add(name); }
+        });
+
+        // No mutations occurred — writeback list must be empty
+        assertTrue(writebackNames.isEmpty());
+    }
+}
diff --git 
a/modeler/cayenne-modeler-prefs/src/test/java/org/apache/cayenne/modeler/pref/dbconnector/DBConnectorTest.java
 
b/modeler/cayenne-modeler-prefs/src/test/java/org/apache/cayenne/modeler/pref/dbconnector/DBConnectorTest.java
new file mode 100644
index 000000000..c9211db99
--- /dev/null
+++ 
b/modeler/cayenne-modeler-prefs/src/test/java/org/apache/cayenne/modeler/pref/dbconnector/DBConnectorTest.java
@@ -0,0 +1,118 @@
+/*****************************************************************
+ *   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
+ *
+ *    https://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.cayenne.modeler.pref.dbconnector;
+
+import org.apache.cayenne.configuration.DataSourceDescriptor;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class DBConnectorTest {
+
+    @Test
+    public void copyToConnectorReturnsFalseWhenIdentical() {
+        DBConnector src = connector("jdbc:h2:mem", "sa", "pw", 
"org.h2.Driver", "org.h2.H2Adapter");
+        DBConnector dst = connector("jdbc:h2:mem", "sa", "pw", 
"org.h2.Driver", "org.h2.H2Adapter");
+
+        assertFalse(src.copyTo(dst));
+    }
+
+    @Test
+    public void copyToConnectorReturnsTrueOnChange() {
+        DBConnector src = connector("jdbc:h2:new", "sa", "pw", 
"org.h2.Driver", "org.h2.H2Adapter");
+        DBConnector dst = connector("jdbc:h2:old", "sa", "pw", 
"org.h2.Driver", "org.h2.H2Adapter");
+
+        assertTrue(src.copyTo(dst));
+        assertEquals("jdbc:h2:new", dst.getUrl());
+    }
+
+    @Test
+    public void copyToConnectorCopiesAllFiveFields() {
+        DBConnector src = connector("url", "user", "pass", "driver", 
"adapter");
+        DBConnector dst = new DBConnector();
+
+        src.copyTo(dst);
+
+        assertEquals("url", dst.getUrl());
+        assertEquals("user", dst.getUserName());
+        assertEquals("pass", dst.getPassword());
+        assertEquals("driver", dst.getJdbcDriver());
+        assertEquals("adapter", dst.getDbAdapter());
+    }
+
+    @Test
+    public void copyToDataSourceDescriptorCopiesDriverUrlUserPassword() {
+        DBConnector src = connector("jdbc:pg://db", "admin", "secret", 
"org.postgresql.Driver", "PgAdapter");
+        DataSourceDescriptor dst = new DataSourceDescriptor();
+
+        src.copyTo(dst);
+
+        assertEquals("jdbc:pg://db", dst.getDataSourceUrl());
+        assertEquals("admin", dst.getUserName());
+        assertEquals("secret", dst.getPassword());
+        assertEquals("org.postgresql.Driver", dst.getJdbcDriver());
+    }
+
+    @Test
+    public void copyToDataSourceDescriptorDoesNotCopyDbAdapter() {
+        // All 4 copyable fields already match; only dbAdapter differs.
+        // copyTo must return false — dbAdapter is not part of the 
DataSourceDescriptor copy.
+        DBConnector src = connector("url", "user", "pass", "driver", 
"SomeAdapter");
+        DataSourceDescriptor dst = new DataSourceDescriptor();
+        dst.setDataSourceUrl("url");
+        dst.setUserName("user");
+        dst.setPassword("pass");
+        dst.setJdbcDriver("driver");
+
+        assertFalse(src.copyTo(dst));
+    }
+
+    @Test
+    public void copyToConnectorHandlesNullFields() {
+        DBConnector src = new DBConnector();
+        DBConnector dst = connector("url", "user", "pass", "driver", 
"adapter");
+
+        assertTrue(src.copyTo(dst));
+        assertNull(dst.getUrl());
+        assertNull(dst.getUserName());
+        assertNull(dst.getPassword());
+        assertNull(dst.getJdbcDriver());
+        assertNull(dst.getDbAdapter());
+    }
+
+    private static DBConnector connector(
+            String url,
+            String userName,
+            String password,
+            String jdbcDriver,
+            String dbAdapter) {
+
+        DBConnector c = new DBConnector();
+        c.setUrl(url);
+        c.setUserName(userName);
+        c.setPassword(password);
+        c.setJdbcDriver(jdbcDriver);
+        c.setDbAdapter(dbAdapter);
+        return c;
+    }
+}
diff --git 
a/modeler/cayenne-modeler-prefs/src/test/java/org/apache/cayenne/modeler/pref/dbconnector/DBConnectorsTest.java
 
b/modeler/cayenne-modeler-prefs/src/test/java/org/apache/cayenne/modeler/pref/dbconnector/DBConnectorsTest.java
new file mode 100644
index 000000000..b6c2191bc
--- /dev/null
+++ 
b/modeler/cayenne-modeler-prefs/src/test/java/org/apache/cayenne/modeler/pref/dbconnector/DBConnectorsTest.java
@@ -0,0 +1,138 @@
+/*****************************************************************
+ *   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
+ *
+ *    https://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.cayenne.modeler.pref.dbconnector;
+
+import org.junit.jupiter.api.Test;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+public class DBConnectorsTest {
+
+    @Test
+    public void putAddsConnectorAndFiresListener() {
+        DBConnectors registry = new DBConnectors();
+        List<String> updated = new ArrayList<>();
+        registry.addListener(new DBConnectors.Listener() {
+            public void connectionUpdated(String name) { updated.add(name); }
+            public void connectionRemoved(String name) { }
+        });
+
+        registry.put("main", new DBConnector());
+
+        assertEquals(List.of("main"), updated);
+        assertTrue(registry.getAll().containsKey("main"));
+    }
+
+    @Test
+    public void putOnExistingNameFiresUpdateListener() {
+        DBConnectors registry = new DBConnectors();
+        registry.put("main", new DBConnector());
+
+        List<String> updated = new ArrayList<>();
+        registry.addListener(new DBConnectors.Listener() {
+            public void connectionUpdated(String name) { updated.add(name); }
+            public void connectionRemoved(String name) { }
+        });
+
+        registry.put("main", new DBConnector());
+
+        assertEquals(List.of("main"), updated);
+    }
+
+    @Test
+    public void removeExistingFiresListener() {
+        DBConnectors registry = new DBConnectors();
+        registry.put("main", new DBConnector());
+
+        List<String> removed = new ArrayList<>();
+        registry.addListener(new DBConnectors.Listener() {
+            public void connectionUpdated(String name) { }
+            public void connectionRemoved(String name) { removed.add(name); }
+        });
+
+        registry.remove("main");
+
+        assertEquals(List.of("main"), removed);
+        assertFalse(registry.getAll().containsKey("main"));
+    }
+
+    @Test
+    public void removeNonExistentDoesNotFireListener() {
+        DBConnectors registry = new DBConnectors();
+        List<String> removed = new ArrayList<>();
+        registry.addListener(new DBConnectors.Listener() {
+            public void connectionUpdated(String name) { }
+            public void connectionRemoved(String name) { removed.add(name); }
+        });
+
+        registry.remove("nonexistent");
+
+        assertTrue(removed.isEmpty());
+    }
+
+    @Test
+    public void getAllReturnsUnmodifiableMap() {
+        DBConnectors registry = new DBConnectors();
+        registry.put("main", new DBConnector());
+
+        Map<String, DBConnector> all = registry.getAll();
+
+        assertThrows(UnsupportedOperationException.class, () -> 
all.put("other", new DBConnector()));
+    }
+
+    @Test
+    public void getAllPreservesInsertionOrder() {
+        DBConnectors registry = new DBConnectors();
+        registry.put("alpha", new DBConnector());
+        registry.put("beta", new DBConnector());
+        registry.put("gamma", new DBConnector());
+
+        List<String> keys = new ArrayList<>(registry.getAll().keySet());
+        assertEquals(List.of("alpha", "beta", "gamma"), keys);
+    }
+
+    @Test
+    public void multipleListenersAllNotified() {
+        DBConnectors registry = new DBConnectors();
+        List<String> calls1 = new ArrayList<>();
+        List<String> calls2 = new ArrayList<>();
+
+        registry.addListener(new DBConnectors.Listener() {
+            public void connectionUpdated(String name) { calls1.add(name); }
+            public void connectionRemoved(String name) { }
+        });
+        registry.addListener(new DBConnectors.Listener() {
+            public void connectionUpdated(String name) { calls2.add(name); }
+            public void connectionRemoved(String name) { }
+        });
+
+        registry.put("conn", new DBConnector());
+
+        assertEquals(List.of("conn"), calls1);
+        assertEquals(List.of("conn"), calls2);
+    }
+}
diff --git 
a/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/Application.java
 
b/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/Application.java
index 8311aee3b..d7542a0d6 100644
--- 
a/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/Application.java
+++ 
b/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/Application.java
@@ -33,7 +33,7 @@ import org.apache.cayenne.modeler.log.ModelerLogFactory;
 import org.apache.cayenne.modeler.platform.UIInitializer;
 import org.apache.cayenne.modeler.toolkit.filechooser.FileChooserFactory;
 import org.apache.cayenne.modeler.pref.adapters.ClasspathPrefs;
-import org.apache.cayenne.modeler.pref.adapters.DBConnectorPrefs;
+import org.apache.cayenne.modeler.pref.adapters.DBConnectorsPrefs;
 import org.apache.cayenne.modeler.pref.adapters.GeneralPrefs;
 import org.apache.cayenne.modeler.pref.PrefsLocator;
 import org.apache.cayenne.modeler.pref.PrefsManager;
@@ -190,7 +190,7 @@ public class Application {
 
         getPrefsManager().runMigrations();
 
-        this.dbConnectors = new 
DBConnectorPrefs(prefsLocator.appNode(DBConnectorPrefs.NODE)).getConnectors();
+        this.dbConnectors = new 
DBConnectorsPrefs(prefsLocator.appNode(DBConnectorsPrefs.NODE)).getConnectors();
 
         refreshClassLoader();
 
diff --git 
a/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/pref/adapters/DataMapPrefs.java
 
b/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/pref/adapters/DataMapPrefs.java
index 31da2ff49..4c2c87071 100644
--- 
a/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/pref/adapters/DataMapPrefs.java
+++ 
b/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/pref/adapters/DataMapPrefs.java
@@ -53,12 +53,7 @@ public final class DataMapPrefs extends PrefsAdapter {
     }
 
     public void setSuperclassPackage(String superclassPackage) {
-        if (prefs != null) {
-            if (superclassPackage == null) {
-                superclassPackage = "";
-            }
-            prefs.put(SUPERCLASS_PACKAGE_PROPERTY, superclassPackage);
-        }
+        putOrRemove(SUPERCLASS_PACKAGE_PROPERTY, superclassPackage);
     }
 
     /**
@@ -66,7 +61,7 @@ public final class DataMapPrefs extends PrefsAdapter {
      * connection has been configured (URL not set).
      */
     public DBConnector getConnector() {
-        if (prefs == null || prefs.get(DBConnector.URL_PROPERTY, null) == 
null) {
+        if (prefs.get(DBConnector.URL_PROPERTY, null) == null) {
             return null;
         }
         DBConnector connector = new DBConnector();
@@ -79,18 +74,11 @@ public final class DataMapPrefs extends PrefsAdapter {
     }
 
     public void setConnector(DBConnector connector) {
-        if (prefs == null) {
-            return;
-        }
-        if (connector.getDbAdapter() != null) {
-            prefs.put(DBConnector.DB_ADAPTER_PROPERTY, 
connector.getDbAdapter());
-        } else {
-            prefs.remove(DBConnector.DB_ADAPTER_PROPERTY);
-        }
-        prefs.put(DBConnector.URL_PROPERTY, connector.getUrl());
-        prefs.put(DBConnector.USER_NAME_PROPERTY, connector.getUserName());
-        prefs.put(DBConnector.PASSWORD_PROPERTY, connector.getPassword());
-        prefs.put(DBConnector.JDBC_DRIVER_PROPERTY, connector.getJdbcDriver());
+        putOrRemove(DBConnector.DB_ADAPTER_PROPERTY, connector.getDbAdapter());
+        putOrRemove(DBConnector.URL_PROPERTY, connector.getUrl());
+        putOrRemove(DBConnector.USER_NAME_PROPERTY, connector.getUserName());
+        putOrRemove(DBConnector.PASSWORD_PROPERTY, connector.getPassword());
+        putOrRemove(DBConnector.JDBC_DRIVER_PROPERTY, 
connector.getJdbcDriver());
     }
 
     public boolean hasDbAdapter() {

Reply via email to