From 98d57eb9b4cc8734a99afc1888c12f0a129a6dcc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Schild?= Date: Fri, 24 Jul 2026 23:06:23 +0200 Subject: [PATCH] Reference-count NextcloudConnector so shared client survives sibling close (#87) close() shut down the static, shared HTTP client, breaking any other NextcloudConnector still in use. Track the number of open connectors and only shut the shared client down when the last one is closed; close() is now idempotent. shutdown() still forces an immediate teardown. TestConnectorLifecycle verifies a second connector keeps working after the first is closed. Co-Authored-By: Claude Opus 4.8 --- Changelog.md | 4 ++ .../nextcloud/api/NextcloudConnector.java | 33 +++++++++-- .../nextcloud/api/TestConnectorLifecycle.java | 59 +++++++++++++++++++ 3 files changed, 91 insertions(+), 5 deletions(-) create mode 100644 src/test/java/org/aarboard/nextcloud/api/TestConnectorLifecycle.java diff --git a/Changelog.md b/Changelog.md index 01a678f..f609da6 100644 --- a/Changelog.md +++ b/Changelog.md @@ -1,6 +1,10 @@ # Changelog for nextcloud api ## Version 14.2.0 +- Fix connector lifecycle: closing a `NextcloudConnector` now shuts down the + shared HTTP client only once the last open connector is closed, so closing + one connector no longer breaks others still in use (issue #87). Use + `shutdown()` to force an immediate teardown. - Add system tags support: list, create and delete system tags, and assign or remove tags on a file via the new `SystemTags` connector and `NextcloudConnector` methods (issue #110) diff --git a/src/main/java/org/aarboard/nextcloud/api/NextcloudConnector.java b/src/main/java/org/aarboard/nextcloud/api/NextcloudConnector.java index c6d8871..25a9df9 100644 --- a/src/main/java/org/aarboard/nextcloud/api/NextcloudConnector.java +++ b/src/main/java/org/aarboard/nextcloud/api/NextcloudConnector.java @@ -49,6 +49,15 @@ public class NextcloudConnector implements AutoCloseable { + /** + * Number of open connectors sharing the static HTTP client, so it is only + * shut down once the last connector is closed (see issue #87). + */ + private static final java.util.concurrent.atomic.AtomicInteger OPEN_INSTANCES = + new java.util.concurrent.atomic.AtomicInteger(0); + + private volatile boolean closed = false; + private final ServerConfig serverConfig; private final ProvisionConnector pc; private final FilesharingConnector fc; @@ -121,6 +130,7 @@ public NextcloudConnector(String originalServiceUrl, AuthenticationConfig authen fl = new Files(this.serverConfig); gf = new GroupFolders(this.serverConfig); st = new SystemTags(this.serverConfig); + OPEN_INSTANCES.incrementAndGet(); } catch (MalformedURLException e) { throw new IllegalArgumentException(e); @@ -144,6 +154,7 @@ public NextcloudConnector(String serverName, boolean useHTTPS, int port, fl = new Files(this.serverConfig); gf = new GroupFolders(this.serverConfig); st = new SystemTags(this.serverConfig); + OPEN_INSTANCES.incrementAndGet(); } /** @@ -193,8 +204,11 @@ public void setWebDavPathResolverAsType(final WebDavPathResolverBuilder.TYPE typ } /** - * Close the HTTP client. Perform this to cleanly shut down this - * application. + * Immediately shuts down the shared HTTP client, regardless of how many + * other {@link NextcloudConnector} instances are still open. Prefer + * {@link #close()} (e.g. via try-with-resources), which only shuts the + * shared client down once the last connector is closed. Use this only when + * you explicitly want to tear everything down at once. * * @throws IOException In case of IO errors */ @@ -401,13 +415,22 @@ public void setGroupFolderQuota(int groupFolderId, long quota) { } /** - * Close the HTTP client. Perform this to cleanly shut down this - * application. + * Closes this connector. The shared HTTP client is only shut down once the + * last open {@link NextcloudConnector} has been closed, so closing one + * connector no longer breaks others that are still in use (see issue #87). + * Idempotent: closing an already-closed connector does nothing. * * @throws Exception In case of errors */ + @Override public void close() throws Exception { - shutdown(); + if (closed) { + return; + } + closed = true; + if (OPEN_INSTANCES.decrementAndGet() <= 0) { + shutdown(); + } } /** diff --git a/src/test/java/org/aarboard/nextcloud/api/TestConnectorLifecycle.java b/src/test/java/org/aarboard/nextcloud/api/TestConnectorLifecycle.java new file mode 100644 index 0000000..5fc3c26 --- /dev/null +++ b/src/test/java/org/aarboard/nextcloud/api/TestConnectorLifecycle.java @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2026 a.schild + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.aarboard.nextcloud.api; + +import static org.junit.Assert.assertNotNull; + +import org.junit.Test; + +/** + * Verifies the shared-HTTP-client lifecycle: closing one connector must not + * shut the shared client down while another connector is still in use + * (issue #87). + * + * @author a.schild + */ +public class TestConnectorLifecycle { + + @Test + public void testClosingOneConnectorDoesNotBreakAnother() throws Exception { + TestHelper th = new TestHelper(); + String serverName = th.getServerName(); + if (serverName == null) { + return; + } + NextcloudConnector first = newConnector(th); + NextcloudConnector second = newConnector(th); + try { + assertNotNull(first.getShares()); + assertNotNull(second.getShares()); + + // Closing the first connector must not tear down the shared client + // that the second one still relies on. + first.close(); + + assertNotNull(second.getShares()); + } finally { + second.close(); + } + } + + private NextcloudConnector newConnector(TestHelper th) { + return new NextcloudConnector(th.getServerName(), th.getServerPort() == 443, + th.getServerPort(), th.getUserName(), th.getPassword()); + } +}