From b7f395e3af370ffa07c6df99d32022ee5e195571 Mon Sep 17 00:00:00 2001 From: jameshanna-symphony Date: Wed, 30 Sep 2026 14:05:21 +0100 Subject: [PATCH 01/17] BE-9865 Add User Organisation Service --- .../com/symphony/bdk/core/OboServices.java | 21 ++ .../com/symphony/bdk/core/ServiceFactory.java | 17 + .../com/symphony/bdk/core/SymphonyBdk.java | 21 ++ .../service/user/UserOrganizationService.java | 330 ++++++++++++++++ .../service/user/model/UserOrganization.java | 121 ++++++ .../core/service/user/model/package-info.java | 4 + .../symphony/bdk/core/ServiceFactoryTest.java | 8 + .../user/UserOrganizationServiceTest.java | 356 ++++++++++++++++++ .../bdk/spring/config/BdkCoreConfig.java | 7 + .../spring/config/BdkOboServiceConfig.java | 10 + .../bdk/spring/config/BdkServiceConfig.java | 10 + .../SymphonyBdkMockedConfiguration.java | 5 + 12 files changed, 910 insertions(+) create mode 100644 symphony-bdk-core/src/main/java/com/symphony/bdk/core/service/user/UserOrganizationService.java create mode 100644 symphony-bdk-core/src/main/java/com/symphony/bdk/core/service/user/model/UserOrganization.java create mode 100644 symphony-bdk-core/src/main/java/com/symphony/bdk/core/service/user/model/package-info.java create mode 100644 symphony-bdk-core/src/test/java/com/symphony/bdk/core/service/user/UserOrganizationServiceTest.java diff --git a/symphony-bdk-core/src/main/java/com/symphony/bdk/core/OboServices.java b/symphony-bdk-core/src/main/java/com/symphony/bdk/core/OboServices.java index 51b4dbf25..8eb117503 100644 --- a/symphony-bdk-core/src/main/java/com/symphony/bdk/core/OboServices.java +++ b/symphony-bdk-core/src/main/java/com/symphony/bdk/core/OboServices.java @@ -12,6 +12,7 @@ import com.symphony.bdk.core.service.signal.OboSignalService; import com.symphony.bdk.core.service.stream.OboStreamService; import com.symphony.bdk.core.service.user.OboUserService; +import com.symphony.bdk.core.service.user.UserOrganizationService; import org.apiguardian.api.API; @@ -25,6 +26,7 @@ public class OboServices { private final OboStreamService oboStreamService; private final OboUserService oboUserService; + private final UserOrganizationService oboUserOrganizationService; private final OboMessageService oboMessageService; private final OboPresenceService oboPresenceService; private final OboConnectionService oboConnectionService; @@ -46,6 +48,7 @@ public OboServices( oboStreamService = serviceFactory.getStreamService(); oboUserService = serviceFactory.getUserService(); + oboUserOrganizationService = serviceFactory.getUserOrganizationService(); oboMessageService = serviceFactory.getMessageService(); oboPresenceService = serviceFactory.getPresenceService(); oboConnectionService = serviceFactory.getConnectionService(); @@ -71,6 +74,24 @@ public OboUserService users() { return oboUserService; } + /** + * Get the {@link UserOrganizationService} using the provided OBO session in constructor. + * + * @return a {@link UserOrganizationService} instance with the provided OBO session. + */ + public UserOrganizationService userOrganization() { + return oboUserOrganizationService; + } + + /** + * Get the {@link UserOrganizationService} using the provided OBO session in constructor. + * + * @return a {@link UserOrganizationService} instance with the provided OBO session. + */ + public UserOrganizationService getUserOrganizationService() { + return oboUserOrganizationService; + } + /** * Get the {@link OboPresenceService} using the provided OBO session in constructor. * diff --git a/symphony-bdk-core/src/main/java/com/symphony/bdk/core/ServiceFactory.java b/symphony-bdk-core/src/main/java/com/symphony/bdk/core/ServiceFactory.java index 17f0305bb..9e5ac43b4 100644 --- a/symphony-bdk-core/src/main/java/com/symphony/bdk/core/ServiceFactory.java +++ b/symphony-bdk-core/src/main/java/com/symphony/bdk/core/ServiceFactory.java @@ -26,6 +26,7 @@ import com.symphony.bdk.core.service.session.SessionService; import com.symphony.bdk.core.service.signal.SignalService; import com.symphony.bdk.core.service.stream.StreamService; +import com.symphony.bdk.core.service.user.UserOrganizationService; import com.symphony.bdk.core.service.user.UserService; import com.symphony.bdk.gen.api.AppEntitlementApi; import com.symphony.bdk.gen.api.ApplicationApi; @@ -75,6 +76,7 @@ class ServiceFactory { private final ApiClient podClient; + private final ApiClient usersClient; private final ApiClient agentClient; private final ApiClient datafeedAgentClient; private final ApiClient datahoseAgentClient; @@ -100,6 +102,7 @@ public ServiceFactory( ) { this.config = config; this.podClient = apiClientFactory.getPodClient(); + this.usersClient = apiClientFactory.getUsersClient(); this.agentClient = apiClientFactory.getAgentClient(); this.datafeedAgentClient = apiClientFactory.getDatafeedAgentClient(); this.datahoseAgentClient = apiClientFactory.getDatahoseAgentClient(); @@ -120,6 +123,11 @@ public ServiceFactory( this.podClient.getAuthentications().put(BEARER_AUTH, new OAuthentication(oAuthSession::getBearerToken)); this.podClient.addEnforcedAuthenticationScheme(BEARER_AUTH); + if (this.usersClient != null) { + this.usersClient.getAuthentications().put(BEARER_AUTH, new OAuthentication(oAuthSession::getBearerToken)); + this.usersClient.addEnforcedAuthenticationScheme(BEARER_AUTH); + } + this.agentClient.getAuthentications().put(BEARER_AUTH, new OAuthentication(oAuthSession::getBearerToken)); this.agentClient.addEnforcedAuthenticationScheme(BEARER_AUTH); @@ -142,6 +150,15 @@ public UserService getUserService() { retryBuilder); } + /** + * Returns a fully initialized {@link UserOrganizationService}. + * + * @return a new {@link UserOrganizationService} instance. + */ + public UserOrganizationService getUserOrganizationService() { + return new UserOrganizationService(this.usersClient, this.authSession, this.retryBuilder); + } + /** * Returns a fully initialized {@link StreamService}. * diff --git a/symphony-bdk-core/src/main/java/com/symphony/bdk/core/SymphonyBdk.java b/symphony-bdk-core/src/main/java/com/symphony/bdk/core/SymphonyBdk.java index 4b367dd48..7fe0fe575 100644 --- a/symphony-bdk-core/src/main/java/com/symphony/bdk/core/SymphonyBdk.java +++ b/symphony-bdk-core/src/main/java/com/symphony/bdk/core/SymphonyBdk.java @@ -24,6 +24,7 @@ import com.symphony.bdk.core.service.session.SessionService; import com.symphony.bdk.core.service.signal.SignalService; import com.symphony.bdk.core.service.stream.StreamService; +import com.symphony.bdk.core.service.user.UserOrganizationService; import com.symphony.bdk.core.service.user.UserService; import com.symphony.bdk.core.util.ServiceLookup; import com.symphony.bdk.extension.BdkExtension; @@ -60,6 +61,7 @@ public class SymphonyBdk { private final ActivityRegistry activityRegistry; private final StreamService streamService; private final UserService userService; + private final UserOrganizationService userOrganizationService; private final MessageService messageService; private final PresenceService presenceService; private final ConnectionService connectionService; @@ -162,6 +164,7 @@ protected SymphonyBdk( this.sessionService = serviceFactory != null ? serviceFactory.getSessionService() : null; this.userService = serviceFactory != null ? serviceFactory.getUserService() : null; + this.userOrganizationService = serviceFactory != null ? serviceFactory.getUserOrganizationService() : null; this.streamService = serviceFactory != null ? serviceFactory.getStreamService() : null; this.presenceService = serviceFactory != null ? serviceFactory.getPresenceService() : null; this.connectionService = serviceFactory != null ? serviceFactory.getConnectionService() : null; @@ -243,6 +246,24 @@ public UserService users() { return getOrThrowNoBotConfig(this.userService); } + /** + * Get the {@link UserOrganizationService} from a Bdk entry point. + * + * @return {@link UserOrganizationService} instance. + */ + public UserOrganizationService userOrganization() { + return getOrThrowNoBotConfig(this.userOrganizationService); + } + + /** + * Get the {@link UserOrganizationService} from a Bdk entry point. + * + * @return {@link UserOrganizationService} instance. + */ + public UserOrganizationService getUserOrganizationService() { + return this.userOrganizationService; + } + /** * Get the {@link StreamService} from a Bdk entry point. * diff --git a/symphony-bdk-core/src/main/java/com/symphony/bdk/core/service/user/UserOrganizationService.java b/symphony-bdk-core/src/main/java/com/symphony/bdk/core/service/user/UserOrganizationService.java new file mode 100644 index 000000000..9cce93578 --- /dev/null +++ b/symphony-bdk-core/src/main/java/com/symphony/bdk/core/service/user/UserOrganizationService.java @@ -0,0 +1,330 @@ +package com.symphony.bdk.core.service.user; + +import com.symphony.bdk.core.auth.AuthSession; +import com.symphony.bdk.core.retry.RetryWithRecovery; +import com.symphony.bdk.core.retry.RetryWithRecoveryBuilder; +import com.symphony.bdk.core.retry.function.SupplierWithApiException; +import com.symphony.bdk.core.service.OboService; +import com.symphony.bdk.core.service.user.model.UserOrganization; +import com.symphony.bdk.core.service.user.model.UserOrganization.Directory; +import com.symphony.bdk.core.service.user.model.UserOrganization.DistributionList; +import com.symphony.bdk.core.service.user.model.UserOrganization.Recipient; +import com.symphony.bdk.core.service.user.model.UserOrganization.Workspace; +import com.symphony.bdk.http.api.ApiClient; +import com.symphony.bdk.http.api.ApiException; +import com.symphony.bdk.http.api.ApiResponse; +import com.symphony.bdk.http.api.util.TypeReference; + +import lombok.extern.slf4j.Slf4j; +import org.apiguardian.api.API; +import org.jspecify.annotations.Nullable; +import tools.jackson.core.JacksonException; +import tools.jackson.databind.DeserializationFeature; +import tools.jackson.databind.JsonNode; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.json.JsonMapper; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Service to retrieve user organization data, including custom folders/directories, + * multi-chat workspaces, and blast distribution lists from Symphony client data. + */ +@Slf4j +@API(status = API.Status.EXPERIMENTAL) +public class UserOrganizationService implements OboService { + + private static final ObjectMapper MAPPER = JsonMapper.builder() + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) + .build(); + + private static final String CLIENT_DATA_ROOT = "/webcontroller/clientdata/GET/symphonyPrime"; + private static final String DISTRIBUTION_LISTS_SUBPATH = "C2/distributionLists"; + private static final String LEFTNAV_SUBPATH = "leftnav"; + private static final String CANVAS_STATE_SUBPATH = "canvasState"; + + private final ApiClient apiClient; + private final @Nullable AuthSession authSession; + private final RetryWithRecoveryBuilder retryBuilder; + + public UserOrganizationService(ApiClient apiClient, AuthSession authSession, + RetryWithRecoveryBuilder retryBuilder) { + this.apiClient = apiClient; + this.authSession = authSession; + this.retryBuilder = RetryWithRecoveryBuilder.copyWithoutRecoveryStrategies(retryBuilder) + .recoveryStrategy(ApiException::isUnauthorized, authSession::refresh); + } + + public UserOrganizationService(ApiClient apiClient, RetryWithRecoveryBuilder retryBuilder) { + this.apiClient = apiClient; + this.authSession = null; + this.retryBuilder = RetryWithRecoveryBuilder.copyWithoutRecoveryStrategies(retryBuilder); + } + + @Override + public UserOrganizationService obo(AuthSession oboSession) { + return new UserOrganizationService(this.apiClient, oboSession, this.retryBuilder); + } + + /** + * Retrieves the current user's aggregated organization data (directories, workspaces, and distribution lists). + * + * @return a {@link UserOrganization} containing the user's organization data. + */ + public UserOrganization getUserOrganization() { + return executeAndRetry("getUserOrganization", () -> { + JsonNode rootNode = fetchClientDataJson(""); + List directories; + List workspaces; + + if (rootNode != null) { + directories = parseDirectories(rootNode); + workspaces = parseWorkspaces(rootNode); + } else { + JsonNode leftNavNode = fetchClientDataJson(LEFTNAV_SUBPATH); + directories = leftNavNode != null ? parseDirectories(leftNavNode) : Collections.emptyList(); + + JsonNode canvasNode = fetchClientDataJson(CANVAS_STATE_SUBPATH); + workspaces = canvasNode != null ? parseWorkspaces(canvasNode) : Collections.emptyList(); + } + + JsonNode distroNode = fetchClientDataJson(DISTRIBUTION_LISTS_SUBPATH); + List distributionLists = distroNode != null ? parseDistributionLists(distroNode) : Collections.emptyList(); + + return new UserOrganization(directories, workspaces, distributionLists); + }); + } + + /** + * Retrieves the current user's custom directories/folders. + * + * @return a list of {@link Directory} instances. + */ + public List getUserDirectories() { + return executeAndRetry("getUserDirectories", () -> { + JsonNode leftNavNode = fetchClientDataJson(LEFTNAV_SUBPATH); + if (leftNavNode == null) { + leftNavNode = fetchClientDataJson(""); + } + return leftNavNode != null ? parseDirectories(leftNavNode) : Collections.emptyList(); + }); + } + + /** + * Retrieves the current user's workspaces. + * + * @return a list of {@link Workspace} instances. + */ + public List getUserWorkspaces() { + return executeAndRetry("getUserWorkspaces", () -> { + JsonNode rootNode = fetchClientDataJson(""); + if (rootNode == null) { + rootNode = fetchClientDataJson(CANVAS_STATE_SUBPATH); + } + return rootNode != null ? parseWorkspaces(rootNode) : Collections.emptyList(); + }); + } + + /** + * Retrieves the current user's blast distribution lists. + * + * @return a list of {@link DistributionList} instances. + */ + public List getUserDistributionLists() { + return executeAndRetry("getUserDistributionLists", () -> { + JsonNode distroNode = fetchClientDataJson(DISTRIBUTION_LISTS_SUBPATH); + return distroNode != null ? parseDistributionLists(distroNode) : Collections.emptyList(); + }); + } + + private T executeAndRetry(String name, SupplierWithApiException supplier) { + checkAuthSession(this.authSession); + return RetryWithRecovery.executeAndRetry(this.retryBuilder, name, this.apiClient.getBasePath(), supplier); + } + + private @Nullable JsonNode fetchClientDataJson(String subPath) throws ApiException { + Map headerParams = new HashMap<>(); + if (this.authSession != null && this.authSession.getSessionToken() != null) { + headerParams.put("sessionToken", this.authSession.getSessionToken()); + } + + String fullPath = CLIENT_DATA_ROOT + (subPath.isEmpty() ? "/" : "/" + subPath); + try { + ApiResponse response = this.apiClient.invokeAPI( + fullPath, + "GET", + Collections.emptyList(), + null, + headerParams, + Collections.emptyMap(), + Collections.emptyMap(), + "application/json", + "application/json", + new String[0], + new TypeReference() {} + ); + if (response != null && response.getData() != null) { + Object data = response.getData(); + if (data instanceof String s) { + if (!s.trim().isEmpty()) { + return MAPPER.readTree(s); + } + } else { + return MAPPER.valueToTree(data); + } + } + } catch (ApiException e) { + if (e.getCode() == 404) { + log.debug("No client-data found for path: {}", fullPath); + return null; + } + throw e; + } catch (JacksonException e) { + log.warn("Failed to parse client-data JSON for path: {}", fullPath, e); + } + return null; + } + + /** + * Parses custom folders/directories from leftnav JSON. + */ + public static List parseDirectories(JsonNode root) { + JsonNode doc = root.has("document") ? root.get("document") : root; + JsonNode leftnav = doc.has("leftnav") ? doc.get("leftnav") : doc; + + List directories = new ArrayList<>(); + for (JsonNode item : leftnav.path("groups").path("conversations")) { + if (item.isObject()) { + String id = item.path("id").asText(null); + String name = item.path("name").asText(null); + List streamIds = extractStreamIds(item.path("items")); + if (id != null || name != null || !streamIds.isEmpty()) { + directories.add(new Directory(id, name, streamIds)); + } + } + } + return directories; + } + + /** + * Parses workspaces from canvasState, dynamicLayouts, or customWorkspaces. + */ + public static List parseWorkspaces(JsonNode root) { + JsonNode doc = root.path("document"); + if (doc.isMissingNode()) doc = root; + + JsonNode savedTabs = doc.path("canvasState").path("savedTabs"); + JsonNode dynamicLayouts = doc.path("dynamicLayouts"); + + List workspaces = new ArrayList<>(); + if (savedTabs.isArray() && !savedTabs.isEmpty()) { + for (JsonNode tab : savedTabs) { + if (tab.path("isWorkspace").asBoolean(false) || tab.has("layoutId")) { + String id = tab.path("id").asText(null); + String name = tab.hasNonNull("title") ? tab.get("title").asText() : tab.path("label").asText(null); + String layoutId = tab.path("layoutId").asText(null); + List streamIds = new ArrayList<>(); + if (layoutId != null && dynamicLayouts.has(layoutId)) { + extractStreams(dynamicLayouts.get(layoutId).path("dynamicModules"), streamIds); + } + if (id != null || name != null || !streamIds.isEmpty()) { + workspaces.add(new Workspace(id, name, streamIds)); + } + } + } + } else { + JsonNode customWs = doc.has("customWorkspaces") ? doc.get("customWorkspaces") : doc.path("workspaces"); + for (JsonNode ws : customWs) { + if (ws.isObject()) { + List streamIds = extractStreamIds(ws.path("items")); + if (streamIds.isEmpty()) streamIds = extractStreamIds(ws.path("streams")); + if (streamIds.isEmpty()) streamIds = extractStreamIds(ws.path("streamIds")); + workspaces.add(new Workspace(ws.path("id").asText(null), ws.path("name").asText(null), streamIds)); + } + } + } + return workspaces; + } + + private static void extractStreams(JsonNode node, List streamIds) { + if (node == null || node.isMissingNode()) return; + if (node.isArray()) { + node.forEach(child -> extractStreams(child, streamIds)); + } else if (node.isObject() && ("chat".equalsIgnoreCase(node.path("type").asText()) || node.has("props"))) { + String id = node.path("props").hasNonNull("id") ? node.path("props").get("id").asText() + : (node.path("props").path("item").hasNonNull("id") ? node.path("props").path("item").get("id").asText() + : node.path("viewId").asText(null)); + if (id != null && !streamIds.contains(id)) { + streamIds.add(id); + } + } + } + + private static List extractStreamIds(JsonNode arrayNode) { + List streamIds = new ArrayList<>(); + if (arrayNode.isArray()) { + for (JsonNode item : arrayNode) { + String id = item.isObject() ? (item.hasNonNull("id") ? item.get("id").asText() : item.path("streamId").asText(null)) : item.asText(null); + if (id != null) { + streamIds.add(id); + } + } + } + return streamIds; + } + + /** + * Parses distribution lists from C2/distributionLists response using Jackson DTO binding. + */ + public static List parseDistributionLists(JsonNode root) { + JsonNode doc = root.path("document"); + if (doc.isMissingNode()) doc = root; + if (doc.has("C2") && doc.get("C2").has("distributionLists")) { + doc = doc.get("C2").get("distributionLists"); + } + + try { + if (doc.isObject()) { + Map map = MAPPER.treeToValue(doc, + new tools.jackson.core.type.TypeReference>() {}); + return map != null ? map.values().stream().map(RawDistributionList::toDomain).toList() : Collections.emptyList(); + } else if (doc.isArray()) { + List list = MAPPER.treeToValue(doc, + new tools.jackson.core.type.TypeReference>() {}); + return list != null ? list.stream().map(RawDistributionList::toDomain).toList() : Collections.emptyList(); + } + } catch (JacksonException e) { + log.warn("Failed to deserialize distribution lists", e); + } + return Collections.emptyList(); + } + + /** + * Internal DTO for Jackson direct binding from SymphonyPrime client-data payload. + */ + @API(status = API.Status.EXPERIMENTAL) + public record RawDistributionList( + @Nullable String id, + @Nullable String name, + @Nullable List recipients, + @Nullable List items, + @Nullable List streamIds + ) { + public DistributionList toDomain() { + List recs = this.recipients != null ? this.recipients : Collections.emptyList(); + List streams = new ArrayList<>(); + for (Recipient r : recs) { + if ("stream".equalsIgnoreCase(r.getType()) && r.getId() != null) { + streams.add(r.getId()); + } + } + if (streams.isEmpty() && this.items != null) streams.addAll(this.items); + if (streams.isEmpty() && this.streamIds != null) streams.addAll(this.streamIds); + return new DistributionList(this.id, this.name, streams, recs); + } + } +} diff --git a/symphony-bdk-core/src/main/java/com/symphony/bdk/core/service/user/model/UserOrganization.java b/symphony-bdk-core/src/main/java/com/symphony/bdk/core/service/user/model/UserOrganization.java new file mode 100644 index 000000000..2c0ef3d5f --- /dev/null +++ b/symphony-bdk-core/src/main/java/com/symphony/bdk/core/service/user/model/UserOrganization.java @@ -0,0 +1,121 @@ +package com.symphony.bdk.core.service.user.model; + +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; +import org.apiguardian.api.API; +import org.jspecify.annotations.Nullable; + +import java.util.Collections; +import java.util.List; + +/** + * Aggregates a user's personal organization data including directories/folders, multi-chat workspaces, + * and blast distribution lists. + */ +@Getter +@ToString +@EqualsAndHashCode +@NoArgsConstructor(force = true) +@AllArgsConstructor +@API(status = API.Status.EXPERIMENTAL) +public class UserOrganization { + + private final List directories; + private final List workspaces; + private final List distributionLists; + + public List getDirectories() { + return directories != null ? directories : Collections.emptyList(); + } + + public List getWorkspaces() { + return workspaces != null ? workspaces : Collections.emptyList(); + } + + public List getDistributionLists() { + return distributionLists != null ? distributionLists : Collections.emptyList(); + } + + /** + * Represents a custom user directory/folder containing conversation stream IDs. + */ + @Getter + @ToString + @EqualsAndHashCode + @NoArgsConstructor(force = true) + @AllArgsConstructor + @API(status = API.Status.EXPERIMENTAL) + public static class Directory { + private final @Nullable String id; + private final @Nullable String name; + private final List streamIds; + + public List getStreamIds() { + return streamIds != null ? streamIds : Collections.emptyList(); + } + } + + /** + * Represents a user workspace configuration with pinned or grouped conversation stream IDs. + */ + @Getter + @ToString + @EqualsAndHashCode + @NoArgsConstructor(force = true) + @AllArgsConstructor + @API(status = API.Status.EXPERIMENTAL) + public static class Workspace { + private final @Nullable String id; + private final @Nullable String name; + private final List streamIds; + + public List getStreamIds() { + return streamIds != null ? streamIds : Collections.emptyList(); + } + } + + /** + * Represents a blast distribution list containing stream IDs and detailed recipients. + */ + @Getter + @ToString + @EqualsAndHashCode + @NoArgsConstructor(force = true) + @AllArgsConstructor + @API(status = API.Status.EXPERIMENTAL) + public static class DistributionList { + private final @Nullable String id; + private final @Nullable String name; + private final List streamIds; + private final List recipients; + + public DistributionList(@Nullable String id, @Nullable String name, List streamIds) { + this(id, name, streamIds, Collections.emptyList()); + } + + public List getStreamIds() { + return streamIds != null ? streamIds : Collections.emptyList(); + } + + public List getRecipients() { + return recipients != null ? recipients : Collections.emptyList(); + } + } + + /** + * Represents a recipient in a blast distribution list (can be a user or a stream). + */ + @Getter + @ToString + @EqualsAndHashCode + @NoArgsConstructor(force = true) + @AllArgsConstructor + @API(status = API.Status.EXPERIMENTAL) + public static class Recipient { + private final @Nullable String id; + private final @Nullable String type; + } +} diff --git a/symphony-bdk-core/src/main/java/com/symphony/bdk/core/service/user/model/package-info.java b/symphony-bdk-core/src/main/java/com/symphony/bdk/core/service/user/model/package-info.java new file mode 100644 index 000000000..4538a74d9 --- /dev/null +++ b/symphony-bdk-core/src/main/java/com/symphony/bdk/core/service/user/model/package-info.java @@ -0,0 +1,4 @@ +@NullMarked +package com.symphony.bdk.core.service.user.model; + +import org.jspecify.annotations.NullMarked; diff --git a/symphony-bdk-core/src/test/java/com/symphony/bdk/core/ServiceFactoryTest.java b/symphony-bdk-core/src/test/java/com/symphony/bdk/core/ServiceFactoryTest.java index 4e151e335..39c88dc1b 100644 --- a/symphony-bdk-core/src/test/java/com/symphony/bdk/core/ServiceFactoryTest.java +++ b/symphony-bdk-core/src/test/java/com/symphony/bdk/core/ServiceFactoryTest.java @@ -30,6 +30,7 @@ import com.symphony.bdk.core.service.session.SessionService; import com.symphony.bdk.core.service.signal.SignalService; import com.symphony.bdk.core.service.stream.StreamService; +import com.symphony.bdk.core.service.user.UserOrganizationService; import com.symphony.bdk.core.service.user.UserService; import com.symphony.bdk.gen.api.model.UserV2; import com.symphony.bdk.http.api.ApiClient; @@ -55,6 +56,7 @@ void setUp() throws BdkConfigException { this.apiClientFactory = mock(ApiClientFactory.class); when(this.apiClientFactory.getPodClient()).thenReturn(mPodClient); + when(this.apiClientFactory.getUsersClient()).thenReturn(mock(ApiClient.class)); when(this.apiClientFactory.getAgentClient()).thenReturn(mock(ApiClient.class)); ApiClient datafeedAgentClient = mock(ApiClient.class); @@ -71,6 +73,12 @@ void getUserServiceTest() { assertNotNull(userService); } + @Test + void getUserOrganizationServiceTest() { + UserOrganizationService userOrganizationService = this.serviceFactory.getUserOrganizationService(); + assertNotNull(userOrganizationService); + } + @Test void getStreamServiceTest() { StreamService streamService = this.serviceFactory.getStreamService(); diff --git a/symphony-bdk-core/src/test/java/com/symphony/bdk/core/service/user/UserOrganizationServiceTest.java b/symphony-bdk-core/src/test/java/com/symphony/bdk/core/service/user/UserOrganizationServiceTest.java new file mode 100644 index 000000000..4efbc5e7f --- /dev/null +++ b/symphony-bdk-core/src/test/java/com/symphony/bdk/core/service/user/UserOrganizationServiceTest.java @@ -0,0 +1,356 @@ +package com.symphony.bdk.core.service.user; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.symphony.bdk.core.auth.AuthSession; +import com.symphony.bdk.core.retry.RetryWithRecoveryBuilder; +import com.symphony.bdk.core.service.user.model.UserOrganization; +import com.symphony.bdk.core.service.user.model.UserOrganization.Directory; +import com.symphony.bdk.core.service.user.model.UserOrganization.DistributionList; +import com.symphony.bdk.core.service.user.model.UserOrganization.Workspace; +import com.symphony.bdk.core.test.MockApiClient; +import com.symphony.bdk.http.api.ApiClient; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.List; + +class UserOrganizationServiceTest { + + private static final String SESSION_TOKEN = "test-session-token"; + private static final String ROOT_PATH = "/webcontroller/clientdata/GET/symphonyPrime/"; + private static final String LEFTNAV_PATH = "/webcontroller/clientdata/GET/symphonyPrime/leftnav"; + private static final String WORKSPACES_PATH = "/webcontroller/clientdata/GET/symphonyPrime/canvasState"; + private static final String DISTRO_LISTS_PATH = "/webcontroller/clientdata/GET/symphonyPrime/C2/distributionLists"; + + private MockApiClient mockApiClient; + private AuthSession authSession; + private UserOrganizationService service; + + @BeforeEach + void setUp() { + this.mockApiClient = new MockApiClient(); + this.authSession = mock(AuthSession.class); + when(this.authSession.getSessionToken()).thenReturn(SESSION_TOKEN); + + ApiClient usersClient = this.mockApiClient.getApiClient(""); + this.service = new UserOrganizationService(usersClient, this.authSession, new RetryWithRecoveryBuilder<>()); + } + + @Test + void testGetUserOrganizationSuccessWithRootSnapshot() { + String rootJson = "{\n" + + " \"status\": \"OK\",\n" + + " \"document\": {\n" + + " \"leftnav\": {\n" + + " \"groups\": {\n" + + " \"conversations\": [\n" + + " \"raw-stream-1\",\n" + + " {\n" + + " \"id\": \"folder-27001\",\n" + + " \"name\": \"Other Teams\",\n" + + " \"items\": [\"stream-a\", \"stream-b\"]\n" + + " }\n" + + " ]\n" + + " }\n" + + " },\n" + + " \"canvasState\": {\n" + + " \"savedTabs\": [\n" + + " {\n" + + " \"id\": \"tab-1001\",\n" + + " \"title\": \"SRE Workspace\",\n" + + " \"isWorkspace\": true,\n" + + " \"layoutId\": \"layout-sre\"\n" + + " }\n" + + " ]\n" + + " },\n" + + " \"dynamicLayouts\": {\n" + + " \"layout-sre\": {\n" + + " \"dynamicModules\": [\n" + + " [\n" + + " {\n" + + " \"type\": \"chat\",\n" + + " \"props\": {\"id\": \"sre-stream-1\", \"item\": {\"name\": \"Pierre\"}}\n" + + " },\n" + + " {\n" + + " \"type\": \"chat\",\n" + + " \"props\": {\"id\": \"sre-stream-2\", \"item\": {\"name\": \"Cedric\"}}\n" + + " }\n" + + " ]\n" + + " ]\n" + + " }\n" + + " }\n" + + " }\n" + + "}"; + + String distroJson = "{\n" + + " \"status\": \"OK\",\n" + + " \"document\": {\n" + + " \"a97be35a-a9a8-48c0-9863-3e697f76c9d5\": {\n" + + " \"id\": \"a97be35a-a9a8-48c0-9863-3e697f76c9d5\",\n" + + " \"name\": \"Blast People\",\n" + + " \"recipients\": [\n" + + " {\"id\": \"699220675788807\", \"type\": \"user\"},\n" + + " {\"id\": \"bPCpxk9mbzZnFvrGAYY6T3///mDhvvDwdA==\", \"type\": \"stream\"}\n" + + " ]\n" + + " }\n" + + " }\n" + + "}"; + + this.mockApiClient.onGet(ROOT_PATH, rootJson); + this.mockApiClient.onGet(DISTRO_LISTS_PATH, distroJson); + + UserOrganization org = this.service.getUserOrganization(); + assertNotNull(org); + + // Assert directories + assertEquals(1, org.getDirectories().size()); + Directory dir = org.getDirectories().get(0); + assertEquals("folder-27001", dir.getId()); + assertEquals("Other Teams", dir.getName()); + assertEquals(List.of("stream-a", "stream-b"), dir.getStreamIds()); + + // Assert workspaces + assertEquals(1, org.getWorkspaces().size()); + Workspace ws = org.getWorkspaces().get(0); + assertEquals("tab-1001", ws.getId()); + assertEquals("SRE Workspace", ws.getName()); + assertEquals(List.of("sre-stream-1", "sre-stream-2"), ws.getStreamIds()); + + // Assert distribution lists + assertEquals(1, org.getDistributionLists().size()); + DistributionList dl = org.getDistributionLists().get(0); + assertEquals("a97be35a-a9a8-48c0-9863-3e697f76c9d5", dl.getId()); + assertEquals("Blast People", dl.getName()); + assertEquals(List.of("bPCpxk9mbzZnFvrGAYY6T3///mDhvvDwdA=="), dl.getStreamIds()); + assertEquals(2, dl.getRecipients().size()); + assertEquals("699220675788807", dl.getRecipients().get(0).getId()); + assertEquals("user", dl.getRecipients().get(0).getType()); + assertEquals("bPCpxk9mbzZnFvrGAYY6T3///mDhvvDwdA==", dl.getRecipients().get(1).getId()); + assertEquals("stream", dl.getRecipients().get(1).getType()); + } + + @Test + void testGetUserDirectoriesSubpath() { + String leftNavJson = "{\n" + + " \"status\": \"OK\",\n" + + " \"document\": {\n" + + " \"groups\": {\n" + + " \"conversations\": [\n" + + " \"chat-pinned-1\",\n" + + " {\n" + + " \"id\": \"folder-1\",\n" + + " \"name\": \"My Folder\",\n" + + " \"items\": [\"s1\", \"s2\"]\n" + + " }\n" + + " ]\n" + + " }\n" + + " }\n" + + "}"; + + this.mockApiClient.onGet(LEFTNAV_PATH, leftNavJson); + + List directories = this.service.getUserDirectories(); + assertEquals(1, directories.size()); + assertEquals("folder-1", directories.get(0).getId()); + assertEquals("My Folder", directories.get(0).getName()); + assertEquals(List.of("s1", "s2"), directories.get(0).getStreamIds()); + } + + @Test + void testGetUserDistributionLists() { + String distroJson = "{\n" + + " \"status\": \"OK\",\n" + + " \"document\": {\n" + + " \"list-1\": {\n" + + " \"id\": \"list-1\",\n" + + " \"name\": \"VIP List\",\n" + + " \"recipients\": [\n" + + " {\"id\": \"user-1\", \"type\": \"user\"},\n" + + " {\"id\": \"stream-1\", \"type\": \"stream\"}\n" + + " ]\n" + + " }\n" + + " }\n" + + "}"; + + this.mockApiClient.onGet(DISTRO_LISTS_PATH, distroJson); + + List lists = this.service.getUserDistributionLists(); + assertEquals(1, lists.size()); + assertEquals("list-1", lists.get(0).getId()); + assertEquals("VIP List", lists.get(0).getName()); + assertEquals(List.of("stream-1"), lists.get(0).getStreamIds()); + } + + @Test + void testGetWorkspacesFallbackToCustomWorkspaces() { + String fallbackJson = "{\n" + + " \"status\": \"OK\",\n" + + " \"document\": {\n" + + " \"customWorkspaces\": [\n" + + " {\n" + + " \"id\": \"ws-1\",\n" + + " \"name\": \"Legacy Workspace\",\n" + + " \"items\": [\"s-100\", \"s-200\"]\n" + + " }\n" + + " ]\n" + + " }\n" + + "}"; + + this.mockApiClient.onGet(ROOT_PATH, fallbackJson); + + List workspaces = this.service.getUserWorkspaces(); + assertEquals(1, workspaces.size()); + assertEquals("ws-1", workspaces.get(0).getId()); + assertEquals("Legacy Workspace", workspaces.get(0).getName()); + assertEquals(List.of("s-100", "s-200"), workspaces.get(0).getStreamIds()); + } + + @Test + void test404ReturnsEmptyGracefully() { + this.mockApiClient.onGet(404, ROOT_PATH, "{}"); + this.mockApiClient.onGet(404, LEFTNAV_PATH, "{}"); + this.mockApiClient.onGet(404, WORKSPACES_PATH, "{}"); + this.mockApiClient.onGet(404, DISTRO_LISTS_PATH, "{}"); + + UserOrganization org = this.service.getUserOrganization(); + assertNotNull(org); + assertTrue(org.getDirectories().isEmpty()); + assertTrue(org.getWorkspaces().isEmpty()); + assertTrue(org.getDistributionLists().isEmpty()); + + assertTrue(this.service.getUserDirectories().isEmpty()); + assertTrue(this.service.getUserWorkspaces().isEmpty()); + assertTrue(this.service.getUserDistributionLists().isEmpty()); + } + + @Test + void testOboModeExecution() { + AuthSession oboSession = mock(AuthSession.class); + when(oboSession.getSessionToken()).thenReturn("obo-token"); + + UserOrganizationService oboService = this.service.obo(oboSession); + assertNotNull(oboService); + + String distroJson = "{\n" + + " \"status\": \"OK\",\n" + + " \"document\": {\n" + + " \"list-obo\": {\n" + + " \"id\": \"list-obo\",\n" + + " \"name\": \"OBO List\",\n" + + " \"recipients\": [{\"id\": \"stream-obo\", \"type\": \"stream\"}]\n" + + " }\n" + + " }\n" + + "}"; + + this.mockApiClient.onGet(DISTRO_LISTS_PATH, distroJson); + List lists = oboService.getUserDistributionLists(); + assertEquals(1, lists.size()); + assertEquals("list-obo", lists.get(0).getId()); + } + + @Test + void testMissingAuthSessionThrowsIllegalStateException() { + ApiClient usersClient = this.mockApiClient.getApiClient(""); + UserOrganizationService unauthenticatedService = new UserOrganizationService(usersClient, new RetryWithRecoveryBuilder<>()); + + assertThrows(IllegalStateException.class, unauthenticatedService::getUserOrganization); + } + + @Test + void testParseDistributionListsWithUserProvidedSample() { + String sample = "{\n" + + " \"status\": \"OK\",\n" + + " \"document\": {\n" + + " \"a97be35a-a9a8-48c0-9863-3e697f76c9d5\": {\n" + + " \"id\": \"a97be35a-a9a8-48c0-9863-3e697f76c9d5\",\n" + + " \"name\": \"Blast People\",\n" + + " \"recipients\": [\n" + + " {\n" + + " \"id\": \"699220675788807\",\n" + + " \"type\": \"user\"\n" + + " },\n" + + " {\n" + + " \"id\": \"699220675788806\",\n" + + " \"type\": \"user\"\n" + + " },\n" + + " {\n" + + " \"id\": \"bPCpxk9mbzZnFvrGAYY6T3///mDhvvDwdA==\",\n" + + " \"type\": \"stream\"\n" + + " }\n" + + " ]\n" + + " }\n" + + " }\n" + + "}"; + + this.mockApiClient.onGet(DISTRO_LISTS_PATH, sample); + + List result = this.service.getUserDistributionLists(); + assertEquals(1, result.size()); + DistributionList dl = result.get(0); + assertEquals("a97be35a-a9a8-48c0-9863-3e697f76c9d5", dl.getId()); + assertEquals("Blast People", dl.getName()); + assertEquals(1, dl.getStreamIds().size()); + assertEquals("bPCpxk9mbzZnFvrGAYY6T3///mDhvvDwdA==", dl.getStreamIds().get(0)); + assertEquals(3, dl.getRecipients().size()); + assertEquals("699220675788807", dl.getRecipients().get(0).getId()); + assertEquals("user", dl.getRecipients().get(0).getType()); + assertEquals("699220675788806", dl.getRecipients().get(1).getId()); + assertEquals("user", dl.getRecipients().get(1).getType()); + assertEquals("bPCpxk9mbzZnFvrGAYY6T3///mDhvvDwdA==", dl.getRecipients().get(2).getId()); + assertEquals("stream", dl.getRecipients().get(2).getType()); + } + + @Test + void testParseDirectoriesWithPrimeJsonSnippet() { + String primeSnippet = "{\n" + + " \"status\": \"OK\",\n" + + " \"document\": {\n" + + " \"leftnav\": {\n" + + " \"groups\": {\n" + + " \"conversations\": [\n" + + " \"mQGBc6YxSXe4VymtQpVdA3///qZraOq2dA==\",\n" + + " {\n" + + " \"id\": \"folder-27001\",\n" + + " \"name\": \"Other Teams\",\n" + + " \"items\": [\n" + + " \"hBYiyjJxRpyRgy5y3nBQgX///rNLnbBjdA==\",\n" + + " \"Xak486Jkcco5WOQkHlgZBn///rMC6YWkdA==\",\n" + + " \"/UwvEK2mMofbtrhFw5NY83///p3g/tb2dA==\"\n" + + " ]\n" + + " },\n" + + " {\n" + + " \"id\": \"folder-2008\",\n" + + " \"name\": \"RANDOM\",\n" + + " \"items\": []\n" + + " },\n" + + " \"/Mc+INWneo2r0A3efYhAKn///pYciXMrdA==\"\n" + + " ]\n" + + " }\n" + + " }\n" + + " }\n" + + "}"; + + this.mockApiClient.onGet(LEFTNAV_PATH, primeSnippet); + + List directories = this.service.getUserDirectories(); + assertEquals(2, directories.size()); + + Directory d1 = directories.get(0); + assertEquals("folder-27001", d1.getId()); + assertEquals("Other Teams", d1.getName()); + assertEquals(3, d1.getStreamIds().size()); + assertTrue(d1.getStreamIds().contains("hBYiyjJxRpyRgy5y3nBQgX///rNLnbBjdA==")); + + Directory d2 = directories.get(1); + assertEquals("folder-2008", d2.getId()); + assertEquals("RANDOM", d2.getName()); + assertTrue(d2.getStreamIds().isEmpty()); + } +} diff --git a/symphony-bdk-spring/symphony-bdk-core-spring-boot-starter/src/main/java/com/symphony/bdk/spring/config/BdkCoreConfig.java b/symphony-bdk-spring/symphony-bdk-core-spring-boot-starter/src/main/java/com/symphony/bdk/spring/config/BdkCoreConfig.java index de3fc0025..a3d57cab1 100644 --- a/symphony-bdk-spring/symphony-bdk-core-spring-boot-starter/src/main/java/com/symphony/bdk/spring/config/BdkCoreConfig.java +++ b/symphony-bdk-spring/symphony-bdk-core-spring-boot-starter/src/main/java/com/symphony/bdk/spring/config/BdkCoreConfig.java @@ -71,6 +71,13 @@ public ApiClient podApiClient(ApiClientFactory apiClientFactory, Optional botSession, BdkConfig config) { + ApiClient client = apiClientFactory.getUsersClient(); + addCommonJwtConfig(client, botSession, config); + return client; + } + @Bean(name = "relayApiClient") public ApiClient relayApiClient(ApiClientFactory apiClientFactory) { return apiClientFactory.getRelayClient(); diff --git a/symphony-bdk-spring/symphony-bdk-core-spring-boot-starter/src/main/java/com/symphony/bdk/spring/config/BdkOboServiceConfig.java b/symphony-bdk-spring/symphony-bdk-core-spring-boot-starter/src/main/java/com/symphony/bdk/spring/config/BdkOboServiceConfig.java index 6abcf7c5e..1e857f430 100644 --- a/symphony-bdk-spring/symphony-bdk-core-spring-boot-starter/src/main/java/com/symphony/bdk/spring/config/BdkOboServiceConfig.java +++ b/symphony-bdk-spring/symphony-bdk-core-spring-boot-starter/src/main/java/com/symphony/bdk/spring/config/BdkOboServiceConfig.java @@ -12,6 +12,7 @@ import com.symphony.bdk.core.service.session.SessionService; import com.symphony.bdk.core.service.signal.SignalService; import com.symphony.bdk.core.service.stream.StreamService; +import com.symphony.bdk.core.service.user.UserOrganizationService; import com.symphony.bdk.core.service.user.UserService; import com.symphony.bdk.gen.api.AttachmentsApi; import com.symphony.bdk.gen.api.AuditTrailApi; @@ -30,10 +31,12 @@ import com.symphony.bdk.gen.api.StreamsApi; import com.symphony.bdk.gen.api.UserApi; import com.symphony.bdk.gen.api.UsersApi; +import com.symphony.bdk.http.api.ApiClient; import com.symphony.bdk.template.api.TemplateEngine; import org.apiguardian.api.API; import org.springframework.beans.factory.BeanInitializationException; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.context.annotation.Bean; @@ -84,6 +87,13 @@ public UserService oboUserService(UserApi userApi, UsersApi usersApi, AuditTrail return new UserService(userApi, usersApi, auditTrailApi, new RetryWithRecoveryBuilder<>().retryConfig(config.getRetry())); } + @Bean + @ConditionalOnMissingBean + public UserOrganizationService oboUserOrganizationService( + @Qualifier("usersApiClient") ApiClient usersApiClient, BdkConfig config) { + return new UserOrganizationService(usersApiClient, new RetryWithRecoveryBuilder<>().retryConfig(config.getRetry())); + } + @Bean @ConditionalOnMissingBean public PresenceService oboPresenceService(PresenceApi presenceApi, BdkConfig config) { diff --git a/symphony-bdk-spring/symphony-bdk-core-spring-boot-starter/src/main/java/com/symphony/bdk/spring/config/BdkServiceConfig.java b/symphony-bdk-spring/symphony-bdk-core-spring-boot-starter/src/main/java/com/symphony/bdk/spring/config/BdkServiceConfig.java index 5228a722e..a217814f9 100644 --- a/symphony-bdk-spring/symphony-bdk-core-spring-boot-starter/src/main/java/com/symphony/bdk/spring/config/BdkServiceConfig.java +++ b/symphony-bdk-spring/symphony-bdk-core-spring-boot-starter/src/main/java/com/symphony/bdk/spring/config/BdkServiceConfig.java @@ -13,6 +13,7 @@ import com.symphony.bdk.core.service.session.SessionService; import com.symphony.bdk.core.service.signal.SignalService; import com.symphony.bdk.core.service.stream.StreamService; +import com.symphony.bdk.core.service.user.UserOrganizationService; import com.symphony.bdk.core.service.user.UserService; import com.symphony.bdk.gen.api.AppEntitlementApi; import com.symphony.bdk.gen.api.ApplicationApi; @@ -35,10 +36,12 @@ import com.symphony.bdk.gen.api.SystemApi; import com.symphony.bdk.gen.api.UserApi; import com.symphony.bdk.gen.api.UsersApi; +import com.symphony.bdk.http.api.ApiClient; import com.symphony.bdk.spring.service.BotInfoService; import com.symphony.bdk.template.api.TemplateEngine; import org.apiguardian.api.API; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; @@ -70,6 +73,13 @@ public UserService userService(UserApi userApi, UsersApi usersApi, AuditTrailApi return new UserService(userApi, usersApi, auditTrailApi, botSession, new RetryWithRecoveryBuilder<>().retryConfig(config.getRetry())); } + @Bean + @ConditionalOnMissingBean + public UserOrganizationService userOrganizationService( + @Qualifier("usersApiClient") ApiClient usersApiClient, AuthSession botSession, BdkConfig config) { + return new UserOrganizationService(usersApiClient, botSession, new RetryWithRecoveryBuilder<>().retryConfig(config.getRetry())); + } + @Bean @ConditionalOnMissingBean public DisclaimerService disclaimerService(DisclaimerApi disclaimerApi, AuthSession botSession, BdkConfig config) { diff --git a/symphony-bdk-spring/symphony-bdk-core-spring-boot-starter/src/test/java/com/symphony/bdk/spring/SymphonyBdkMockedConfiguration.java b/symphony-bdk-spring/symphony-bdk-core-spring-boot-starter/src/test/java/com/symphony/bdk/spring/SymphonyBdkMockedConfiguration.java index c23f2178e..d6484f406 100644 --- a/symphony-bdk-spring/symphony-bdk-core-spring-boot-starter/src/test/java/com/symphony/bdk/spring/SymphonyBdkMockedConfiguration.java +++ b/symphony-bdk-spring/symphony-bdk-core-spring-boot-starter/src/test/java/com/symphony/bdk/spring/SymphonyBdkMockedConfiguration.java @@ -81,6 +81,11 @@ public ApiClient getPodClient() { return this.podApiClient.getApiClient("/pod"); } + @Override + public ApiClient getUsersClient() { + return this.podApiClient.getApiClient(""); + } + @Override public ApiClient getRelayClient() { From ff22875e6363b998cb39bd8892e8b2760df65a03 Mon Sep 17 00:00:00 2001 From: jameshanna-symphony Date: Wed, 30 Sep 2026 16:02:35 +0100 Subject: [PATCH 02/17] Update UserOrganizationService.java --- .../service/user/UserOrganizationService.java | 274 +++++++----------- 1 file changed, 97 insertions(+), 177 deletions(-) diff --git a/symphony-bdk-core/src/main/java/com/symphony/bdk/core/service/user/UserOrganizationService.java b/symphony-bdk-core/src/main/java/com/symphony/bdk/core/service/user/UserOrganizationService.java index 9cce93578..6386e26d2 100644 --- a/symphony-bdk-core/src/main/java/com/symphony/bdk/core/service/user/UserOrganizationService.java +++ b/symphony-bdk-core/src/main/java/com/symphony/bdk/core/service/user/UserOrganizationService.java @@ -23,12 +23,14 @@ import tools.jackson.databind.JsonNode; import tools.jackson.databind.ObjectMapper; import tools.jackson.databind.json.JsonMapper; +import tools.jackson.databind.node.MissingNode; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Objects; /** * Service to retrieve user organization data, including custom folders/directories, @@ -43,9 +45,7 @@ public class UserOrganizationService implements OboService { - JsonNode rootNode = fetchClientDataJson(""); - List directories; - List workspaces; + JsonNode primeDoc = fetchDocument(""); + JsonNode distroDoc = fetchDocument(DISTRO_LISTS_PATH); - if (rootNode != null) { - directories = parseDirectories(rootNode); - workspaces = parseWorkspaces(rootNode); - } else { - JsonNode leftNavNode = fetchClientDataJson(LEFTNAV_SUBPATH); - directories = leftNavNode != null ? parseDirectories(leftNavNode) : Collections.emptyList(); - - JsonNode canvasNode = fetchClientDataJson(CANVAS_STATE_SUBPATH); - workspaces = canvasNode != null ? parseWorkspaces(canvasNode) : Collections.emptyList(); - } - - JsonNode distroNode = fetchClientDataJson(DISTRIBUTION_LISTS_SUBPATH); - List distributionLists = distroNode != null ? parseDistributionLists(distroNode) : Collections.emptyList(); - - return new UserOrganization(directories, workspaces, distributionLists); + return new UserOrganization( + parseDirectories(primeDoc), + parseWorkspaces(primeDoc), + parseDistributionLists(distroDoc) + ); }); } @@ -106,11 +95,11 @@ public UserOrganization getUserOrganization() { */ public List getUserDirectories() { return executeAndRetry("getUserDirectories", () -> { - JsonNode leftNavNode = fetchClientDataJson(LEFTNAV_SUBPATH); - if (leftNavNode == null) { - leftNavNode = fetchClientDataJson(""); + JsonNode doc = fetchDocument("leftnav"); + if (doc.isMissingNode()) { + doc = fetchDocument(""); } - return leftNavNode != null ? parseDirectories(leftNavNode) : Collections.emptyList(); + return parseDirectories(doc); }); } @@ -120,13 +109,7 @@ public List getUserDirectories() { * @return a list of {@link Workspace} instances. */ public List getUserWorkspaces() { - return executeAndRetry("getUserWorkspaces", () -> { - JsonNode rootNode = fetchClientDataJson(""); - if (rootNode == null) { - rootNode = fetchClientDataJson(CANVAS_STATE_SUBPATH); - } - return rootNode != null ? parseWorkspaces(rootNode) : Collections.emptyList(); - }); + return executeAndRetry("getUserWorkspaces", () -> parseWorkspaces(fetchDocument(""))); } /** @@ -135,75 +118,22 @@ public List getUserWorkspaces() { * @return a list of {@link DistributionList} instances. */ public List getUserDistributionLists() { - return executeAndRetry("getUserDistributionLists", () -> { - JsonNode distroNode = fetchClientDataJson(DISTRIBUTION_LISTS_SUBPATH); - return distroNode != null ? parseDistributionLists(distroNode) : Collections.emptyList(); - }); - } - - private T executeAndRetry(String name, SupplierWithApiException supplier) { - checkAuthSession(this.authSession); - return RetryWithRecovery.executeAndRetry(this.retryBuilder, name, this.apiClient.getBasePath(), supplier); - } - - private @Nullable JsonNode fetchClientDataJson(String subPath) throws ApiException { - Map headerParams = new HashMap<>(); - if (this.authSession != null && this.authSession.getSessionToken() != null) { - headerParams.put("sessionToken", this.authSession.getSessionToken()); - } - - String fullPath = CLIENT_DATA_ROOT + (subPath.isEmpty() ? "/" : "/" + subPath); - try { - ApiResponse response = this.apiClient.invokeAPI( - fullPath, - "GET", - Collections.emptyList(), - null, - headerParams, - Collections.emptyMap(), - Collections.emptyMap(), - "application/json", - "application/json", - new String[0], - new TypeReference() {} - ); - if (response != null && response.getData() != null) { - Object data = response.getData(); - if (data instanceof String s) { - if (!s.trim().isEmpty()) { - return MAPPER.readTree(s); - } - } else { - return MAPPER.valueToTree(data); - } - } - } catch (ApiException e) { - if (e.getCode() == 404) { - log.debug("No client-data found for path: {}", fullPath); - return null; - } - throw e; - } catch (JacksonException e) { - log.warn("Failed to parse client-data JSON for path: {}", fullPath, e); - } - return null; + return executeAndRetry("getUserDistributionLists", () -> parseDistributionLists(fetchDocument(DISTRO_LISTS_PATH))); } /** * Parses custom folders/directories from leftnav JSON. */ - public static List parseDirectories(JsonNode root) { - JsonNode doc = root.has("document") ? root.get("document") : root; - JsonNode leftnav = doc.has("leftnav") ? doc.get("leftnav") : doc; - + public static List parseDirectories(JsonNode doc) { List directories = new ArrayList<>(); + JsonNode leftnav = doc.has("leftnav") ? doc.get("leftnav") : doc; for (JsonNode item : leftnav.path("groups").path("conversations")) { if (item.isObject()) { - String id = item.path("id").asText(null); - String name = item.path("name").asText(null); - List streamIds = extractStreamIds(item.path("items")); - if (id != null || name != null || !streamIds.isEmpty()) { - directories.add(new Directory(id, name, streamIds)); + try { + FolderDto folder = MAPPER.treeToValue(item, FolderDto.class); + directories.add(new Directory(folder.id(), folder.name(), folder.items() != null ? folder.items() : Collections.emptyList())); + } catch (JacksonException e) { + log.warn("Failed to parse folder: {}", item, e); } } } @@ -211,119 +141,109 @@ public static List parseDirectories(JsonNode root) { } /** - * Parses workspaces from canvasState, dynamicLayouts, or customWorkspaces. + * Parses workspaces from canvasState and dynamicLayouts (or customWorkspaces fallback). */ - public static List parseWorkspaces(JsonNode root) { - JsonNode doc = root.path("document"); - if (doc.isMissingNode()) doc = root; - + public static List parseWorkspaces(JsonNode doc) { + List workspaces = new ArrayList<>(); JsonNode savedTabs = doc.path("canvasState").path("savedTabs"); JsonNode dynamicLayouts = doc.path("dynamicLayouts"); - List workspaces = new ArrayList<>(); - if (savedTabs.isArray() && !savedTabs.isEmpty()) { - for (JsonNode tab : savedTabs) { - if (tab.path("isWorkspace").asBoolean(false) || tab.has("layoutId")) { - String id = tab.path("id").asText(null); - String name = tab.hasNonNull("title") ? tab.get("title").asText() : tab.path("label").asText(null); - String layoutId = tab.path("layoutId").asText(null); - List streamIds = new ArrayList<>(); - if (layoutId != null && dynamicLayouts.has(layoutId)) { - extractStreams(dynamicLayouts.get(layoutId).path("dynamicModules"), streamIds); - } - if (id != null || name != null || !streamIds.isEmpty()) { - workspaces.add(new Workspace(id, name, streamIds)); - } - } + for (JsonNode tab : savedTabs) { + if (tab.path("isWorkspace").asBoolean(false) || tab.has("layoutId")) { + String layoutId = tab.path("layoutId").asText(""); + List streams = dynamicLayouts.path(layoutId).path("dynamicModules").findValues("props") + .stream() + .map(p -> p.path("id").asText(null)) + .filter(Objects::nonNull) + .distinct() + .toList(); + workspaces.add(new Workspace(tab.path("id").asText(null), tab.path("title").asText(null), streams)); } - } else { - JsonNode customWs = doc.has("customWorkspaces") ? doc.get("customWorkspaces") : doc.path("workspaces"); - for (JsonNode ws : customWs) { + } + + if (workspaces.isEmpty()) { + for (JsonNode ws : doc.path("customWorkspaces")) { if (ws.isObject()) { - List streamIds = extractStreamIds(ws.path("items")); - if (streamIds.isEmpty()) streamIds = extractStreamIds(ws.path("streams")); - if (streamIds.isEmpty()) streamIds = extractStreamIds(ws.path("streamIds")); - workspaces.add(new Workspace(ws.path("id").asText(null), ws.path("name").asText(null), streamIds)); + List items = ws.path("items").values().stream().map(JsonNode::asText).toList(); + workspaces.add(new Workspace(ws.path("id").asText(null), ws.path("name").asText(null), items)); } } } return workspaces; } - private static void extractStreams(JsonNode node, List streamIds) { - if (node == null || node.isMissingNode()) return; - if (node.isArray()) { - node.forEach(child -> extractStreams(child, streamIds)); - } else if (node.isObject() && ("chat".equalsIgnoreCase(node.path("type").asText()) || node.has("props"))) { - String id = node.path("props").hasNonNull("id") ? node.path("props").get("id").asText() - : (node.path("props").path("item").hasNonNull("id") ? node.path("props").path("item").get("id").asText() - : node.path("viewId").asText(null)); - if (id != null && !streamIds.contains(id)) { - streamIds.add(id); - } - } - } - - private static List extractStreamIds(JsonNode arrayNode) { - List streamIds = new ArrayList<>(); - if (arrayNode.isArray()) { - for (JsonNode item : arrayNode) { - String id = item.isObject() ? (item.hasNonNull("id") ? item.get("id").asText() : item.path("streamId").asText(null)) : item.asText(null); - if (id != null) { - streamIds.add(id); - } + /** + * Parses distribution lists from C2/distributionLists response using Jackson DTO binding. + */ + public static List parseDistributionLists(JsonNode doc) { + JsonNode distroNode = doc.path("C2").has("distributionLists") ? doc.path("C2").get("distributionLists") : doc; + try { + if (distroNode.isObject()) { + Map map = MAPPER.treeToValue(distroNode, + new tools.jackson.core.type.TypeReference>() {}); + return map != null ? map.values().stream().map(DistributionListDto::toDomain).toList() : Collections.emptyList(); + } else if (distroNode.isArray()) { + List list = MAPPER.treeToValue(distroNode, + new tools.jackson.core.type.TypeReference>() {}); + return list != null ? list.stream().map(DistributionListDto::toDomain).toList() : Collections.emptyList(); } + } catch (JacksonException e) { + log.warn("Failed to deserialize distribution lists", e); } - return streamIds; + return Collections.emptyList(); } - /** - * Parses distribution lists from C2/distributionLists response using Jackson DTO binding. - */ - public static List parseDistributionLists(JsonNode root) { - JsonNode doc = root.path("document"); - if (doc.isMissingNode()) doc = root; - if (doc.has("C2") && doc.get("C2").has("distributionLists")) { - doc = doc.get("C2").get("distributionLists"); + private JsonNode fetchDocument(String subPath) throws ApiException { + Map headerParams = new HashMap<>(); + if (this.authSession != null && this.authSession.getSessionToken() != null) { + headerParams.put("sessionToken", this.authSession.getSessionToken()); } + String fullPath = CLIENT_DATA_ROOT + (subPath.isEmpty() ? "/" : "/" + subPath); try { - if (doc.isObject()) { - Map map = MAPPER.treeToValue(doc, - new tools.jackson.core.type.TypeReference>() {}); - return map != null ? map.values().stream().map(RawDistributionList::toDomain).toList() : Collections.emptyList(); - } else if (doc.isArray()) { - List list = MAPPER.treeToValue(doc, - new tools.jackson.core.type.TypeReference>() {}); - return list != null ? list.stream().map(RawDistributionList::toDomain).toList() : Collections.emptyList(); + ApiResponse response = this.apiClient.invokeAPI( + fullPath, "GET", Collections.emptyList(), null, headerParams, + Collections.emptyMap(), Collections.emptyMap(), + "application/json", "application/json", new String[0], new TypeReference() {} + ); + if (response != null && response.getData() != null) { + Object data = response.getData(); + JsonNode root = (data instanceof String s) ? MAPPER.readTree(s) : MAPPER.valueToTree(data); + return root.path("document"); + } + } catch (ApiException e) { + if (e.getCode() == 404) { + return MissingNode.getInstance(); } + throw e; } catch (JacksonException e) { - log.warn("Failed to deserialize distribution lists", e); + log.warn("Failed to parse client-data JSON for path: {}", fullPath, e); } - return Collections.emptyList(); + return MissingNode.getInstance(); } + private T executeAndRetry(String name, SupplierWithApiException supplier) { + checkAuthSession(this.authSession); + return RetryWithRecovery.executeAndRetry(this.retryBuilder, name, this.apiClient.getBasePath(), supplier); + } + + /** + * Internal DTO for Jackson folder binding. + */ + @API(status = API.Status.EXPERIMENTAL) + public record FolderDto(@Nullable String id, @Nullable String name, @Nullable List items) {} + /** - * Internal DTO for Jackson direct binding from SymphonyPrime client-data payload. + * Internal DTO for Jackson distribution list binding. */ @API(status = API.Status.EXPERIMENTAL) - public record RawDistributionList( - @Nullable String id, - @Nullable String name, - @Nullable List recipients, - @Nullable List items, - @Nullable List streamIds - ) { + public record DistributionListDto(@Nullable String id, @Nullable String name, @Nullable List recipients) { public DistributionList toDomain() { List recs = this.recipients != null ? this.recipients : Collections.emptyList(); - List streams = new ArrayList<>(); - for (Recipient r : recs) { - if ("stream".equalsIgnoreCase(r.getType()) && r.getId() != null) { - streams.add(r.getId()); - } - } - if (streams.isEmpty() && this.items != null) streams.addAll(this.items); - if (streams.isEmpty() && this.streamIds != null) streams.addAll(this.streamIds); + List streams = recs.stream() + .filter(r -> "stream".equalsIgnoreCase(r.getType()) && r.getId() != null) + .map(Recipient::getId) + .toList(); return new DistributionList(this.id, this.name, streams, recs); } } From ac32e3e2892169c380266404e9f7f31477771ac7 Mon Sep 17 00:00:00 2001 From: jameshanna-symphony Date: Fri, 2 Oct 2026 09:46:52 +0100 Subject: [PATCH 03/17] Remove redunant code as we use symphony api spec + SBE Users --- .../service/user/UserOrganizationService.java | 209 ++---------- .../service/user/model/UserOrganization.java | 121 ------- .../core/service/user/model/package-info.java | 4 - .../user/UserOrganizationServiceTest.java | 319 +++--------------- 4 files changed, 80 insertions(+), 573 deletions(-) delete mode 100644 symphony-bdk-core/src/main/java/com/symphony/bdk/core/service/user/model/UserOrganization.java delete mode 100644 symphony-bdk-core/src/main/java/com/symphony/bdk/core/service/user/model/package-info.java diff --git a/symphony-bdk-core/src/main/java/com/symphony/bdk/core/service/user/UserOrganizationService.java b/symphony-bdk-core/src/main/java/com/symphony/bdk/core/service/user/UserOrganizationService.java index 6386e26d2..bed8b77a3 100644 --- a/symphony-bdk-core/src/main/java/com/symphony/bdk/core/service/user/UserOrganizationService.java +++ b/symphony-bdk-core/src/main/java/com/symphony/bdk/core/service/user/UserOrganizationService.java @@ -5,55 +5,34 @@ import com.symphony.bdk.core.retry.RetryWithRecoveryBuilder; import com.symphony.bdk.core.retry.function.SupplierWithApiException; import com.symphony.bdk.core.service.OboService; -import com.symphony.bdk.core.service.user.model.UserOrganization; -import com.symphony.bdk.core.service.user.model.UserOrganization.Directory; -import com.symphony.bdk.core.service.user.model.UserOrganization.DistributionList; -import com.symphony.bdk.core.service.user.model.UserOrganization.Recipient; -import com.symphony.bdk.core.service.user.model.UserOrganization.Workspace; +import com.symphony.bdk.gen.api.UserOrganisationsApi; +import com.symphony.bdk.gen.api.model.OrganisationStructure; +import com.symphony.bdk.gen.api.model.UserOrganisationsResponse; import com.symphony.bdk.http.api.ApiClient; import com.symphony.bdk.http.api.ApiException; -import com.symphony.bdk.http.api.ApiResponse; -import com.symphony.bdk.http.api.util.TypeReference; -import lombok.extern.slf4j.Slf4j; import org.apiguardian.api.API; import org.jspecify.annotations.Nullable; -import tools.jackson.core.JacksonException; -import tools.jackson.databind.DeserializationFeature; -import tools.jackson.databind.JsonNode; -import tools.jackson.databind.ObjectMapper; -import tools.jackson.databind.json.JsonMapper; -import tools.jackson.databind.node.MissingNode; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; import java.util.List; -import java.util.Map; -import java.util.Objects; /** - * Service to retrieve user organization data, including custom folders/directories, - * multi-chat workspaces, and blast distribution lists from Symphony client data. + * Service to retrieve the authenticated user's personal chat organization structures — + * custom directories/folders, multi-chat workspaces, and blast distribution lists — from the + * {@code GET /v5/users/organisations} endpoint. */ -@Slf4j @API(status = API.Status.EXPERIMENTAL) public class UserOrganizationService implements OboService { - private static final ObjectMapper MAPPER = JsonMapper.builder() - .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) - .build(); - - private static final String CLIENT_DATA_ROOT = "/webcontroller/clientdata/GET/symphonyPrime"; - private static final String DISTRO_LISTS_PATH = "C2/distributionLists"; - private final ApiClient apiClient; + private final UserOrganisationsApi userOrganisationsApi; private final @Nullable AuthSession authSession; private final RetryWithRecoveryBuilder retryBuilder; public UserOrganizationService(ApiClient apiClient, AuthSession authSession, RetryWithRecoveryBuilder retryBuilder) { this.apiClient = apiClient; + this.userOrganisationsApi = new UserOrganisationsApi(apiClient); this.authSession = authSession; this.retryBuilder = RetryWithRecoveryBuilder.copyWithoutRecoveryStrategies(retryBuilder) .recoveryStrategy(ApiException::isUnauthorized, authSession::refresh); @@ -61,6 +40,7 @@ public UserOrganizationService(ApiClient apiClient, AuthSession authSession, public UserOrganizationService(ApiClient apiClient, RetryWithRecoveryBuilder retryBuilder) { this.apiClient = apiClient; + this.userOrganisationsApi = new UserOrganisationsApi(apiClient); this.authSession = null; this.retryBuilder = RetryWithRecoveryBuilder.copyWithoutRecoveryStrategies(retryBuilder); } @@ -71,180 +51,45 @@ public UserOrganizationService obo(AuthSession oboSession) { } /** - * Retrieves the current user's aggregated organization data (directories, workspaces, and distribution lists). + * Retrieves the authenticated user's aggregated organization data (directories, workspaces, and + * blast distribution lists) for the default {@code symphonyPrime} application. * - * @return a {@link UserOrganization} containing the user's organization data. + * @return a {@link UserOrganisationsResponse} containing the user's organization data. */ - public UserOrganization getUserOrganization() { - return executeAndRetry("getUserOrganization", () -> { - JsonNode primeDoc = fetchDocument(""); - JsonNode distroDoc = fetchDocument(DISTRO_LISTS_PATH); - - return new UserOrganization( - parseDirectories(primeDoc), - parseWorkspaces(primeDoc), - parseDistributionLists(distroDoc) - ); - }); + public UserOrganisationsResponse getUserOrganization() { + return executeAndRetry("getUserOrganisations", + () -> this.userOrganisationsApi.getUserOrganisations(this.authSession.getSessionToken(), null)); } /** - * Retrieves the current user's custom directories/folders. + * Retrieves the authenticated user's custom directories/folders. * - * @return a list of {@link Directory} instances. + * @return a list of {@link OrganisationStructure} directories. */ - public List getUserDirectories() { - return executeAndRetry("getUserDirectories", () -> { - JsonNode doc = fetchDocument("leftnav"); - if (doc.isMissingNode()) { - doc = fetchDocument(""); - } - return parseDirectories(doc); - }); + public List getUserDirectories() { + return getUserOrganization().getDirectories(); } /** - * Retrieves the current user's workspaces. + * Retrieves the authenticated user's workspaces. * - * @return a list of {@link Workspace} instances. + * @return a list of {@link OrganisationStructure} workspaces. */ - public List getUserWorkspaces() { - return executeAndRetry("getUserWorkspaces", () -> parseWorkspaces(fetchDocument(""))); + public List getUserWorkspaces() { + return getUserOrganization().getWorkspaces(); } /** - * Retrieves the current user's blast distribution lists. + * Retrieves the authenticated user's blast distribution lists. * - * @return a list of {@link DistributionList} instances. - */ - public List getUserDistributionLists() { - return executeAndRetry("getUserDistributionLists", () -> parseDistributionLists(fetchDocument(DISTRO_LISTS_PATH))); - } - - /** - * Parses custom folders/directories from leftnav JSON. + * @return a list of {@link OrganisationStructure} blast lists. */ - public static List parseDirectories(JsonNode doc) { - List directories = new ArrayList<>(); - JsonNode leftnav = doc.has("leftnav") ? doc.get("leftnav") : doc; - for (JsonNode item : leftnav.path("groups").path("conversations")) { - if (item.isObject()) { - try { - FolderDto folder = MAPPER.treeToValue(item, FolderDto.class); - directories.add(new Directory(folder.id(), folder.name(), folder.items() != null ? folder.items() : Collections.emptyList())); - } catch (JacksonException e) { - log.warn("Failed to parse folder: {}", item, e); - } - } - } - return directories; - } - - /** - * Parses workspaces from canvasState and dynamicLayouts (or customWorkspaces fallback). - */ - public static List parseWorkspaces(JsonNode doc) { - List workspaces = new ArrayList<>(); - JsonNode savedTabs = doc.path("canvasState").path("savedTabs"); - JsonNode dynamicLayouts = doc.path("dynamicLayouts"); - - for (JsonNode tab : savedTabs) { - if (tab.path("isWorkspace").asBoolean(false) || tab.has("layoutId")) { - String layoutId = tab.path("layoutId").asText(""); - List streams = dynamicLayouts.path(layoutId).path("dynamicModules").findValues("props") - .stream() - .map(p -> p.path("id").asText(null)) - .filter(Objects::nonNull) - .distinct() - .toList(); - workspaces.add(new Workspace(tab.path("id").asText(null), tab.path("title").asText(null), streams)); - } - } - - if (workspaces.isEmpty()) { - for (JsonNode ws : doc.path("customWorkspaces")) { - if (ws.isObject()) { - List items = ws.path("items").values().stream().map(JsonNode::asText).toList(); - workspaces.add(new Workspace(ws.path("id").asText(null), ws.path("name").asText(null), items)); - } - } - } - return workspaces; - } - - /** - * Parses distribution lists from C2/distributionLists response using Jackson DTO binding. - */ - public static List parseDistributionLists(JsonNode doc) { - JsonNode distroNode = doc.path("C2").has("distributionLists") ? doc.path("C2").get("distributionLists") : doc; - try { - if (distroNode.isObject()) { - Map map = MAPPER.treeToValue(distroNode, - new tools.jackson.core.type.TypeReference>() {}); - return map != null ? map.values().stream().map(DistributionListDto::toDomain).toList() : Collections.emptyList(); - } else if (distroNode.isArray()) { - List list = MAPPER.treeToValue(distroNode, - new tools.jackson.core.type.TypeReference>() {}); - return list != null ? list.stream().map(DistributionListDto::toDomain).toList() : Collections.emptyList(); - } - } catch (JacksonException e) { - log.warn("Failed to deserialize distribution lists", e); - } - return Collections.emptyList(); - } - - private JsonNode fetchDocument(String subPath) throws ApiException { - Map headerParams = new HashMap<>(); - if (this.authSession != null && this.authSession.getSessionToken() != null) { - headerParams.put("sessionToken", this.authSession.getSessionToken()); - } - - String fullPath = CLIENT_DATA_ROOT + (subPath.isEmpty() ? "/" : "/" + subPath); - try { - ApiResponse response = this.apiClient.invokeAPI( - fullPath, "GET", Collections.emptyList(), null, headerParams, - Collections.emptyMap(), Collections.emptyMap(), - "application/json", "application/json", new String[0], new TypeReference() {} - ); - if (response != null && response.getData() != null) { - Object data = response.getData(); - JsonNode root = (data instanceof String s) ? MAPPER.readTree(s) : MAPPER.valueToTree(data); - return root.path("document"); - } - } catch (ApiException e) { - if (e.getCode() == 404) { - return MissingNode.getInstance(); - } - throw e; - } catch (JacksonException e) { - log.warn("Failed to parse client-data JSON for path: {}", fullPath, e); - } - return MissingNode.getInstance(); + public List getUserDistributionLists() { + return getUserOrganization().getBlastLists(); } private T executeAndRetry(String name, SupplierWithApiException supplier) { checkAuthSession(this.authSession); return RetryWithRecovery.executeAndRetry(this.retryBuilder, name, this.apiClient.getBasePath(), supplier); } - - /** - * Internal DTO for Jackson folder binding. - */ - @API(status = API.Status.EXPERIMENTAL) - public record FolderDto(@Nullable String id, @Nullable String name, @Nullable List items) {} - - /** - * Internal DTO for Jackson distribution list binding. - */ - @API(status = API.Status.EXPERIMENTAL) - public record DistributionListDto(@Nullable String id, @Nullable String name, @Nullable List recipients) { - public DistributionList toDomain() { - List recs = this.recipients != null ? this.recipients : Collections.emptyList(); - List streams = recs.stream() - .filter(r -> "stream".equalsIgnoreCase(r.getType()) && r.getId() != null) - .map(Recipient::getId) - .toList(); - return new DistributionList(this.id, this.name, streams, recs); - } - } } diff --git a/symphony-bdk-core/src/main/java/com/symphony/bdk/core/service/user/model/UserOrganization.java b/symphony-bdk-core/src/main/java/com/symphony/bdk/core/service/user/model/UserOrganization.java deleted file mode 100644 index 2c0ef3d5f..000000000 --- a/symphony-bdk-core/src/main/java/com/symphony/bdk/core/service/user/model/UserOrganization.java +++ /dev/null @@ -1,121 +0,0 @@ -package com.symphony.bdk.core.service.user.model; - -import lombok.AllArgsConstructor; -import lombok.EqualsAndHashCode; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.ToString; -import org.apiguardian.api.API; -import org.jspecify.annotations.Nullable; - -import java.util.Collections; -import java.util.List; - -/** - * Aggregates a user's personal organization data including directories/folders, multi-chat workspaces, - * and blast distribution lists. - */ -@Getter -@ToString -@EqualsAndHashCode -@NoArgsConstructor(force = true) -@AllArgsConstructor -@API(status = API.Status.EXPERIMENTAL) -public class UserOrganization { - - private final List directories; - private final List workspaces; - private final List distributionLists; - - public List getDirectories() { - return directories != null ? directories : Collections.emptyList(); - } - - public List getWorkspaces() { - return workspaces != null ? workspaces : Collections.emptyList(); - } - - public List getDistributionLists() { - return distributionLists != null ? distributionLists : Collections.emptyList(); - } - - /** - * Represents a custom user directory/folder containing conversation stream IDs. - */ - @Getter - @ToString - @EqualsAndHashCode - @NoArgsConstructor(force = true) - @AllArgsConstructor - @API(status = API.Status.EXPERIMENTAL) - public static class Directory { - private final @Nullable String id; - private final @Nullable String name; - private final List streamIds; - - public List getStreamIds() { - return streamIds != null ? streamIds : Collections.emptyList(); - } - } - - /** - * Represents a user workspace configuration with pinned or grouped conversation stream IDs. - */ - @Getter - @ToString - @EqualsAndHashCode - @NoArgsConstructor(force = true) - @AllArgsConstructor - @API(status = API.Status.EXPERIMENTAL) - public static class Workspace { - private final @Nullable String id; - private final @Nullable String name; - private final List streamIds; - - public List getStreamIds() { - return streamIds != null ? streamIds : Collections.emptyList(); - } - } - - /** - * Represents a blast distribution list containing stream IDs and detailed recipients. - */ - @Getter - @ToString - @EqualsAndHashCode - @NoArgsConstructor(force = true) - @AllArgsConstructor - @API(status = API.Status.EXPERIMENTAL) - public static class DistributionList { - private final @Nullable String id; - private final @Nullable String name; - private final List streamIds; - private final List recipients; - - public DistributionList(@Nullable String id, @Nullable String name, List streamIds) { - this(id, name, streamIds, Collections.emptyList()); - } - - public List getStreamIds() { - return streamIds != null ? streamIds : Collections.emptyList(); - } - - public List getRecipients() { - return recipients != null ? recipients : Collections.emptyList(); - } - } - - /** - * Represents a recipient in a blast distribution list (can be a user or a stream). - */ - @Getter - @ToString - @EqualsAndHashCode - @NoArgsConstructor(force = true) - @AllArgsConstructor - @API(status = API.Status.EXPERIMENTAL) - public static class Recipient { - private final @Nullable String id; - private final @Nullable String type; - } -} diff --git a/symphony-bdk-core/src/main/java/com/symphony/bdk/core/service/user/model/package-info.java b/symphony-bdk-core/src/main/java/com/symphony/bdk/core/service/user/model/package-info.java deleted file mode 100644 index 4538a74d9..000000000 --- a/symphony-bdk-core/src/main/java/com/symphony/bdk/core/service/user/model/package-info.java +++ /dev/null @@ -1,4 +0,0 @@ -@NullMarked -package com.symphony.bdk.core.service.user.model; - -import org.jspecify.annotations.NullMarked; diff --git a/symphony-bdk-core/src/test/java/com/symphony/bdk/core/service/user/UserOrganizationServiceTest.java b/symphony-bdk-core/src/test/java/com/symphony/bdk/core/service/user/UserOrganizationServiceTest.java index 4efbc5e7f..7cfd98fc8 100644 --- a/symphony-bdk-core/src/test/java/com/symphony/bdk/core/service/user/UserOrganizationServiceTest.java +++ b/symphony-bdk-core/src/test/java/com/symphony/bdk/core/service/user/UserOrganizationServiceTest.java @@ -9,11 +9,9 @@ import com.symphony.bdk.core.auth.AuthSession; import com.symphony.bdk.core.retry.RetryWithRecoveryBuilder; -import com.symphony.bdk.core.service.user.model.UserOrganization; -import com.symphony.bdk.core.service.user.model.UserOrganization.Directory; -import com.symphony.bdk.core.service.user.model.UserOrganization.DistributionList; -import com.symphony.bdk.core.service.user.model.UserOrganization.Workspace; import com.symphony.bdk.core.test.MockApiClient; +import com.symphony.bdk.gen.api.model.OrganisationStructure; +import com.symphony.bdk.gen.api.model.UserOrganisationsResponse; import com.symphony.bdk.http.api.ApiClient; import org.junit.jupiter.api.BeforeEach; @@ -24,10 +22,19 @@ class UserOrganizationServiceTest { private static final String SESSION_TOKEN = "test-session-token"; - private static final String ROOT_PATH = "/webcontroller/clientdata/GET/symphonyPrime/"; - private static final String LEFTNAV_PATH = "/webcontroller/clientdata/GET/symphonyPrime/leftnav"; - private static final String WORKSPACES_PATH = "/webcontroller/clientdata/GET/symphonyPrime/canvasState"; - private static final String DISTRO_LISTS_PATH = "/webcontroller/clientdata/GET/symphonyPrime/C2/distributionLists"; + private static final String ORGANISATIONS_PATH = "/v5/users/organisations"; + + private static final String ORGANISATIONS_JSON = "{\n" + + " \"directories\": [\n" + + " {\"id\": \"folder-27001\", \"name\": \"Other Teams\", \"streamIds\": [\"stream-a\", \"stream-b\"]}\n" + + " ],\n" + + " \"workspaces\": [\n" + + " {\"id\": \"tab-1001\", \"name\": \"SRE Workspace\", \"streamIds\": [\"sre-stream-1\", \"sre-stream-2\"]}\n" + + " ],\n" + + " \"blastLists\": [\n" + + " {\"id\": \"a97be35a\", \"name\": \"Blast People\", \"streamIds\": [\"blast-stream-1\"]}\n" + + " ]\n" + + "}"; private MockApiClient mockApiClient; private AuthSession authSession; @@ -44,190 +51,71 @@ void setUp() { } @Test - void testGetUserOrganizationSuccessWithRootSnapshot() { - String rootJson = "{\n" - + " \"status\": \"OK\",\n" - + " \"document\": {\n" - + " \"leftnav\": {\n" - + " \"groups\": {\n" - + " \"conversations\": [\n" - + " \"raw-stream-1\",\n" - + " {\n" - + " \"id\": \"folder-27001\",\n" - + " \"name\": \"Other Teams\",\n" - + " \"items\": [\"stream-a\", \"stream-b\"]\n" - + " }\n" - + " ]\n" - + " }\n" - + " },\n" - + " \"canvasState\": {\n" - + " \"savedTabs\": [\n" - + " {\n" - + " \"id\": \"tab-1001\",\n" - + " \"title\": \"SRE Workspace\",\n" - + " \"isWorkspace\": true,\n" - + " \"layoutId\": \"layout-sre\"\n" - + " }\n" - + " ]\n" - + " },\n" - + " \"dynamicLayouts\": {\n" - + " \"layout-sre\": {\n" - + " \"dynamicModules\": [\n" - + " [\n" - + " {\n" - + " \"type\": \"chat\",\n" - + " \"props\": {\"id\": \"sre-stream-1\", \"item\": {\"name\": \"Pierre\"}}\n" - + " },\n" - + " {\n" - + " \"type\": \"chat\",\n" - + " \"props\": {\"id\": \"sre-stream-2\", \"item\": {\"name\": \"Cedric\"}}\n" - + " }\n" - + " ]\n" - + " ]\n" - + " }\n" - + " }\n" - + " }\n" - + "}"; + void testGetUserOrganizationSuccess() { + this.mockApiClient.onGet(ORGANISATIONS_PATH, ORGANISATIONS_JSON); - String distroJson = "{\n" - + " \"status\": \"OK\",\n" - + " \"document\": {\n" - + " \"a97be35a-a9a8-48c0-9863-3e697f76c9d5\": {\n" - + " \"id\": \"a97be35a-a9a8-48c0-9863-3e697f76c9d5\",\n" - + " \"name\": \"Blast People\",\n" - + " \"recipients\": [\n" - + " {\"id\": \"699220675788807\", \"type\": \"user\"},\n" - + " {\"id\": \"bPCpxk9mbzZnFvrGAYY6T3///mDhvvDwdA==\", \"type\": \"stream\"}\n" - + " ]\n" - + " }\n" - + " }\n" - + "}"; - - this.mockApiClient.onGet(ROOT_PATH, rootJson); - this.mockApiClient.onGet(DISTRO_LISTS_PATH, distroJson); - - UserOrganization org = this.service.getUserOrganization(); + UserOrganisationsResponse org = this.service.getUserOrganization(); assertNotNull(org); - // Assert directories assertEquals(1, org.getDirectories().size()); - Directory dir = org.getDirectories().get(0); + OrganisationStructure dir = org.getDirectories().get(0); assertEquals("folder-27001", dir.getId()); assertEquals("Other Teams", dir.getName()); assertEquals(List.of("stream-a", "stream-b"), dir.getStreamIds()); - // Assert workspaces assertEquals(1, org.getWorkspaces().size()); - Workspace ws = org.getWorkspaces().get(0); + OrganisationStructure ws = org.getWorkspaces().get(0); assertEquals("tab-1001", ws.getId()); assertEquals("SRE Workspace", ws.getName()); assertEquals(List.of("sre-stream-1", "sre-stream-2"), ws.getStreamIds()); - // Assert distribution lists - assertEquals(1, org.getDistributionLists().size()); - DistributionList dl = org.getDistributionLists().get(0); - assertEquals("a97be35a-a9a8-48c0-9863-3e697f76c9d5", dl.getId()); - assertEquals("Blast People", dl.getName()); - assertEquals(List.of("bPCpxk9mbzZnFvrGAYY6T3///mDhvvDwdA=="), dl.getStreamIds()); - assertEquals(2, dl.getRecipients().size()); - assertEquals("699220675788807", dl.getRecipients().get(0).getId()); - assertEquals("user", dl.getRecipients().get(0).getType()); - assertEquals("bPCpxk9mbzZnFvrGAYY6T3///mDhvvDwdA==", dl.getRecipients().get(1).getId()); - assertEquals("stream", dl.getRecipients().get(1).getType()); + assertEquals(1, org.getBlastLists().size()); + OrganisationStructure bl = org.getBlastLists().get(0); + assertEquals("a97be35a", bl.getId()); + assertEquals("Blast People", bl.getName()); + assertEquals(List.of("blast-stream-1"), bl.getStreamIds()); } @Test - void testGetUserDirectoriesSubpath() { - String leftNavJson = "{\n" - + " \"status\": \"OK\",\n" - + " \"document\": {\n" - + " \"groups\": {\n" - + " \"conversations\": [\n" - + " \"chat-pinned-1\",\n" - + " {\n" - + " \"id\": \"folder-1\",\n" - + " \"name\": \"My Folder\",\n" - + " \"items\": [\"s1\", \"s2\"]\n" - + " }\n" - + " ]\n" - + " }\n" - + " }\n" - + "}"; - - this.mockApiClient.onGet(LEFTNAV_PATH, leftNavJson); + void testGetUserDirectories() { + this.mockApiClient.onGet(ORGANISATIONS_PATH, ORGANISATIONS_JSON); - List directories = this.service.getUserDirectories(); + List directories = this.service.getUserDirectories(); assertEquals(1, directories.size()); - assertEquals("folder-1", directories.get(0).getId()); - assertEquals("My Folder", directories.get(0).getName()); - assertEquals(List.of("s1", "s2"), directories.get(0).getStreamIds()); + assertEquals("folder-27001", directories.get(0).getId()); + assertEquals(List.of("stream-a", "stream-b"), directories.get(0).getStreamIds()); } @Test - void testGetUserDistributionLists() { - String distroJson = "{\n" - + " \"status\": \"OK\",\n" - + " \"document\": {\n" - + " \"list-1\": {\n" - + " \"id\": \"list-1\",\n" - + " \"name\": \"VIP List\",\n" - + " \"recipients\": [\n" - + " {\"id\": \"user-1\", \"type\": \"user\"},\n" - + " {\"id\": \"stream-1\", \"type\": \"stream\"}\n" - + " ]\n" - + " }\n" - + " }\n" - + "}"; - - this.mockApiClient.onGet(DISTRO_LISTS_PATH, distroJson); + void testGetUserWorkspaces() { + this.mockApiClient.onGet(ORGANISATIONS_PATH, ORGANISATIONS_JSON); - List lists = this.service.getUserDistributionLists(); - assertEquals(1, lists.size()); - assertEquals("list-1", lists.get(0).getId()); - assertEquals("VIP List", lists.get(0).getName()); - assertEquals(List.of("stream-1"), lists.get(0).getStreamIds()); + List workspaces = this.service.getUserWorkspaces(); + assertEquals(1, workspaces.size()); + assertEquals("tab-1001", workspaces.get(0).getId()); + assertEquals("SRE Workspace", workspaces.get(0).getName()); } @Test - void testGetWorkspacesFallbackToCustomWorkspaces() { - String fallbackJson = "{\n" - + " \"status\": \"OK\",\n" - + " \"document\": {\n" - + " \"customWorkspaces\": [\n" - + " {\n" - + " \"id\": \"ws-1\",\n" - + " \"name\": \"Legacy Workspace\",\n" - + " \"items\": [\"s-100\", \"s-200\"]\n" - + " }\n" - + " ]\n" - + " }\n" - + "}"; - - this.mockApiClient.onGet(ROOT_PATH, fallbackJson); + void testGetUserDistributionLists() { + this.mockApiClient.onGet(ORGANISATIONS_PATH, ORGANISATIONS_JSON); - List workspaces = this.service.getUserWorkspaces(); - assertEquals(1, workspaces.size()); - assertEquals("ws-1", workspaces.get(0).getId()); - assertEquals("Legacy Workspace", workspaces.get(0).getName()); - assertEquals(List.of("s-100", "s-200"), workspaces.get(0).getStreamIds()); + List blastLists = this.service.getUserDistributionLists(); + assertEquals(1, blastLists.size()); + assertEquals("a97be35a", blastLists.get(0).getId()); + assertEquals("Blast People", blastLists.get(0).getName()); + assertEquals(List.of("blast-stream-1"), blastLists.get(0).getStreamIds()); } @Test - void test404ReturnsEmptyGracefully() { - this.mockApiClient.onGet(404, ROOT_PATH, "{}"); - this.mockApiClient.onGet(404, LEFTNAV_PATH, "{}"); - this.mockApiClient.onGet(404, WORKSPACES_PATH, "{}"); - this.mockApiClient.onGet(404, DISTRO_LISTS_PATH, "{}"); + void testEmptyResponseReturnsEmptyLists() { + this.mockApiClient.onGet(ORGANISATIONS_PATH, "{}"); - UserOrganization org = this.service.getUserOrganization(); + UserOrganisationsResponse org = this.service.getUserOrganization(); assertNotNull(org); assertTrue(org.getDirectories().isEmpty()); assertTrue(org.getWorkspaces().isEmpty()); - assertTrue(org.getDistributionLists().isEmpty()); - - assertTrue(this.service.getUserDirectories().isEmpty()); - assertTrue(this.service.getUserWorkspaces().isEmpty()); - assertTrue(this.service.getUserDistributionLists().isEmpty()); + assertTrue(org.getBlastLists().isEmpty()); } @Test @@ -238,119 +126,18 @@ void testOboModeExecution() { UserOrganizationService oboService = this.service.obo(oboSession); assertNotNull(oboService); - String distroJson = "{\n" - + " \"status\": \"OK\",\n" - + " \"document\": {\n" - + " \"list-obo\": {\n" - + " \"id\": \"list-obo\",\n" - + " \"name\": \"OBO List\",\n" - + " \"recipients\": [{\"id\": \"stream-obo\", \"type\": \"stream\"}]\n" - + " }\n" - + " }\n" - + "}"; - - this.mockApiClient.onGet(DISTRO_LISTS_PATH, distroJson); - List lists = oboService.getUserDistributionLists(); - assertEquals(1, lists.size()); - assertEquals("list-obo", lists.get(0).getId()); + this.mockApiClient.onGet(ORGANISATIONS_PATH, ORGANISATIONS_JSON); + List blastLists = oboService.getUserDistributionLists(); + assertEquals(1, blastLists.size()); + assertEquals("a97be35a", blastLists.get(0).getId()); } @Test void testMissingAuthSessionThrowsIllegalStateException() { ApiClient usersClient = this.mockApiClient.getApiClient(""); - UserOrganizationService unauthenticatedService = new UserOrganizationService(usersClient, new RetryWithRecoveryBuilder<>()); + UserOrganizationService unauthenticatedService = + new UserOrganizationService(usersClient, new RetryWithRecoveryBuilder<>()); assertThrows(IllegalStateException.class, unauthenticatedService::getUserOrganization); } - - @Test - void testParseDistributionListsWithUserProvidedSample() { - String sample = "{\n" - + " \"status\": \"OK\",\n" - + " \"document\": {\n" - + " \"a97be35a-a9a8-48c0-9863-3e697f76c9d5\": {\n" - + " \"id\": \"a97be35a-a9a8-48c0-9863-3e697f76c9d5\",\n" - + " \"name\": \"Blast People\",\n" - + " \"recipients\": [\n" - + " {\n" - + " \"id\": \"699220675788807\",\n" - + " \"type\": \"user\"\n" - + " },\n" - + " {\n" - + " \"id\": \"699220675788806\",\n" - + " \"type\": \"user\"\n" - + " },\n" - + " {\n" - + " \"id\": \"bPCpxk9mbzZnFvrGAYY6T3///mDhvvDwdA==\",\n" - + " \"type\": \"stream\"\n" - + " }\n" - + " ]\n" - + " }\n" - + " }\n" - + "}"; - - this.mockApiClient.onGet(DISTRO_LISTS_PATH, sample); - - List result = this.service.getUserDistributionLists(); - assertEquals(1, result.size()); - DistributionList dl = result.get(0); - assertEquals("a97be35a-a9a8-48c0-9863-3e697f76c9d5", dl.getId()); - assertEquals("Blast People", dl.getName()); - assertEquals(1, dl.getStreamIds().size()); - assertEquals("bPCpxk9mbzZnFvrGAYY6T3///mDhvvDwdA==", dl.getStreamIds().get(0)); - assertEquals(3, dl.getRecipients().size()); - assertEquals("699220675788807", dl.getRecipients().get(0).getId()); - assertEquals("user", dl.getRecipients().get(0).getType()); - assertEquals("699220675788806", dl.getRecipients().get(1).getId()); - assertEquals("user", dl.getRecipients().get(1).getType()); - assertEquals("bPCpxk9mbzZnFvrGAYY6T3///mDhvvDwdA==", dl.getRecipients().get(2).getId()); - assertEquals("stream", dl.getRecipients().get(2).getType()); - } - - @Test - void testParseDirectoriesWithPrimeJsonSnippet() { - String primeSnippet = "{\n" - + " \"status\": \"OK\",\n" - + " \"document\": {\n" - + " \"leftnav\": {\n" - + " \"groups\": {\n" - + " \"conversations\": [\n" - + " \"mQGBc6YxSXe4VymtQpVdA3///qZraOq2dA==\",\n" - + " {\n" - + " \"id\": \"folder-27001\",\n" - + " \"name\": \"Other Teams\",\n" - + " \"items\": [\n" - + " \"hBYiyjJxRpyRgy5y3nBQgX///rNLnbBjdA==\",\n" - + " \"Xak486Jkcco5WOQkHlgZBn///rMC6YWkdA==\",\n" - + " \"/UwvEK2mMofbtrhFw5NY83///p3g/tb2dA==\"\n" - + " ]\n" - + " },\n" - + " {\n" - + " \"id\": \"folder-2008\",\n" - + " \"name\": \"RANDOM\",\n" - + " \"items\": []\n" - + " },\n" - + " \"/Mc+INWneo2r0A3efYhAKn///pYciXMrdA==\"\n" - + " ]\n" - + " }\n" - + " }\n" - + " }\n" - + "}"; - - this.mockApiClient.onGet(LEFTNAV_PATH, primeSnippet); - - List directories = this.service.getUserDirectories(); - assertEquals(2, directories.size()); - - Directory d1 = directories.get(0); - assertEquals("folder-27001", d1.getId()); - assertEquals("Other Teams", d1.getName()); - assertEquals(3, d1.getStreamIds().size()); - assertTrue(d1.getStreamIds().contains("hBYiyjJxRpyRgy5y3nBQgX///rNLnbBjdA==")); - - Directory d2 = directories.get(1); - assertEquals("folder-2008", d2.getId()); - assertEquals("RANDOM", d2.getName()); - assertTrue(d2.getStreamIds().isEmpty()); - } } From 4d52a7610386302147cfd47addb4e2a75af31b15 Mon Sep 17 00:00:00 2001 From: jameshanna-symphony Date: Fri, 2 Oct 2026 09:56:46 +0100 Subject: [PATCH 04/17] Update build.gradle --- symphony-bdk-core/build.gradle | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/symphony-bdk-core/build.gradle b/symphony-bdk-core/build.gradle index 139ce401d..a1cbd501a 100644 --- a/symphony-bdk-core/build.gradle +++ b/symphony-bdk-core/build.gradle @@ -81,6 +81,12 @@ dependencies { // OpenAPI code generation def apiBaseUrl = "https://raw.githubusercontent.com/finos/symphony-api-spec/a8efe8a6193769d0657fc57758918138aa4483bb" +// TEMP (BE-9865): the /v5/users/organisations endpoint is not yet in the pinned FINOS commit. +// Source the Users spec from the PR branch until the FINOS PR merges, then DELETE this override +// and bump apiBaseUrl above to the merged commit SHA. +def apiBaseUrlOverrides = [ + Users: "https://raw.githubusercontent.com/jameshanna-symphony/symphony-api-spec/BE-9865_Add_User_Organisation_Endpoint" +] def generatedFolder = "${layout.buildDirectory.get()}/generated/openapi" def apisToGenerate = [ Agent: 'agent/agent-api-public-deprecated.yaml', @@ -94,7 +100,7 @@ sourceSets.main.java.srcDirs += "$generatedFolder/src/main/java" apisToGenerate.each { api, path -> def downloadTask = tasks.register("download$api", Download) { - src "$apiBaseUrl/$path" + src "${apiBaseUrlOverrides.getOrDefault(api, apiBaseUrl)}/$path" dest "${layout.buildDirectory.get()}/openapi/$api/${path.split("/")[1]}" overwrite false useETag "all" From 091a595b643fdd80e84e92c383542a36a26e47bc Mon Sep 17 00:00:00 2001 From: jameshanna-symphony Date: Fri, 2 Oct 2026 10:07:03 +0100 Subject: [PATCH 05/17] Update UserOrganizationServiceTest.java --- .../bdk/core/service/user/UserOrganizationServiceTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/symphony-bdk-core/src/test/java/com/symphony/bdk/core/service/user/UserOrganizationServiceTest.java b/symphony-bdk-core/src/test/java/com/symphony/bdk/core/service/user/UserOrganizationServiceTest.java index 7cfd98fc8..deaa3c3af 100644 --- a/symphony-bdk-core/src/test/java/com/symphony/bdk/core/service/user/UserOrganizationServiceTest.java +++ b/symphony-bdk-core/src/test/java/com/symphony/bdk/core/service/user/UserOrganizationServiceTest.java @@ -22,7 +22,7 @@ class UserOrganizationServiceTest { private static final String SESSION_TOKEN = "test-session-token"; - private static final String ORGANISATIONS_PATH = "/v5/users/organisations"; + private static final String ORGANISATIONS_PATH = "/v5/users/current/organisations"; private static final String ORGANISATIONS_JSON = "{\n" + " \"directories\": [\n" From ac2ad9e41758f0281a6691dd6195f9a0e90abcb7 Mon Sep 17 00:00:00 2001 From: jameshanna-symphony Date: Mon, 5 Oct 2026 16:42:47 +0100 Subject: [PATCH 06/17] Update Object return --- .../service/user/UserOrganizationService.java | 7 ++++--- .../user/UserOrganizationServiceTest.java | 20 +++++++++++++------ 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/symphony-bdk-core/src/main/java/com/symphony/bdk/core/service/user/UserOrganizationService.java b/symphony-bdk-core/src/main/java/com/symphony/bdk/core/service/user/UserOrganizationService.java index bed8b77a3..03f6ee088 100644 --- a/symphony-bdk-core/src/main/java/com/symphony/bdk/core/service/user/UserOrganizationService.java +++ b/symphony-bdk-core/src/main/java/com/symphony/bdk/core/service/user/UserOrganizationService.java @@ -6,6 +6,7 @@ import com.symphony.bdk.core.retry.function.SupplierWithApiException; import com.symphony.bdk.core.service.OboService; import com.symphony.bdk.gen.api.UserOrganisationsApi; +import com.symphony.bdk.gen.api.model.BlastList; import com.symphony.bdk.gen.api.model.OrganisationStructure; import com.symphony.bdk.gen.api.model.UserOrganisationsResponse; import com.symphony.bdk.http.api.ApiClient; @@ -80,11 +81,11 @@ public List getUserWorkspaces() { } /** - * Retrieves the authenticated user's blast distribution lists. + * Retrieves the authenticated user's blast distribution lists, each with its recipients. * - * @return a list of {@link OrganisationStructure} blast lists. + * @return a list of {@link BlastList} blast lists. */ - public List getUserDistributionLists() { + public List getUserDistributionLists() { return getUserOrganization().getBlastLists(); } diff --git a/symphony-bdk-core/src/test/java/com/symphony/bdk/core/service/user/UserOrganizationServiceTest.java b/symphony-bdk-core/src/test/java/com/symphony/bdk/core/service/user/UserOrganizationServiceTest.java index deaa3c3af..1125c78d3 100644 --- a/symphony-bdk-core/src/test/java/com/symphony/bdk/core/service/user/UserOrganizationServiceTest.java +++ b/symphony-bdk-core/src/test/java/com/symphony/bdk/core/service/user/UserOrganizationServiceTest.java @@ -10,6 +10,7 @@ import com.symphony.bdk.core.auth.AuthSession; import com.symphony.bdk.core.retry.RetryWithRecoveryBuilder; import com.symphony.bdk.core.test.MockApiClient; +import com.symphony.bdk.gen.api.model.BlastList; import com.symphony.bdk.gen.api.model.OrganisationStructure; import com.symphony.bdk.gen.api.model.UserOrganisationsResponse; import com.symphony.bdk.http.api.ApiClient; @@ -32,7 +33,9 @@ class UserOrganizationServiceTest { + " {\"id\": \"tab-1001\", \"name\": \"SRE Workspace\", \"streamIds\": [\"sre-stream-1\", \"sre-stream-2\"]}\n" + " ],\n" + " \"blastLists\": [\n" - + " {\"id\": \"a97be35a\", \"name\": \"Blast People\", \"streamIds\": [\"blast-stream-1\"]}\n" + + " {\"id\": \"a97be35a\", \"name\": \"Blast People\", \"recipients\": [" + + " {\"id\": \"user-1\", \"type\": \"user\"}," + + " {\"id\": \"blast-stream-1\", \"type\": \"stream\"}]}\n" + " ]\n" + "}"; @@ -70,10 +73,14 @@ void testGetUserOrganizationSuccess() { assertEquals(List.of("sre-stream-1", "sre-stream-2"), ws.getStreamIds()); assertEquals(1, org.getBlastLists().size()); - OrganisationStructure bl = org.getBlastLists().get(0); + BlastList bl = org.getBlastLists().get(0); assertEquals("a97be35a", bl.getId()); assertEquals("Blast People", bl.getName()); - assertEquals(List.of("blast-stream-1"), bl.getStreamIds()); + assertEquals(2, bl.getRecipients().size()); + assertEquals("user-1", bl.getRecipients().get(0).getId()); + assertEquals("user", bl.getRecipients().get(0).getType()); + assertEquals("blast-stream-1", bl.getRecipients().get(1).getId()); + assertEquals("stream", bl.getRecipients().get(1).getType()); } @Test @@ -100,11 +107,12 @@ void testGetUserWorkspaces() { void testGetUserDistributionLists() { this.mockApiClient.onGet(ORGANISATIONS_PATH, ORGANISATIONS_JSON); - List blastLists = this.service.getUserDistributionLists(); + List blastLists = this.service.getUserDistributionLists(); assertEquals(1, blastLists.size()); assertEquals("a97be35a", blastLists.get(0).getId()); assertEquals("Blast People", blastLists.get(0).getName()); - assertEquals(List.of("blast-stream-1"), blastLists.get(0).getStreamIds()); + assertEquals(2, blastLists.get(0).getRecipients().size()); + assertEquals("stream", blastLists.get(0).getRecipients().get(1).getType()); } @Test @@ -127,7 +135,7 @@ void testOboModeExecution() { assertNotNull(oboService); this.mockApiClient.onGet(ORGANISATIONS_PATH, ORGANISATIONS_JSON); - List blastLists = oboService.getUserDistributionLists(); + List blastLists = oboService.getUserDistributionLists(); assertEquals(1, blastLists.size()); assertEquals("a97be35a", blastLists.get(0).getId()); } From 6edc5c6daff37be44941e9a78cef50f587a5c6c6 Mon Sep 17 00:00:00 2001 From: jameshanna-symphony Date: Thu, 8 Oct 2026 14:24:44 +0100 Subject: [PATCH 07/17] rename to clientdata --- .../com/symphony/bdk/core/OboServices.java | 22 +++---- .../com/symphony/bdk/core/ServiceFactory.java | 10 ++-- .../com/symphony/bdk/core/SymphonyBdk.java | 22 +++---- .../ClientDataService.java} | 47 +++++++-------- .../clientdata/OboClientDataService.java | 60 +++++++++++++++++++ .../core/service/clientdata/package-info.java | 4 ++ .../symphony/bdk/core/OboServicesTest.java | 7 +++ .../symphony/bdk/core/ServiceFactoryTest.java | 8 +-- .../ClientDataServiceTest.java} | 31 ++++++---- .../spring/config/BdkOboServiceConfig.java | 6 +- .../bdk/spring/config/BdkServiceConfig.java | 6 +- 11 files changed, 149 insertions(+), 74 deletions(-) rename symphony-bdk-core/src/main/java/com/symphony/bdk/core/service/{user/UserOrganizationService.java => clientdata/ClientDataService.java} (58%) create mode 100644 symphony-bdk-core/src/main/java/com/symphony/bdk/core/service/clientdata/OboClientDataService.java create mode 100644 symphony-bdk-core/src/main/java/com/symphony/bdk/core/service/clientdata/package-info.java rename symphony-bdk-core/src/test/java/com/symphony/bdk/core/service/{user/UserOrganizationServiceTest.java => clientdata/ClientDataServiceTest.java} (86%) diff --git a/symphony-bdk-core/src/main/java/com/symphony/bdk/core/OboServices.java b/symphony-bdk-core/src/main/java/com/symphony/bdk/core/OboServices.java index 8eb117503..7c80d4ce4 100644 --- a/symphony-bdk-core/src/main/java/com/symphony/bdk/core/OboServices.java +++ b/symphony-bdk-core/src/main/java/com/symphony/bdk/core/OboServices.java @@ -11,8 +11,8 @@ import com.symphony.bdk.core.service.session.OboSessionService; import com.symphony.bdk.core.service.signal.OboSignalService; import com.symphony.bdk.core.service.stream.OboStreamService; +import com.symphony.bdk.core.service.clientdata.OboClientDataService; import com.symphony.bdk.core.service.user.OboUserService; -import com.symphony.bdk.core.service.user.UserOrganizationService; import org.apiguardian.api.API; @@ -26,7 +26,7 @@ public class OboServices { private final OboStreamService oboStreamService; private final OboUserService oboUserService; - private final UserOrganizationService oboUserOrganizationService; + private final OboClientDataService oboClientDataService; private final OboMessageService oboMessageService; private final OboPresenceService oboPresenceService; private final OboConnectionService oboConnectionService; @@ -48,7 +48,7 @@ public OboServices( oboStreamService = serviceFactory.getStreamService(); oboUserService = serviceFactory.getUserService(); - oboUserOrganizationService = serviceFactory.getUserOrganizationService(); + oboClientDataService = serviceFactory.getClientDataService(); oboMessageService = serviceFactory.getMessageService(); oboPresenceService = serviceFactory.getPresenceService(); oboConnectionService = serviceFactory.getConnectionService(); @@ -75,21 +75,21 @@ public OboUserService users() { } /** - * Get the {@link UserOrganizationService} using the provided OBO session in constructor. + * Get the {@link OboClientDataService} using the provided OBO session in constructor. * - * @return a {@link UserOrganizationService} instance with the provided OBO session. + * @return an {@link OboClientDataService} instance with the provided OBO session. */ - public UserOrganizationService userOrganization() { - return oboUserOrganizationService; + public OboClientDataService clientData() { + return oboClientDataService; } /** - * Get the {@link UserOrganizationService} using the provided OBO session in constructor. + * Get the {@link OboClientDataService} using the provided OBO session in constructor. * - * @return a {@link UserOrganizationService} instance with the provided OBO session. + * @return an {@link OboClientDataService} instance with the provided OBO session. */ - public UserOrganizationService getUserOrganizationService() { - return oboUserOrganizationService; + public OboClientDataService getClientDataService() { + return oboClientDataService; } /** diff --git a/symphony-bdk-core/src/main/java/com/symphony/bdk/core/ServiceFactory.java b/symphony-bdk-core/src/main/java/com/symphony/bdk/core/ServiceFactory.java index 9e5ac43b4..c4ad229eb 100644 --- a/symphony-bdk-core/src/main/java/com/symphony/bdk/core/ServiceFactory.java +++ b/symphony-bdk-core/src/main/java/com/symphony/bdk/core/ServiceFactory.java @@ -26,7 +26,7 @@ import com.symphony.bdk.core.service.session.SessionService; import com.symphony.bdk.core.service.signal.SignalService; import com.symphony.bdk.core.service.stream.StreamService; -import com.symphony.bdk.core.service.user.UserOrganizationService; +import com.symphony.bdk.core.service.clientdata.ClientDataService; import com.symphony.bdk.core.service.user.UserService; import com.symphony.bdk.gen.api.AppEntitlementApi; import com.symphony.bdk.gen.api.ApplicationApi; @@ -151,12 +151,12 @@ public UserService getUserService() { } /** - * Returns a fully initialized {@link UserOrganizationService}. + * Returns a fully initialized {@link ClientDataService}. * - * @return a new {@link UserOrganizationService} instance. + * @return a new {@link ClientDataService} instance. */ - public UserOrganizationService getUserOrganizationService() { - return new UserOrganizationService(this.usersClient, this.authSession, this.retryBuilder); + public ClientDataService getClientDataService() { + return new ClientDataService(this.usersClient, this.authSession, this.retryBuilder); } /** diff --git a/symphony-bdk-core/src/main/java/com/symphony/bdk/core/SymphonyBdk.java b/symphony-bdk-core/src/main/java/com/symphony/bdk/core/SymphonyBdk.java index 7fe0fe575..0bcbd6f96 100644 --- a/symphony-bdk-core/src/main/java/com/symphony/bdk/core/SymphonyBdk.java +++ b/symphony-bdk-core/src/main/java/com/symphony/bdk/core/SymphonyBdk.java @@ -24,7 +24,7 @@ import com.symphony.bdk.core.service.session.SessionService; import com.symphony.bdk.core.service.signal.SignalService; import com.symphony.bdk.core.service.stream.StreamService; -import com.symphony.bdk.core.service.user.UserOrganizationService; +import com.symphony.bdk.core.service.clientdata.ClientDataService; import com.symphony.bdk.core.service.user.UserService; import com.symphony.bdk.core.util.ServiceLookup; import com.symphony.bdk.extension.BdkExtension; @@ -61,7 +61,7 @@ public class SymphonyBdk { private final ActivityRegistry activityRegistry; private final StreamService streamService; private final UserService userService; - private final UserOrganizationService userOrganizationService; + private final ClientDataService clientDataService; private final MessageService messageService; private final PresenceService presenceService; private final ConnectionService connectionService; @@ -164,7 +164,7 @@ protected SymphonyBdk( this.sessionService = serviceFactory != null ? serviceFactory.getSessionService() : null; this.userService = serviceFactory != null ? serviceFactory.getUserService() : null; - this.userOrganizationService = serviceFactory != null ? serviceFactory.getUserOrganizationService() : null; + this.clientDataService = serviceFactory != null ? serviceFactory.getClientDataService() : null; this.streamService = serviceFactory != null ? serviceFactory.getStreamService() : null; this.presenceService = serviceFactory != null ? serviceFactory.getPresenceService() : null; this.connectionService = serviceFactory != null ? serviceFactory.getConnectionService() : null; @@ -247,21 +247,21 @@ public UserService users() { } /** - * Get the {@link UserOrganizationService} from a Bdk entry point. + * Get the {@link ClientDataService} from a Bdk entry point. * - * @return {@link UserOrganizationService} instance. + * @return {@link ClientDataService} instance. */ - public UserOrganizationService userOrganization() { - return getOrThrowNoBotConfig(this.userOrganizationService); + public ClientDataService clientData() { + return getOrThrowNoBotConfig(this.clientDataService); } /** - * Get the {@link UserOrganizationService} from a Bdk entry point. + * Get the {@link ClientDataService} from a Bdk entry point. * - * @return {@link UserOrganizationService} instance. + * @return {@link ClientDataService} instance. */ - public UserOrganizationService getUserOrganizationService() { - return this.userOrganizationService; + public ClientDataService getClientDataService() { + return this.clientDataService; } /** diff --git a/symphony-bdk-core/src/main/java/com/symphony/bdk/core/service/user/UserOrganizationService.java b/symphony-bdk-core/src/main/java/com/symphony/bdk/core/service/clientdata/ClientDataService.java similarity index 58% rename from symphony-bdk-core/src/main/java/com/symphony/bdk/core/service/user/UserOrganizationService.java rename to symphony-bdk-core/src/main/java/com/symphony/bdk/core/service/clientdata/ClientDataService.java index 03f6ee088..5c572fdc4 100644 --- a/symphony-bdk-core/src/main/java/com/symphony/bdk/core/service/user/UserOrganizationService.java +++ b/symphony-bdk-core/src/main/java/com/symphony/bdk/core/service/clientdata/ClientDataService.java @@ -1,4 +1,4 @@ -package com.symphony.bdk.core.service.user; +package com.symphony.bdk.core.service.clientdata; import com.symphony.bdk.core.auth.AuthSession; import com.symphony.bdk.core.retry.RetryWithRecovery; @@ -18,19 +18,19 @@ import java.util.List; /** - * Service to retrieve the authenticated user's personal chat organization structures — - * custom directories/folders, multi-chat workspaces, and blast distribution lists — from the + * Service to retrieve the authenticated user's client data organization structures — + * custom folders, multi-chat workspaces, and distribution lists — from the * {@code GET /v5/users/organisations} endpoint. */ -@API(status = API.Status.EXPERIMENTAL) -public class UserOrganizationService implements OboService { +@API(status = API.Status.STABLE) +public class ClientDataService implements OboClientDataService, OboService { private final ApiClient apiClient; private final UserOrganisationsApi userOrganisationsApi; private final @Nullable AuthSession authSession; private final RetryWithRecoveryBuilder retryBuilder; - public UserOrganizationService(ApiClient apiClient, AuthSession authSession, + public ClientDataService(ApiClient apiClient, AuthSession authSession, RetryWithRecoveryBuilder retryBuilder) { this.apiClient = apiClient; this.userOrganisationsApi = new UserOrganisationsApi(apiClient); @@ -39,7 +39,7 @@ public UserOrganizationService(ApiClient apiClient, AuthSession authSession, .recoveryStrategy(ApiException::isUnauthorized, authSession::refresh); } - public UserOrganizationService(ApiClient apiClient, RetryWithRecoveryBuilder retryBuilder) { + public ClientDataService(ApiClient apiClient, RetryWithRecoveryBuilder retryBuilder) { this.apiClient = apiClient; this.userOrganisationsApi = new UserOrganisationsApi(apiClient); this.authSession = null; @@ -47,46 +47,41 @@ public UserOrganizationService(ApiClient apiClient, RetryWithRecoveryBuilder } @Override - public UserOrganizationService obo(AuthSession oboSession) { - return new UserOrganizationService(this.apiClient, oboSession, this.retryBuilder); + public OboClientDataService obo(AuthSession oboSession) { + return new ClientDataService(this.apiClient, oboSession, this.retryBuilder); } /** - * Retrieves the authenticated user's aggregated organization data (directories, workspaces, and - * blast distribution lists) for the default {@code symphonyPrime} application. - * - * @return a {@link UserOrganisationsResponse} containing the user's organization data. + * {@inheritDoc} */ - public UserOrganisationsResponse getUserOrganization() { + @Override + public UserOrganisationsResponse getClientData() { return executeAndRetry("getUserOrganisations", () -> this.userOrganisationsApi.getUserOrganisations(this.authSession.getSessionToken(), null)); } /** - * Retrieves the authenticated user's custom directories/folders. - * - * @return a list of {@link OrganisationStructure} directories. + * {@inheritDoc} */ + @Override public List getUserDirectories() { - return getUserOrganization().getDirectories(); + return getClientData().getDirectories(); } /** - * Retrieves the authenticated user's workspaces. - * - * @return a list of {@link OrganisationStructure} workspaces. + * {@inheritDoc} */ + @Override public List getUserWorkspaces() { - return getUserOrganization().getWorkspaces(); + return getClientData().getWorkspaces(); } /** - * Retrieves the authenticated user's blast distribution lists, each with its recipients. - * - * @return a list of {@link BlastList} blast lists. + * {@inheritDoc} */ + @Override public List getUserDistributionLists() { - return getUserOrganization().getBlastLists(); + return getClientData().getBlastLists(); } private T executeAndRetry(String name, SupplierWithApiException supplier) { diff --git a/symphony-bdk-core/src/main/java/com/symphony/bdk/core/service/clientdata/OboClientDataService.java b/symphony-bdk-core/src/main/java/com/symphony/bdk/core/service/clientdata/OboClientDataService.java new file mode 100644 index 000000000..87e144a59 --- /dev/null +++ b/symphony-bdk-core/src/main/java/com/symphony/bdk/core/service/clientdata/OboClientDataService.java @@ -0,0 +1,60 @@ +package com.symphony.bdk.core.service.clientdata; + +import com.symphony.bdk.gen.api.model.BlastList; +import com.symphony.bdk.gen.api.model.OrganisationStructure; +import com.symphony.bdk.gen.api.model.UserOrganisationsResponse; + +import org.apiguardian.api.API; + +import java.util.List; + +/** + * Service interface exposing OBO-enabled endpoints to manage client data. + */ +@API(status = API.Status.STABLE) +public interface OboClientDataService { + + /** + * Retrieves the authenticated user's aggregated client data (directories, workspaces, and + * blast distribution lists) for the default {@code symphonyPrime} application. + * {@link ClientDataService#getClientData()} + * + * @return a {@link UserOrganisationsResponse} containing the user's client data. + */ + UserOrganisationsResponse getClientData(); + + /** + * Retrieves the authenticated user's aggregated organization data. + * + * @deprecated Use {@link #getClientData()} instead. + * @return a {@link UserOrganisationsResponse} containing the user's organization data. + */ + @Deprecated + default UserOrganisationsResponse getUserOrganization() { + return getClientData(); + } + + /** + * Retrieves the authenticated user's custom directories/folders. + * {@link ClientDataService#getUserDirectories()} + * + * @return a list of {@link OrganisationStructure} directories. + */ + List getUserDirectories(); + + /** + * Retrieves the authenticated user's workspaces. + * {@link ClientDataService#getUserWorkspaces()} + * + * @return a list of {@link OrganisationStructure} workspaces. + */ + List getUserWorkspaces(); + + /** + * Retrieves the authenticated user's blast distribution lists, each with its recipients. + * {@link ClientDataService#getUserDistributionLists()} + * + * @return a list of {@link BlastList} blast lists. + */ + List getUserDistributionLists(); +} diff --git a/symphony-bdk-core/src/main/java/com/symphony/bdk/core/service/clientdata/package-info.java b/symphony-bdk-core/src/main/java/com/symphony/bdk/core/service/clientdata/package-info.java new file mode 100644 index 000000000..31e1fb36f --- /dev/null +++ b/symphony-bdk-core/src/main/java/com/symphony/bdk/core/service/clientdata/package-info.java @@ -0,0 +1,4 @@ +@NullMarked +package com.symphony.bdk.core.service.clientdata; + +import org.jspecify.annotations.NullMarked; diff --git a/symphony-bdk-core/src/test/java/com/symphony/bdk/core/OboServicesTest.java b/symphony-bdk-core/src/test/java/com/symphony/bdk/core/OboServicesTest.java index d1596b8ec..b646b803a 100644 --- a/symphony-bdk-core/src/test/java/com/symphony/bdk/core/OboServicesTest.java +++ b/symphony-bdk-core/src/test/java/com/symphony/bdk/core/OboServicesTest.java @@ -6,6 +6,7 @@ import com.symphony.bdk.core.auth.AuthSession; import com.symphony.bdk.core.config.model.BdkConfig; +import com.symphony.bdk.core.service.clientdata.OboClientDataService; import com.symphony.bdk.core.service.connection.OboConnectionService; import com.symphony.bdk.core.service.message.OboMessageService; import com.symphony.bdk.core.service.presence.OboPresenceService; @@ -68,4 +69,10 @@ void testOboSessions() { OboSessionService sessionService = oboServices.sessions(); assertNotNull(sessionService); } + + @Test + void testOboClientData() { + OboClientDataService clientDataService = oboServices.clientData(); + assertNotNull(clientDataService); + } } diff --git a/symphony-bdk-core/src/test/java/com/symphony/bdk/core/ServiceFactoryTest.java b/symphony-bdk-core/src/test/java/com/symphony/bdk/core/ServiceFactoryTest.java index 39c88dc1b..1ab7f1545 100644 --- a/symphony-bdk-core/src/test/java/com/symphony/bdk/core/ServiceFactoryTest.java +++ b/symphony-bdk-core/src/test/java/com/symphony/bdk/core/ServiceFactoryTest.java @@ -30,7 +30,7 @@ import com.symphony.bdk.core.service.session.SessionService; import com.symphony.bdk.core.service.signal.SignalService; import com.symphony.bdk.core.service.stream.StreamService; -import com.symphony.bdk.core.service.user.UserOrganizationService; +import com.symphony.bdk.core.service.clientdata.ClientDataService; import com.symphony.bdk.core.service.user.UserService; import com.symphony.bdk.gen.api.model.UserV2; import com.symphony.bdk.http.api.ApiClient; @@ -74,9 +74,9 @@ void getUserServiceTest() { } @Test - void getUserOrganizationServiceTest() { - UserOrganizationService userOrganizationService = this.serviceFactory.getUserOrganizationService(); - assertNotNull(userOrganizationService); + void getClientDataServiceTest() { + ClientDataService clientDataService = this.serviceFactory.getClientDataService(); + assertNotNull(clientDataService); } @Test diff --git a/symphony-bdk-core/src/test/java/com/symphony/bdk/core/service/user/UserOrganizationServiceTest.java b/symphony-bdk-core/src/test/java/com/symphony/bdk/core/service/clientdata/ClientDataServiceTest.java similarity index 86% rename from symphony-bdk-core/src/test/java/com/symphony/bdk/core/service/user/UserOrganizationServiceTest.java rename to symphony-bdk-core/src/test/java/com/symphony/bdk/core/service/clientdata/ClientDataServiceTest.java index 1125c78d3..b0160a8ca 100644 --- a/symphony-bdk-core/src/test/java/com/symphony/bdk/core/service/user/UserOrganizationServiceTest.java +++ b/symphony-bdk-core/src/test/java/com/symphony/bdk/core/service/clientdata/ClientDataServiceTest.java @@ -1,4 +1,4 @@ -package com.symphony.bdk.core.service.user; +package com.symphony.bdk.core.service.clientdata; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -20,7 +20,7 @@ import java.util.List; -class UserOrganizationServiceTest { +class ClientDataServiceTest { private static final String SESSION_TOKEN = "test-session-token"; private static final String ORGANISATIONS_PATH = "/v5/users/current/organisations"; @@ -41,7 +41,7 @@ class UserOrganizationServiceTest { private MockApiClient mockApiClient; private AuthSession authSession; - private UserOrganizationService service; + private ClientDataService service; @BeforeEach void setUp() { @@ -50,14 +50,14 @@ void setUp() { when(this.authSession.getSessionToken()).thenReturn(SESSION_TOKEN); ApiClient usersClient = this.mockApiClient.getApiClient(""); - this.service = new UserOrganizationService(usersClient, this.authSession, new RetryWithRecoveryBuilder<>()); + this.service = new ClientDataService(usersClient, this.authSession, new RetryWithRecoveryBuilder<>()); } @Test - void testGetUserOrganizationSuccess() { + void testGetClientDataSuccess() { this.mockApiClient.onGet(ORGANISATIONS_PATH, ORGANISATIONS_JSON); - UserOrganisationsResponse org = this.service.getUserOrganization(); + UserOrganisationsResponse org = this.service.getClientData(); assertNotNull(org); assertEquals(1, org.getDirectories().size()); @@ -83,6 +83,15 @@ void testGetUserOrganizationSuccess() { assertEquals("stream", bl.getRecipients().get(1).getType()); } + @Test + void testGetUserOrganizationBackwardsCompatibility() { + this.mockApiClient.onGet(ORGANISATIONS_PATH, ORGANISATIONS_JSON); + + UserOrganisationsResponse org = this.service.getUserOrganization(); + assertNotNull(org); + assertEquals(1, org.getDirectories().size()); + } + @Test void testGetUserDirectories() { this.mockApiClient.onGet(ORGANISATIONS_PATH, ORGANISATIONS_JSON); @@ -119,7 +128,7 @@ void testGetUserDistributionLists() { void testEmptyResponseReturnsEmptyLists() { this.mockApiClient.onGet(ORGANISATIONS_PATH, "{}"); - UserOrganisationsResponse org = this.service.getUserOrganization(); + UserOrganisationsResponse org = this.service.getClientData(); assertNotNull(org); assertTrue(org.getDirectories().isEmpty()); assertTrue(org.getWorkspaces().isEmpty()); @@ -131,7 +140,7 @@ void testOboModeExecution() { AuthSession oboSession = mock(AuthSession.class); when(oboSession.getSessionToken()).thenReturn("obo-token"); - UserOrganizationService oboService = this.service.obo(oboSession); + OboClientDataService oboService = this.service.obo(oboSession); assertNotNull(oboService); this.mockApiClient.onGet(ORGANISATIONS_PATH, ORGANISATIONS_JSON); @@ -143,9 +152,9 @@ void testOboModeExecution() { @Test void testMissingAuthSessionThrowsIllegalStateException() { ApiClient usersClient = this.mockApiClient.getApiClient(""); - UserOrganizationService unauthenticatedService = - new UserOrganizationService(usersClient, new RetryWithRecoveryBuilder<>()); + ClientDataService unauthenticatedService = + new ClientDataService(usersClient, new RetryWithRecoveryBuilder<>()); - assertThrows(IllegalStateException.class, unauthenticatedService::getUserOrganization); + assertThrows(IllegalStateException.class, unauthenticatedService::getClientData); } } diff --git a/symphony-bdk-spring/symphony-bdk-core-spring-boot-starter/src/main/java/com/symphony/bdk/spring/config/BdkOboServiceConfig.java b/symphony-bdk-spring/symphony-bdk-core-spring-boot-starter/src/main/java/com/symphony/bdk/spring/config/BdkOboServiceConfig.java index 1e857f430..8c78c2cd1 100644 --- a/symphony-bdk-spring/symphony-bdk-core-spring-boot-starter/src/main/java/com/symphony/bdk/spring/config/BdkOboServiceConfig.java +++ b/symphony-bdk-spring/symphony-bdk-core-spring-boot-starter/src/main/java/com/symphony/bdk/spring/config/BdkOboServiceConfig.java @@ -12,7 +12,7 @@ import com.symphony.bdk.core.service.session.SessionService; import com.symphony.bdk.core.service.signal.SignalService; import com.symphony.bdk.core.service.stream.StreamService; -import com.symphony.bdk.core.service.user.UserOrganizationService; +import com.symphony.bdk.core.service.clientdata.ClientDataService; import com.symphony.bdk.core.service.user.UserService; import com.symphony.bdk.gen.api.AttachmentsApi; import com.symphony.bdk.gen.api.AuditTrailApi; @@ -89,9 +89,9 @@ public UserService oboUserService(UserApi userApi, UsersApi usersApi, AuditTrail @Bean @ConditionalOnMissingBean - public UserOrganizationService oboUserOrganizationService( + public ClientDataService oboClientDataService( @Qualifier("usersApiClient") ApiClient usersApiClient, BdkConfig config) { - return new UserOrganizationService(usersApiClient, new RetryWithRecoveryBuilder<>().retryConfig(config.getRetry())); + return new ClientDataService(usersApiClient, new RetryWithRecoveryBuilder<>().retryConfig(config.getRetry())); } @Bean diff --git a/symphony-bdk-spring/symphony-bdk-core-spring-boot-starter/src/main/java/com/symphony/bdk/spring/config/BdkServiceConfig.java b/symphony-bdk-spring/symphony-bdk-core-spring-boot-starter/src/main/java/com/symphony/bdk/spring/config/BdkServiceConfig.java index a217814f9..7f94d67bb 100644 --- a/symphony-bdk-spring/symphony-bdk-core-spring-boot-starter/src/main/java/com/symphony/bdk/spring/config/BdkServiceConfig.java +++ b/symphony-bdk-spring/symphony-bdk-core-spring-boot-starter/src/main/java/com/symphony/bdk/spring/config/BdkServiceConfig.java @@ -13,7 +13,7 @@ import com.symphony.bdk.core.service.session.SessionService; import com.symphony.bdk.core.service.signal.SignalService; import com.symphony.bdk.core.service.stream.StreamService; -import com.symphony.bdk.core.service.user.UserOrganizationService; +import com.symphony.bdk.core.service.clientdata.ClientDataService; import com.symphony.bdk.core.service.user.UserService; import com.symphony.bdk.gen.api.AppEntitlementApi; import com.symphony.bdk.gen.api.ApplicationApi; @@ -75,9 +75,9 @@ public UserService userService(UserApi userApi, UsersApi usersApi, AuditTrailApi @Bean @ConditionalOnMissingBean - public UserOrganizationService userOrganizationService( + public ClientDataService clientDataService( @Qualifier("usersApiClient") ApiClient usersApiClient, AuthSession botSession, BdkConfig config) { - return new UserOrganizationService(usersApiClient, botSession, new RetryWithRecoveryBuilder<>().retryConfig(config.getRetry())); + return new ClientDataService(usersApiClient, botSession, new RetryWithRecoveryBuilder<>().retryConfig(config.getRetry())); } @Bean From fa10bcaa4177e450b3a2041495dc3bcdd3989688 Mon Sep 17 00:00:00 2001 From: jameshanna-symphony Date: Thu, 8 Oct 2026 15:42:49 +0100 Subject: [PATCH 08/17] Update naming --- .../service/clientdata/ClientDataService.java | 40 +++++---- .../clientdata/OboClientDataService.java | 44 +++++----- .../clientdata/ClientDataServiceTest.java | 81 ++++++++++--------- 3 files changed, 87 insertions(+), 78 deletions(-) diff --git a/symphony-bdk-core/src/main/java/com/symphony/bdk/core/service/clientdata/ClientDataService.java b/symphony-bdk-core/src/main/java/com/symphony/bdk/core/service/clientdata/ClientDataService.java index 5c572fdc4..83ebaec2b 100644 --- a/symphony-bdk-core/src/main/java/com/symphony/bdk/core/service/clientdata/ClientDataService.java +++ b/symphony-bdk-core/src/main/java/com/symphony/bdk/core/service/clientdata/ClientDataService.java @@ -5,10 +5,10 @@ import com.symphony.bdk.core.retry.RetryWithRecoveryBuilder; import com.symphony.bdk.core.retry.function.SupplierWithApiException; import com.symphony.bdk.core.service.OboService; -import com.symphony.bdk.gen.api.UserOrganisationsApi; -import com.symphony.bdk.gen.api.model.BlastList; -import com.symphony.bdk.gen.api.model.OrganisationStructure; -import com.symphony.bdk.gen.api.model.UserOrganisationsResponse; +import com.symphony.bdk.gen.api.ClientDataApi; +import com.symphony.bdk.gen.api.model.ClientDataResponse; +import com.symphony.bdk.gen.api.model.DistributionLists; +import com.symphony.bdk.gen.api.model.ClientDataStructure; import com.symphony.bdk.http.api.ApiClient; import com.symphony.bdk.http.api.ApiException; @@ -20,20 +20,20 @@ /** * Service to retrieve the authenticated user's client data organization structures — * custom folders, multi-chat workspaces, and distribution lists — from the - * {@code GET /v5/users/organisations} endpoint. + * {@code GET /v5/users/clientdata} endpoint. */ @API(status = API.Status.STABLE) public class ClientDataService implements OboClientDataService, OboService { private final ApiClient apiClient; - private final UserOrganisationsApi userOrganisationsApi; + private final ClientDataApi clientDataApi; private final @Nullable AuthSession authSession; private final RetryWithRecoveryBuilder retryBuilder; public ClientDataService(ApiClient apiClient, AuthSession authSession, RetryWithRecoveryBuilder retryBuilder) { this.apiClient = apiClient; - this.userOrganisationsApi = new UserOrganisationsApi(apiClient); + this.clientDataApi = new ClientDataApi(apiClient); this.authSession = authSession; this.retryBuilder = RetryWithRecoveryBuilder.copyWithoutRecoveryStrategies(retryBuilder) .recoveryStrategy(ApiException::isUnauthorized, authSession::refresh); @@ -41,7 +41,7 @@ public ClientDataService(ApiClient apiClient, AuthSession authSession, public ClientDataService(ApiClient apiClient, RetryWithRecoveryBuilder retryBuilder) { this.apiClient = apiClient; - this.userOrganisationsApi = new UserOrganisationsApi(apiClient); + this.clientDataApi = new ClientDataApi(apiClient); this.authSession = null; this.retryBuilder = RetryWithRecoveryBuilder.copyWithoutRecoveryStrategies(retryBuilder); } @@ -55,24 +55,32 @@ public OboClientDataService obo(AuthSession oboSession) { * {@inheritDoc} */ @Override - public UserOrganisationsResponse getClientData() { - return executeAndRetry("getUserOrganisations", - () -> this.userOrganisationsApi.getUserOrganisations(this.authSession.getSessionToken(), null)); + public ClientDataResponse getClientData() { + return executeAndRetry("getClientData", + () -> this.clientDataApi.getClientData(this.authSession.getSessionToken(), null)); } /** * {@inheritDoc} */ @Override - public List getUserDirectories() { - return getClientData().getDirectories(); + public List getUserFolders() { + return getClientData().getFolders(); } /** * {@inheritDoc} */ @Override - public List getUserWorkspaces() { + public List getUserDirectories() { + return getUserFolders(); + } + + /** + * {@inheritDoc} + */ + @Override + public List getUserWorkspaces() { return getClientData().getWorkspaces(); } @@ -80,8 +88,8 @@ public List getUserWorkspaces() { * {@inheritDoc} */ @Override - public List getUserDistributionLists() { - return getClientData().getBlastLists(); + public List getUserDistributionLists() { + return getClientData().getDistributionLists(); } private T executeAndRetry(String name, SupplierWithApiException supplier) { diff --git a/symphony-bdk-core/src/main/java/com/symphony/bdk/core/service/clientdata/OboClientDataService.java b/symphony-bdk-core/src/main/java/com/symphony/bdk/core/service/clientdata/OboClientDataService.java index 87e144a59..d5558dd2b 100644 --- a/symphony-bdk-core/src/main/java/com/symphony/bdk/core/service/clientdata/OboClientDataService.java +++ b/symphony-bdk-core/src/main/java/com/symphony/bdk/core/service/clientdata/OboClientDataService.java @@ -1,8 +1,8 @@ package com.symphony.bdk.core.service.clientdata; -import com.symphony.bdk.gen.api.model.BlastList; -import com.symphony.bdk.gen.api.model.OrganisationStructure; -import com.symphony.bdk.gen.api.model.UserOrganisationsResponse; +import com.symphony.bdk.gen.api.model.ClientDataResponse; +import com.symphony.bdk.gen.api.model.DistributionLists; +import com.symphony.bdk.gen.api.model.ClientDataStructure; import org.apiguardian.api.API; @@ -15,46 +15,46 @@ public interface OboClientDataService { /** - * Retrieves the authenticated user's aggregated client data (directories, workspaces, and - * blast distribution lists) for the default {@code symphonyPrime} application. + * Retrieves the authenticated user's aggregated client data (folders, workspaces, and + * distribution lists) for the default {@code symphonyPrime} application. * {@link ClientDataService#getClientData()} * - * @return a {@link UserOrganisationsResponse} containing the user's client data. + * @return a {@link ClientDataResponse} containing the user's client data. */ - UserOrganisationsResponse getClientData(); + ClientDataResponse getClientData(); /** - * Retrieves the authenticated user's aggregated organization data. + * Retrieves the authenticated user's custom folders. + * {@link ClientDataService#getUserFolders()} * - * @deprecated Use {@link #getClientData()} instead. - * @return a {@link UserOrganisationsResponse} containing the user's organization data. + * @return a list of {@link ClientDataStructure} folders. */ - @Deprecated - default UserOrganisationsResponse getUserOrganization() { - return getClientData(); - } + List getUserFolders(); /** * Retrieves the authenticated user's custom directories/folders. - * {@link ClientDataService#getUserDirectories()} * - * @return a list of {@link OrganisationStructure} directories. + * @deprecated Use {@link #getUserFolders()} instead. + * @return a list of {@link ClientDataStructure} directories. */ - List getUserDirectories(); + @Deprecated + default List getUserDirectories() { + return getUserFolders(); + } /** * Retrieves the authenticated user's workspaces. * {@link ClientDataService#getUserWorkspaces()} * - * @return a list of {@link OrganisationStructure} workspaces. + * @return a list of {@link ClientDataStructure} workspaces. */ - List getUserWorkspaces(); + List getUserWorkspaces(); /** - * Retrieves the authenticated user's blast distribution lists, each with its recipients. + * Retrieves the authenticated user's distribution lists, each with its recipients. * {@link ClientDataService#getUserDistributionLists()} * - * @return a list of {@link BlastList} blast lists. + * @return a list of {@link DistributionLists} distribution lists. */ - List getUserDistributionLists(); + List getUserDistributionLists(); } diff --git a/symphony-bdk-core/src/test/java/com/symphony/bdk/core/service/clientdata/ClientDataServiceTest.java b/symphony-bdk-core/src/test/java/com/symphony/bdk/core/service/clientdata/ClientDataServiceTest.java index b0160a8ca..2e28f8a4b 100644 --- a/symphony-bdk-core/src/test/java/com/symphony/bdk/core/service/clientdata/ClientDataServiceTest.java +++ b/symphony-bdk-core/src/test/java/com/symphony/bdk/core/service/clientdata/ClientDataServiceTest.java @@ -10,9 +10,9 @@ import com.symphony.bdk.core.auth.AuthSession; import com.symphony.bdk.core.retry.RetryWithRecoveryBuilder; import com.symphony.bdk.core.test.MockApiClient; -import com.symphony.bdk.gen.api.model.BlastList; -import com.symphony.bdk.gen.api.model.OrganisationStructure; -import com.symphony.bdk.gen.api.model.UserOrganisationsResponse; +import com.symphony.bdk.gen.api.model.ClientDataResponse; +import com.symphony.bdk.gen.api.model.DistributionLists; +import com.symphony.bdk.gen.api.model.ClientDataStructure; import com.symphony.bdk.http.api.ApiClient; import org.junit.jupiter.api.BeforeEach; @@ -23,16 +23,16 @@ class ClientDataServiceTest { private static final String SESSION_TOKEN = "test-session-token"; - private static final String ORGANISATIONS_PATH = "/v5/users/current/organisations"; + private static final String CLIENT_DATA_PATH = "/v5/users/clientdata"; - private static final String ORGANISATIONS_JSON = "{\n" - + " \"directories\": [\n" + private static final String CLIENT_DATA_JSON = "{\n" + + " \"folders\": [\n" + " {\"id\": \"folder-27001\", \"name\": \"Other Teams\", \"streamIds\": [\"stream-a\", \"stream-b\"]}\n" + " ],\n" + " \"workspaces\": [\n" + " {\"id\": \"tab-1001\", \"name\": \"SRE Workspace\", \"streamIds\": [\"sre-stream-1\", \"sre-stream-2\"]}\n" + " ],\n" - + " \"blastLists\": [\n" + + " \"distributionLists\": [\n" + " {\"id\": \"a97be35a\", \"name\": \"Blast People\", \"recipients\": [" + " {\"id\": \"user-1\", \"type\": \"user\"}," + " {\"id\": \"blast-stream-1\", \"type\": \"stream\"}]}\n" @@ -55,25 +55,25 @@ void setUp() { @Test void testGetClientDataSuccess() { - this.mockApiClient.onGet(ORGANISATIONS_PATH, ORGANISATIONS_JSON); + this.mockApiClient.onGet(CLIENT_DATA_PATH, CLIENT_DATA_JSON); - UserOrganisationsResponse org = this.service.getClientData(); + ClientDataResponse org = this.service.getClientData(); assertNotNull(org); - assertEquals(1, org.getDirectories().size()); - OrganisationStructure dir = org.getDirectories().get(0); + assertEquals(1, org.getFolders().size()); + ClientDataStructure dir = org.getFolders().get(0); assertEquals("folder-27001", dir.getId()); assertEquals("Other Teams", dir.getName()); assertEquals(List.of("stream-a", "stream-b"), dir.getStreamIds()); assertEquals(1, org.getWorkspaces().size()); - OrganisationStructure ws = org.getWorkspaces().get(0); + ClientDataStructure ws = org.getWorkspaces().get(0); assertEquals("tab-1001", ws.getId()); assertEquals("SRE Workspace", ws.getName()); assertEquals(List.of("sre-stream-1", "sre-stream-2"), ws.getStreamIds()); - assertEquals(1, org.getBlastLists().size()); - BlastList bl = org.getBlastLists().get(0); + assertEquals(1, org.getDistributionLists().size()); + DistributionLists bl = org.getDistributionLists().get(0); assertEquals("a97be35a", bl.getId()); assertEquals("Blast People", bl.getName()); assertEquals(2, bl.getRecipients().size()); @@ -84,19 +84,20 @@ void testGetClientDataSuccess() { } @Test - void testGetUserOrganizationBackwardsCompatibility() { - this.mockApiClient.onGet(ORGANISATIONS_PATH, ORGANISATIONS_JSON); + void testGetUserFolders() { + this.mockApiClient.onGet(CLIENT_DATA_PATH, CLIENT_DATA_JSON); - UserOrganisationsResponse org = this.service.getUserOrganization(); - assertNotNull(org); - assertEquals(1, org.getDirectories().size()); + List folders = this.service.getUserFolders(); + assertEquals(1, folders.size()); + assertEquals("folder-27001", folders.get(0).getId()); + assertEquals(List.of("stream-a", "stream-b"), folders.get(0).getStreamIds()); } @Test - void testGetUserDirectories() { - this.mockApiClient.onGet(ORGANISATIONS_PATH, ORGANISATIONS_JSON); + void testGetUserDirectoriesBackwardsCompatibility() { + this.mockApiClient.onGet(CLIENT_DATA_PATH, CLIENT_DATA_JSON); - List directories = this.service.getUserDirectories(); + List directories = this.service.getUserDirectories(); assertEquals(1, directories.size()); assertEquals("folder-27001", directories.get(0).getId()); assertEquals(List.of("stream-a", "stream-b"), directories.get(0).getStreamIds()); @@ -104,9 +105,9 @@ void testGetUserDirectories() { @Test void testGetUserWorkspaces() { - this.mockApiClient.onGet(ORGANISATIONS_PATH, ORGANISATIONS_JSON); + this.mockApiClient.onGet(CLIENT_DATA_PATH, CLIENT_DATA_JSON); - List workspaces = this.service.getUserWorkspaces(); + List workspaces = this.service.getUserWorkspaces(); assertEquals(1, workspaces.size()); assertEquals("tab-1001", workspaces.get(0).getId()); assertEquals("SRE Workspace", workspaces.get(0).getName()); @@ -114,25 +115,25 @@ void testGetUserWorkspaces() { @Test void testGetUserDistributionLists() { - this.mockApiClient.onGet(ORGANISATIONS_PATH, ORGANISATIONS_JSON); - - List blastLists = this.service.getUserDistributionLists(); - assertEquals(1, blastLists.size()); - assertEquals("a97be35a", blastLists.get(0).getId()); - assertEquals("Blast People", blastLists.get(0).getName()); - assertEquals(2, blastLists.get(0).getRecipients().size()); - assertEquals("stream", blastLists.get(0).getRecipients().get(1).getType()); + this.mockApiClient.onGet(CLIENT_DATA_PATH, CLIENT_DATA_JSON); + + List distributionLists = this.service.getUserDistributionLists(); + assertEquals(1, distributionLists.size()); + assertEquals("a97be35a", distributionLists.get(0).getId()); + assertEquals("Blast People", distributionLists.get(0).getName()); + assertEquals(2, distributionLists.get(0).getRecipients().size()); + assertEquals("stream", distributionLists.get(0).getRecipients().get(1).getType()); } @Test void testEmptyResponseReturnsEmptyLists() { - this.mockApiClient.onGet(ORGANISATIONS_PATH, "{}"); + this.mockApiClient.onGet(CLIENT_DATA_PATH, "{}"); - UserOrganisationsResponse org = this.service.getClientData(); + ClientDataResponse org = this.service.getClientData(); assertNotNull(org); - assertTrue(org.getDirectories().isEmpty()); + assertTrue(org.getFolders().isEmpty()); assertTrue(org.getWorkspaces().isEmpty()); - assertTrue(org.getBlastLists().isEmpty()); + assertTrue(org.getDistributionLists().isEmpty()); } @Test @@ -143,10 +144,10 @@ void testOboModeExecution() { OboClientDataService oboService = this.service.obo(oboSession); assertNotNull(oboService); - this.mockApiClient.onGet(ORGANISATIONS_PATH, ORGANISATIONS_JSON); - List blastLists = oboService.getUserDistributionLists(); - assertEquals(1, blastLists.size()); - assertEquals("a97be35a", blastLists.get(0).getId()); + this.mockApiClient.onGet(CLIENT_DATA_PATH, CLIENT_DATA_JSON); + List distributionLists = oboService.getUserDistributionLists(); + assertEquals(1, distributionLists.size()); + assertEquals("a97be35a", distributionLists.get(0).getId()); } @Test From 501fc86a0fb8731ce0aca301d2e60ae8bb437420 Mon Sep 17 00:00:00 2001 From: jameshanna-symphony Date: Thu, 8 Oct 2026 16:04:20 +0100 Subject: [PATCH 09/17] Remove deprecated method --- .../core/service/clientdata/ClientDataService.java | 8 -------- .../core/service/clientdata/OboClientDataService.java | 11 ----------- .../service/clientdata/ClientDataServiceTest.java | 10 ---------- 3 files changed, 29 deletions(-) diff --git a/symphony-bdk-core/src/main/java/com/symphony/bdk/core/service/clientdata/ClientDataService.java b/symphony-bdk-core/src/main/java/com/symphony/bdk/core/service/clientdata/ClientDataService.java index 83ebaec2b..265251ee3 100644 --- a/symphony-bdk-core/src/main/java/com/symphony/bdk/core/service/clientdata/ClientDataService.java +++ b/symphony-bdk-core/src/main/java/com/symphony/bdk/core/service/clientdata/ClientDataService.java @@ -68,14 +68,6 @@ public List getUserFolders() { return getClientData().getFolders(); } - /** - * {@inheritDoc} - */ - @Override - public List getUserDirectories() { - return getUserFolders(); - } - /** * {@inheritDoc} */ diff --git a/symphony-bdk-core/src/main/java/com/symphony/bdk/core/service/clientdata/OboClientDataService.java b/symphony-bdk-core/src/main/java/com/symphony/bdk/core/service/clientdata/OboClientDataService.java index d5558dd2b..86a80bbde 100644 --- a/symphony-bdk-core/src/main/java/com/symphony/bdk/core/service/clientdata/OboClientDataService.java +++ b/symphony-bdk-core/src/main/java/com/symphony/bdk/core/service/clientdata/OboClientDataService.java @@ -31,17 +31,6 @@ public interface OboClientDataService { */ List getUserFolders(); - /** - * Retrieves the authenticated user's custom directories/folders. - * - * @deprecated Use {@link #getUserFolders()} instead. - * @return a list of {@link ClientDataStructure} directories. - */ - @Deprecated - default List getUserDirectories() { - return getUserFolders(); - } - /** * Retrieves the authenticated user's workspaces. * {@link ClientDataService#getUserWorkspaces()} diff --git a/symphony-bdk-core/src/test/java/com/symphony/bdk/core/service/clientdata/ClientDataServiceTest.java b/symphony-bdk-core/src/test/java/com/symphony/bdk/core/service/clientdata/ClientDataServiceTest.java index 2e28f8a4b..f8944c2ca 100644 --- a/symphony-bdk-core/src/test/java/com/symphony/bdk/core/service/clientdata/ClientDataServiceTest.java +++ b/symphony-bdk-core/src/test/java/com/symphony/bdk/core/service/clientdata/ClientDataServiceTest.java @@ -93,16 +93,6 @@ void testGetUserFolders() { assertEquals(List.of("stream-a", "stream-b"), folders.get(0).getStreamIds()); } - @Test - void testGetUserDirectoriesBackwardsCompatibility() { - this.mockApiClient.onGet(CLIENT_DATA_PATH, CLIENT_DATA_JSON); - - List directories = this.service.getUserDirectories(); - assertEquals(1, directories.size()); - assertEquals("folder-27001", directories.get(0).getId()); - assertEquals(List.of("stream-a", "stream-b"), directories.get(0).getStreamIds()); - } - @Test void testGetUserWorkspaces() { this.mockApiClient.onGet(CLIENT_DATA_PATH, CLIENT_DATA_JSON); From 10bdf83ceead7a13b201f50772c9f6e90bcdc996 Mon Sep 17 00:00:00 2001 From: jameshanna-symphony Date: Tue, 13 Oct 2026 16:36:07 +0100 Subject: [PATCH 10/17] Update ClientDataService.java --- .../bdk/core/service/clientdata/ClientDataService.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/symphony-bdk-core/src/main/java/com/symphony/bdk/core/service/clientdata/ClientDataService.java b/symphony-bdk-core/src/main/java/com/symphony/bdk/core/service/clientdata/ClientDataService.java index 265251ee3..b428f57bd 100644 --- a/symphony-bdk-core/src/main/java/com/symphony/bdk/core/service/clientdata/ClientDataService.java +++ b/symphony-bdk-core/src/main/java/com/symphony/bdk/core/service/clientdata/ClientDataService.java @@ -18,8 +18,8 @@ import java.util.List; /** - * Service to retrieve the authenticated user's client data organization structures — - * custom folders, multi-chat workspaces, and distribution lists — from the + * Service to retrieve the authenticated user's client data organization structures + * custom folders, multi-chat workspaces, and distribution lists from the * {@code GET /v5/users/clientdata} endpoint. */ @API(status = API.Status.STABLE) From 3385e762d7dd4fcd98aa76a6f7eaff9d289103a6 Mon Sep 17 00:00:00 2001 From: jameshanna-symphony Date: Tue, 13 Oct 2026 17:27:01 +0100 Subject: [PATCH 11/17] Update ClientDataServiceTest.java --- .../clientdata/ClientDataServiceTest.java | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/symphony-bdk-core/src/test/java/com/symphony/bdk/core/service/clientdata/ClientDataServiceTest.java b/symphony-bdk-core/src/test/java/com/symphony/bdk/core/service/clientdata/ClientDataServiceTest.java index f8944c2ca..c143fe301 100644 --- a/symphony-bdk-core/src/test/java/com/symphony/bdk/core/service/clientdata/ClientDataServiceTest.java +++ b/symphony-bdk-core/src/test/java/com/symphony/bdk/core/service/clientdata/ClientDataServiceTest.java @@ -34,7 +34,8 @@ class ClientDataServiceTest { + " ],\n" + " \"distributionLists\": [\n" + " {\"id\": \"a97be35a\", \"name\": \"Blast People\", \"recipients\": [" - + " {\"id\": \"user-1\", \"type\": \"user\"}," + + " {\"id\": \"user-1\", \"type\": \"user\", \"name\": \"Jerome Michel\"}," + + " {\"id\": \"folder-1\", \"type\": \"folder\", \"streamIds\": [\"stream-a\"]}," + " {\"id\": \"blast-stream-1\", \"type\": \"stream\"}]}\n" + " ]\n" + "}"; @@ -76,11 +77,15 @@ void testGetClientDataSuccess() { DistributionLists bl = org.getDistributionLists().get(0); assertEquals("a97be35a", bl.getId()); assertEquals("Blast People", bl.getName()); - assertEquals(2, bl.getRecipients().size()); + assertEquals(3, bl.getRecipients().size()); assertEquals("user-1", bl.getRecipients().get(0).getId()); assertEquals("user", bl.getRecipients().get(0).getType()); - assertEquals("blast-stream-1", bl.getRecipients().get(1).getId()); - assertEquals("stream", bl.getRecipients().get(1).getType()); + assertEquals("Jerome Michel", bl.getRecipients().get(0).getName()); + assertEquals("folder-1", bl.getRecipients().get(1).getId()); + assertEquals("folder", bl.getRecipients().get(1).getType()); + assertEquals(List.of("stream-a"), bl.getRecipients().get(1).getStreamIds()); + assertEquals("blast-stream-1", bl.getRecipients().get(2).getId()); + assertEquals("stream", bl.getRecipients().get(2).getType()); } @Test @@ -111,8 +116,8 @@ void testGetUserDistributionLists() { assertEquals(1, distributionLists.size()); assertEquals("a97be35a", distributionLists.get(0).getId()); assertEquals("Blast People", distributionLists.get(0).getName()); - assertEquals(2, distributionLists.get(0).getRecipients().size()); - assertEquals("stream", distributionLists.get(0).getRecipients().get(1).getType()); + assertEquals(3, distributionLists.get(0).getRecipients().size()); + assertEquals("stream", distributionLists.get(0).getRecipients().get(2).getType()); } @Test @@ -138,6 +143,7 @@ void testOboModeExecution() { List distributionLists = oboService.getUserDistributionLists(); assertEquals(1, distributionLists.size()); assertEquals("a97be35a", distributionLists.get(0).getId()); + assertEquals(3, distributionLists.get(0).getRecipients().size()); } @Test From 109d2c0f3cf9111cf9a42600b2380f2b10a4c09a Mon Sep 17 00:00:00 2001 From: jameshanna-symphony Date: Tue, 13 Oct 2026 17:28:02 +0100 Subject: [PATCH 12/17] Update ClientDataServiceTest.java --- .../bdk/core/service/clientdata/ClientDataServiceTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/symphony-bdk-core/src/test/java/com/symphony/bdk/core/service/clientdata/ClientDataServiceTest.java b/symphony-bdk-core/src/test/java/com/symphony/bdk/core/service/clientdata/ClientDataServiceTest.java index c143fe301..d7696a78d 100644 --- a/symphony-bdk-core/src/test/java/com/symphony/bdk/core/service/clientdata/ClientDataServiceTest.java +++ b/symphony-bdk-core/src/test/java/com/symphony/bdk/core/service/clientdata/ClientDataServiceTest.java @@ -34,7 +34,7 @@ class ClientDataServiceTest { + " ],\n" + " \"distributionLists\": [\n" + " {\"id\": \"a97be35a\", \"name\": \"Blast People\", \"recipients\": [" - + " {\"id\": \"user-1\", \"type\": \"user\", \"name\": \"Jerome Michel\"}," + + " {\"id\": \"user-1\", \"type\": \"user\", \"name\": \"Test Name\"}," + " {\"id\": \"folder-1\", \"type\": \"folder\", \"streamIds\": [\"stream-a\"]}," + " {\"id\": \"blast-stream-1\", \"type\": \"stream\"}]}\n" + " ]\n" @@ -80,7 +80,7 @@ void testGetClientDataSuccess() { assertEquals(3, bl.getRecipients().size()); assertEquals("user-1", bl.getRecipients().get(0).getId()); assertEquals("user", bl.getRecipients().get(0).getType()); - assertEquals("Jerome Michel", bl.getRecipients().get(0).getName()); + assertEquals("Test Name", bl.getRecipients().get(0).getName()); assertEquals("folder-1", bl.getRecipients().get(1).getId()); assertEquals("folder", bl.getRecipients().get(1).getType()); assertEquals(List.of("stream-a"), bl.getRecipients().get(1).getStreamIds()); From 857dd66a362daee22c45afe373b142976424864d Mon Sep 17 00:00:00 2001 From: jameshanna-symphony Date: Thu, 15 Oct 2026 15:03:03 +0100 Subject: [PATCH 13/17] Update endpoint to /v5/users/current/clientdata --- .../symphony/bdk/core/service/clientdata/ClientDataService.java | 2 +- .../bdk/core/service/clientdata/ClientDataServiceTest.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/symphony-bdk-core/src/main/java/com/symphony/bdk/core/service/clientdata/ClientDataService.java b/symphony-bdk-core/src/main/java/com/symphony/bdk/core/service/clientdata/ClientDataService.java index b428f57bd..8f13ad10b 100644 --- a/symphony-bdk-core/src/main/java/com/symphony/bdk/core/service/clientdata/ClientDataService.java +++ b/symphony-bdk-core/src/main/java/com/symphony/bdk/core/service/clientdata/ClientDataService.java @@ -20,7 +20,7 @@ /** * Service to retrieve the authenticated user's client data organization structures * custom folders, multi-chat workspaces, and distribution lists from the - * {@code GET /v5/users/clientdata} endpoint. + * {@code GET /v5/users/current/clientdata} endpoint. */ @API(status = API.Status.STABLE) public class ClientDataService implements OboClientDataService, OboService { diff --git a/symphony-bdk-core/src/test/java/com/symphony/bdk/core/service/clientdata/ClientDataServiceTest.java b/symphony-bdk-core/src/test/java/com/symphony/bdk/core/service/clientdata/ClientDataServiceTest.java index d7696a78d..5a2f30ba8 100644 --- a/symphony-bdk-core/src/test/java/com/symphony/bdk/core/service/clientdata/ClientDataServiceTest.java +++ b/symphony-bdk-core/src/test/java/com/symphony/bdk/core/service/clientdata/ClientDataServiceTest.java @@ -23,7 +23,7 @@ class ClientDataServiceTest { private static final String SESSION_TOKEN = "test-session-token"; - private static final String CLIENT_DATA_PATH = "/v5/users/clientdata"; + private static final String CLIENT_DATA_PATH = "/v5/users/current/clientdata"; private static final String CLIENT_DATA_JSON = "{\n" + " \"folders\": [\n" From ed033d91dd5a25fe4d6fdc0b288aa4e9d86fdcb8 Mon Sep 17 00:00:00 2001 From: jameshanna-symphony Date: Tue, 20 Oct 2026 16:29:23 +0100 Subject: [PATCH 14/17] Update build.gradle --- symphony-bdk-core/build.gradle | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/symphony-bdk-core/build.gradle b/symphony-bdk-core/build.gradle index a1cbd501a..1d857c67c 100644 --- a/symphony-bdk-core/build.gradle +++ b/symphony-bdk-core/build.gradle @@ -80,13 +80,7 @@ dependencies { } // OpenAPI code generation -def apiBaseUrl = "https://raw.githubusercontent.com/finos/symphony-api-spec/a8efe8a6193769d0657fc57758918138aa4483bb" -// TEMP (BE-9865): the /v5/users/organisations endpoint is not yet in the pinned FINOS commit. -// Source the Users spec from the PR branch until the FINOS PR merges, then DELETE this override -// and bump apiBaseUrl above to the merged commit SHA. -def apiBaseUrlOverrides = [ - Users: "https://raw.githubusercontent.com/jameshanna-symphony/symphony-api-spec/BE-9865_Add_User_Organisation_Endpoint" -] +def apiBaseUrl = "https://raw.githubusercontent.com/finos/symphony-api-spec/a0b152881a0405b7510cfc7e3a9698e8cb8c566c" def generatedFolder = "${layout.buildDirectory.get()}/generated/openapi" def apisToGenerate = [ Agent: 'agent/agent-api-public-deprecated.yaml', @@ -100,7 +94,7 @@ sourceSets.main.java.srcDirs += "$generatedFolder/src/main/java" apisToGenerate.each { api, path -> def downloadTask = tasks.register("download$api", Download) { - src "${apiBaseUrlOverrides.getOrDefault(api, apiBaseUrl)}/$path" + src "$apiBaseUrl/$path" dest "${layout.buildDirectory.get()}/openapi/$api/${path.split("/")[1]}" overwrite false useETag "all" From dd2f31d943d22ad72ffcce85e41e5f83f848d352 Mon Sep 17 00:00:00 2001 From: jameshanna-symphony Date: Tue, 20 Oct 2026 16:49:19 +0100 Subject: [PATCH 15/17] Update streamcontext endpoint to messages --- .../com/symphony/bdk/core/ServiceFactory.java | 3 +-- .../bdk/core/service/stream/StreamService.java | 18 +++++++++--------- .../core/service/stream/StreamServiceTest.java | 14 +++++++------- .../bdk/spring/config/BdkApiClientsConfig.java | 7 ------- .../bdk/spring/config/BdkOboServiceConfig.java | 5 ++--- .../bdk/spring/config/BdkServiceConfig.java | 5 ++--- 6 files changed, 21 insertions(+), 31 deletions(-) diff --git a/symphony-bdk-core/src/main/java/com/symphony/bdk/core/ServiceFactory.java b/symphony-bdk-core/src/main/java/com/symphony/bdk/core/ServiceFactory.java index c4ad229eb..94c22876d 100644 --- a/symphony-bdk-core/src/main/java/com/symphony/bdk/core/ServiceFactory.java +++ b/symphony-bdk-core/src/main/java/com/symphony/bdk/core/ServiceFactory.java @@ -46,7 +46,6 @@ import com.symphony.bdk.gen.api.SessionApi; import com.symphony.bdk.gen.api.ShareApi; import com.symphony.bdk.gen.api.SignalsApi; -import com.symphony.bdk.gen.api.StreamContextApi; import com.symphony.bdk.gen.api.StreamsApi; import com.symphony.bdk.gen.api.SystemApi; import com.symphony.bdk.gen.api.UserApi; @@ -166,7 +165,7 @@ public ClientDataService getClientDataService() { */ public StreamService getStreamService() { return new StreamService(new StreamsApi(podClient), new RoomMembershipApi(podClient), new ShareApi(agentClient), - new StreamContextApi(agentClient), authSession, retryBuilder); + new MessagesApi(agentClient), authSession, retryBuilder); } public DisclaimerService getDisclaimerService() { diff --git a/symphony-bdk-core/src/main/java/com/symphony/bdk/core/service/stream/StreamService.java b/symphony-bdk-core/src/main/java/com/symphony/bdk/core/service/stream/StreamService.java index 9f9da24e0..95125d647 100644 --- a/symphony-bdk-core/src/main/java/com/symphony/bdk/core/service/stream/StreamService.java +++ b/symphony-bdk-core/src/main/java/com/symphony/bdk/core/service/stream/StreamService.java @@ -12,9 +12,9 @@ import com.symphony.bdk.core.service.pagination.PaginatedService; import com.symphony.bdk.core.service.pagination.model.PaginationAttribute; import com.symphony.bdk.core.service.pagination.model.StreamPaginationAttribute; +import com.symphony.bdk.gen.api.MessagesApi; import com.symphony.bdk.gen.api.RoomMembershipApi; import com.symphony.bdk.gen.api.ShareApi; -import com.symphony.bdk.gen.api.StreamContextApi; import com.symphony.bdk.gen.api.StreamsApi; import com.symphony.bdk.gen.api.model.MemberInfo; import com.symphony.bdk.gen.api.model.RoomDetail; @@ -66,29 +66,29 @@ public class StreamService implements OboStreamService, OboService retryBuilder; public StreamService(StreamsApi streamsApi, RoomMembershipApi membershipApi, ShareApi shareApi, - StreamContextApi streamContextApi, + MessagesApi messagesApi, AuthSession authSession, RetryWithRecoveryBuilder retryBuilder) { this.streamsApi = streamsApi; this.roomMembershipApi = membershipApi; this.shareApi = shareApi; - this.streamContextApi = streamContextApi; + this.messagesApi = messagesApi; this.authSession = authSession; this.retryBuilder = RetryWithRecoveryBuilder.copyWithoutRecoveryStrategies(retryBuilder) .recoveryStrategy(ApiException::isUnauthorized, authSession::refresh); } public StreamService(StreamsApi streamsApi, RoomMembershipApi membershipApi, ShareApi shareApi, - StreamContextApi streamContextApi, + MessagesApi messagesApi, RetryWithRecoveryBuilder retryBuilder) { this.streamsApi = streamsApi; this.roomMembershipApi = membershipApi; this.shareApi = shareApi; - this.streamContextApi = streamContextApi; + this.messagesApi = messagesApi; this.authSession = null; this.retryBuilder = RetryWithRecoveryBuilder.copyWithoutRecoveryStrategies(retryBuilder); } @@ -98,7 +98,7 @@ public StreamService(StreamsApi streamsApi, RoomMembershipApi membershipApi, Sha */ @Override public OboStreamService obo(AuthSession oboSession) { - return new StreamService(streamsApi, roomMembershipApi, shareApi, streamContextApi, oboSession, retryBuilder); + return new StreamService(streamsApi, roomMembershipApi, shareApi, messagesApi, oboSession, retryBuilder); } /** @@ -505,8 +505,8 @@ public List listRoomMembers(String roomId) { @Override public String getStreamContext(String streamId) { - return executeAndRetry("getStreamContext", streamContextApi.getApiClient().getBasePath(), - () -> streamContextApi.v1StreamContext(toUrlSafeIdIfNeeded(streamId), authSession.getSessionToken(), authSession.getKeyManagerToken())); + return executeAndRetry("getStreamContext", messagesApi.getApiClient().getBasePath(), + () -> messagesApi.v1StreamContext(toUrlSafeIdIfNeeded(streamId), null, null, null, null, authSession.getSessionToken(), authSession.getKeyManagerToken())); } private T executeAndRetry(String name, String address, SupplierWithApiException supplier) { diff --git a/symphony-bdk-core/src/test/java/com/symphony/bdk/core/service/stream/StreamServiceTest.java b/symphony-bdk-core/src/test/java/com/symphony/bdk/core/service/stream/StreamServiceTest.java index dc18ec929..f96a568d0 100644 --- a/symphony-bdk-core/src/test/java/com/symphony/bdk/core/service/stream/StreamServiceTest.java +++ b/symphony-bdk-core/src/test/java/com/symphony/bdk/core/service/stream/StreamServiceTest.java @@ -21,7 +21,7 @@ import com.symphony.bdk.core.test.MockApiClient; import com.symphony.bdk.gen.api.RoomMembershipApi; import com.symphony.bdk.gen.api.ShareApi; -import com.symphony.bdk.gen.api.StreamContextApi; +import com.symphony.bdk.gen.api.MessagesApi; import com.symphony.bdk.gen.api.StreamsApi; import com.symphony.bdk.gen.api.model.MemberInfo; import com.symphony.bdk.gen.api.model.RoomDetail; @@ -86,7 +86,7 @@ public class StreamServiceTest { private RoomMembershipApi spyRoomMembershipApi; private AuthSession authSession; private StreamsApi streamsApi; - private StreamContextApi streamContextApi; + private MessagesApi messagesApi; private ShareApi shareApi; @BeforeEach @@ -99,8 +99,8 @@ void setUp() { this.spyRoomMembershipApi = spy(new RoomMembershipApi(podClient)); this.streamsApi = spy(new StreamsApi(podClient)); this.shareApi = new ShareApi(agentClient); - this.streamContextApi = new StreamContextApi(agentClient); - this.service = new StreamService(this.streamsApi, this.spyRoomMembershipApi, this.shareApi, this.streamContextApi, + this.messagesApi = new MessagesApi(agentClient); + this.service = new StreamService(this.streamsApi, this.spyRoomMembershipApi, this.shareApi, this.messagesApi, this.authSession, new RetryWithRecoveryBuilder<>()); when(authSession.getSessionToken()).thenReturn("1234"); @@ -109,7 +109,7 @@ void setUp() { @Test void nonOboEndpointShouldThrowExceptionInOboMode() { - this.service = new StreamService(this.streamsApi, this.spyRoomMembershipApi, this.shareApi, this.streamContextApi, + this.service = new StreamService(this.streamsApi, this.spyRoomMembershipApi, this.shareApi, this.messagesApi, new RetryWithRecoveryBuilder<>()); assertThrows(IllegalStateException.class, () -> this.service.getStream("")); @@ -120,7 +120,7 @@ void getStreamInOboMode() throws IOException { this.mockApiClient.onGet(V2_STREAM_INFO.replace("{sid}", "p9B316LKDto7iOECc8Xuz3qeWsc0bdA"), JsonHelper.readFromClasspath("/stream/v2_stream_attributes.json")); - this.service = new StreamService(this.streamsApi, this.spyRoomMembershipApi, this.shareApi, this.streamContextApi, + this.service = new StreamService(this.streamsApi, this.spyRoomMembershipApi, this.shareApi, this.messagesApi, new RetryWithRecoveryBuilder<>()); V2StreamAttributes stream = this.service.obo(this.authSession).getStream("p9B316LKDto7iOECc8Xuz3qeWsc0bdA"); @@ -523,7 +523,7 @@ void getStreamContextTest() { void getStreamContextInOboMode() { this.mockApiClient.onGet(V1_STREAM_CONTEXT.replace("{sid}", "p9B316LKDto7iOECc8Xuz3qeWsc0bdA"), "\"Markdown Context\""); - this.service = new StreamService(this.streamsApi, this.spyRoomMembershipApi, this.shareApi, this.streamContextApi, + this.service = new StreamService(this.streamsApi, this.spyRoomMembershipApi, this.shareApi, this.messagesApi, new RetryWithRecoveryBuilder<>()); String context = this.service.obo(this.authSession).getStreamContext("p9B316LKDto7iOECc8Xuz3qeWsc0bdA"); diff --git a/symphony-bdk-spring/symphony-bdk-core-spring-boot-starter/src/main/java/com/symphony/bdk/spring/config/BdkApiClientsConfig.java b/symphony-bdk-spring/symphony-bdk-core-spring-boot-starter/src/main/java/com/symphony/bdk/spring/config/BdkApiClientsConfig.java index b498c474a..06785ac59 100644 --- a/symphony-bdk-spring/symphony-bdk-core-spring-boot-starter/src/main/java/com/symphony/bdk/spring/config/BdkApiClientsConfig.java +++ b/symphony-bdk-spring/symphony-bdk-core-spring-boot-starter/src/main/java/com/symphony/bdk/spring/config/BdkApiClientsConfig.java @@ -1,6 +1,5 @@ package com.symphony.bdk.spring.config; -import com.symphony.bdk.gen.api.StreamContextApi; import com.symphony.bdk.http.api.ApiClient; import com.symphony.bdk.gen.api.AppEntitlementApi; import com.symphony.bdk.gen.api.ApplicationApi; @@ -195,12 +194,6 @@ public StreamsApi streamsApi(@Qualifier("podApiClient") ApiClient podApiClient) return new StreamsApi(podApiClient); } - @Bean - @ConditionalOnMissingBean - public StreamContextApi streamsContextApi(@Qualifier("agentApiClient") ApiClient agentApiClient) { - return new StreamContextApi(agentApiClient); - } - @Bean @ConditionalOnMissingBean public SystemApi systemApi(@Qualifier("agentApiClient") ApiClient agentApiClient) { diff --git a/symphony-bdk-spring/symphony-bdk-core-spring-boot-starter/src/main/java/com/symphony/bdk/spring/config/BdkOboServiceConfig.java b/symphony-bdk-spring/symphony-bdk-core-spring-boot-starter/src/main/java/com/symphony/bdk/spring/config/BdkOboServiceConfig.java index 8c78c2cd1..fb364856d 100644 --- a/symphony-bdk-spring/symphony-bdk-core-spring-boot-starter/src/main/java/com/symphony/bdk/spring/config/BdkOboServiceConfig.java +++ b/symphony-bdk-spring/symphony-bdk-core-spring-boot-starter/src/main/java/com/symphony/bdk/spring/config/BdkOboServiceConfig.java @@ -27,7 +27,6 @@ import com.symphony.bdk.gen.api.SessionApi; import com.symphony.bdk.gen.api.ShareApi; import com.symphony.bdk.gen.api.SignalsApi; -import com.symphony.bdk.gen.api.StreamContextApi; import com.symphony.bdk.gen.api.StreamsApi; import com.symphony.bdk.gen.api.UserApi; import com.symphony.bdk.gen.api.UsersApi; @@ -77,8 +76,8 @@ public SessionService oboSessionService(SessionApi sessionApi, BdkConfig config) @Bean @ConditionalOnMissingBean public StreamService oboStreamService(StreamsApi streamsApi, RoomMembershipApi roomMembershipApi, ShareApi shareApi, - StreamContextApi streamContextApi, BdkConfig config) { - return new StreamService(streamsApi, roomMembershipApi, shareApi, streamContextApi, new RetryWithRecoveryBuilder<>().retryConfig(config.getRetry())); + MessagesApi messagesApi, BdkConfig config) { + return new StreamService(streamsApi, roomMembershipApi, shareApi, messagesApi, new RetryWithRecoveryBuilder<>().retryConfig(config.getRetry())); } @Bean diff --git a/symphony-bdk-spring/symphony-bdk-core-spring-boot-starter/src/main/java/com/symphony/bdk/spring/config/BdkServiceConfig.java b/symphony-bdk-spring/symphony-bdk-core-spring-boot-starter/src/main/java/com/symphony/bdk/spring/config/BdkServiceConfig.java index 7f94d67bb..415122450 100644 --- a/symphony-bdk-spring/symphony-bdk-core-spring-boot-starter/src/main/java/com/symphony/bdk/spring/config/BdkServiceConfig.java +++ b/symphony-bdk-spring/symphony-bdk-core-spring-boot-starter/src/main/java/com/symphony/bdk/spring/config/BdkServiceConfig.java @@ -31,7 +31,6 @@ import com.symphony.bdk.gen.api.SessionApi; import com.symphony.bdk.gen.api.ShareApi; import com.symphony.bdk.gen.api.SignalsApi; -import com.symphony.bdk.gen.api.StreamContextApi; import com.symphony.bdk.gen.api.StreamsApi; import com.symphony.bdk.gen.api.SystemApi; import com.symphony.bdk.gen.api.UserApi; @@ -62,9 +61,9 @@ public SessionService sessionService(SessionApi sessionApi, AuthSession botSessi @Bean @ConditionalOnMissingBean - public StreamService streamService(StreamsApi streamsApi, RoomMembershipApi roomMembershipApi, ShareApi shareApi, StreamContextApi streamContextApi, + public StreamService streamService(StreamsApi streamsApi, RoomMembershipApi roomMembershipApi, ShareApi shareApi, MessagesApi messagesApi, AuthSession botSession, BdkConfig config) { - return new StreamService(streamsApi, roomMembershipApi, shareApi, streamContextApi, botSession, new RetryWithRecoveryBuilder<>().retryConfig(config.getRetry())); + return new StreamService(streamsApi, roomMembershipApi, shareApi, messagesApi, botSession, new RetryWithRecoveryBuilder<>().retryConfig(config.getRetry())); } @Bean From 419475ac3bb6e57597af68b438b11dcfb9ea82d5 Mon Sep 17 00:00:00 2001 From: jameshanna-symphony Date: Wed, 21 Oct 2026 10:18:58 +0100 Subject: [PATCH 16/17] Add openspec --- .../client-data-service/.openspec.yaml | 2 + .../changes/client-data-service/design.md | 28 ++++++++++ .../changes/client-data-service/proposal.md | 53 +++++++++++++++++++ .../specs/client-data-service/spec.md | 44 +++++++++++++++ openspec/changes/client-data-service/tasks.md | 26 +++++++++ openspec/specs/client-data-service/spec.md | 49 +++++++++++++++++ 6 files changed, 202 insertions(+) create mode 100644 openspec/changes/client-data-service/.openspec.yaml create mode 100644 openspec/changes/client-data-service/design.md create mode 100644 openspec/changes/client-data-service/proposal.md create mode 100644 openspec/changes/client-data-service/specs/client-data-service/spec.md create mode 100644 openspec/changes/client-data-service/tasks.md create mode 100644 openspec/specs/client-data-service/spec.md diff --git a/openspec/changes/client-data-service/.openspec.yaml b/openspec/changes/client-data-service/.openspec.yaml new file mode 100644 index 000000000..01660bab0 --- /dev/null +++ b/openspec/changes/client-data-service/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-10-08 diff --git a/openspec/changes/client-data-service/design.md b/openspec/changes/client-data-service/design.md new file mode 100644 index 000000000..926f9bdea --- /dev/null +++ b/openspec/changes/client-data-service/design.md @@ -0,0 +1,28 @@ +## Context + +The `GET /v5/users/current/clientdata` endpoint returns personal client data for the authenticated user, aggregated across custom folders, multi-chat workspaces, and distribution lists for application `symphonyPrime`. + +This design introduces a first-class `ClientDataService` and its OBO counterpart `OboClientDataService` in `symphony-bdk-core`, wired into `ServiceFactory`, `SymphonyBdk`, `OboServices`, and Spring Boot auto-configurations. + +## Goals / Non-Goals + +**Goals:** +- Provide a first-class service for retrieving client data (`ClientDataResponse`), folders, workspaces, and distribution lists. +- Support both bot service account sessions and On-Behalf-Of (OBO) user sessions. +- Provide automatic retry with recovery on 401 unauthorized (token refresh) via `RetryWithRecoveryBuilder`. +- Provide Spring Boot starter auto-configurations for bot and OBO modes. +- Align with upstream FINOS `symphony-api-spec` updates (`MessagesApi` replacing `StreamContextApi`). + +**Non-Goals:** +- Modifying or updating client data (endpoint is read-only). +- Creating or managing distribution list recipients. + +## Decisions + +**D1 — Package Structure.** The service is placed in its own dedicated package, `com.symphony.bdk.core.service.clientdata`, alongside other functional service packages (`stream`, `message`, `user`, etc.). It includes `package-info.java` annotated with `@NullMarked`. + +**D2 — Service Interface and Ergonomics.** Following BDK conventions (e.g., `UserService` and `OboUserService`), `ClientDataService` implements `OboClientDataService` and `OboService`. In addition to the full aggregated `getClientData()` method, convenience methods (`getUserFolders()`, `getUserWorkspaces()`, `getUserDistributionLists()`) extract sub-components directly. + +**D3 — OBO Support.** Calling `.obo(AuthSession)` on `ClientDataService` creates a new instance scoped to the user session, while maintaining the same retry policies. `OboServices.clientData()` provides entry-point access. + +**D4 — Upstream MessagesApi Alignment.** OpenAPI specification commit `a0b152881a0405b7510cfc7e3a9698e8cb8c566c` on FINOS includes PR #238, which changed the OpenAPI tag for `/v1/stream/{sid}/context` from `StreamContext` to `Messages`. Consequently, `StreamContextApi` was removed by OpenAPI generator and its operation consolidated into `MessagesApi`. `StreamService`, `ServiceFactory`, and Spring starter configurations are updated to use `MessagesApi` directly. diff --git a/openspec/changes/client-data-service/proposal.md b/openspec/changes/client-data-service/proposal.md new file mode 100644 index 000000000..6e6fdfe51 --- /dev/null +++ b/openspec/changes/client-data-service/proposal.md @@ -0,0 +1,53 @@ +## Why + +Symphony users organize personal workflows and conversation spaces in the client through custom folders (grouping conversation streams), multi-chat workspaces, and distribution lists. Previously, the BDK Java SDK had no service to access this client-side organization data, requiring bots or extension applications to either manage state externally or call underlying REST endpoints without BDK retry, auth session, or pagination support. + +With the addition of the `GET /v5/users/current/clientdata` endpoint in SBE Users and its formal specification in `symphony-api-spec`, BDK Java needs a first-class service to expose this data cleanly to bots and OBO user sessions. + +## What Changes + +- **New package `com.symphony.bdk.core.service.clientdata`** containing: + - `ClientDataService`: Service implementing `OboClientDataService` and `OboService` with automatic retry and 401 session token recovery. + - `OboClientDataService`: Service interface exposing OBO-enabled client data methods. + - `package-info.java`: Annotated with `@NullMarked`. +- **Client Data Methods**: + - `getClientData()`: Retrieves aggregated client data (`ClientDataResponse`) containing folders, workspaces, and distribution lists for application `symphonyPrime`. + - `getUserFolders()`: Convenience accessor returning list of `ClientDataStructure` folders. + - `getUserWorkspaces()`: Convenience accessor returning list of `ClientDataStructure` workspaces. + - `getUserDistributionLists()`: Convenience accessor returning list of `DistributionLists`. +- **BDK Entry Point Access**: + - `SymphonyBdk.clientData()` and `SymphonyBdk.getClientDataService()` accessors. + - `OboServices.clientData()` and `OboServices.getClientDataService()` accessors. + - `ServiceFactory.getClientDataService()` factory method. +- **Spring Boot Starter Configuration**: + - `@Bean clientDataService(...)` auto-configured in `BdkServiceConfig`. + - `@Bean oboClientDataService(...)` auto-configured in `BdkOboServiceConfig`. +- **OpenAPI Specification Bump & Upstream Alignment**: + - Bumped `apiBaseUrl` in `symphony-bdk-core/build.gradle` to FINOS `symphony-api-spec` commit `a0b152881a0405b7510cfc7e3a9698e8cb8c566c`. + - Aligned with upstream PR #238 (`BE-10206`): migrated `StreamService`, `ServiceFactory`, and Spring starter configurations from `StreamContextApi` to `MessagesApi` following the `/v1/stream/{sid}/context` OpenAPI tag update. +- **Testing**: + - Comprehensive unit test coverage in `ClientDataServiceTest` covering successful responses, empty responses, recipient details, OBO execution, and unauthorized state exceptions. + - Updated `StreamServiceTest`, `ServiceFactoryTest`, and `OboServicesTest`. + +## Capabilities + +### New Capabilities +- `client-data-service`: Access to the authenticated user's personal client data structures (custom folders, workspaces, and distribution lists) via bot and OBO sessions. + +### Modified Capabilities +*(none)* + +## Impact + +**Code**: +- New service package `com.symphony.bdk.core.service.clientdata` +- `SymphonyBdk.java`, `OboServices.java`, `ServiceFactory.java`: added client data accessors +- `StreamService.java`, `ServiceFactory.java`: updated to use `MessagesApi` +- `BdkServiceConfig.java`, `BdkOboServiceConfig.java`, `BdkApiClientsConfig.java`: Spring starter bean configurations updated +- `symphony-bdk-core/build.gradle`: bumped `apiBaseUrl` + +**Dependencies**: +- No new external runtime dependencies. + +**Docs**: +- Updated documentation and capability specs. diff --git a/openspec/changes/client-data-service/specs/client-data-service/spec.md b/openspec/changes/client-data-service/specs/client-data-service/spec.md new file mode 100644 index 000000000..e07090a5d --- /dev/null +++ b/openspec/changes/client-data-service/specs/client-data-service/spec.md @@ -0,0 +1,44 @@ +## ADDED Requirements + +### Requirement: Client Data Retrieval +`ClientDataService` and `OboClientDataService` SHALL retrieve the authenticated user's aggregated client data from the `GET /v5/users/current/clientdata` endpoint, returning a `ClientDataResponse` containing the user's folders, workspaces, and distribution lists for application `symphonyPrime`. + +#### Scenario: Successful client data retrieval +- **WHEN** `getClientData()` is called with an active bot or OBO session +- **THEN** the client data endpoint is queried and a populated `ClientDataResponse` is returned + +#### Scenario: Unauthenticated invocation +- **WHEN** `getClientData()` is called on an unauthenticated service instance without an active `AuthSession` +- **THEN** an `IllegalStateException` is thrown before any network call is initiated + +### Requirement: Folders and Workspaces Access +`ClientDataService` and `OboClientDataService` SHALL provide convenience accessors `getUserFolders()` and `getUserWorkspaces()` that return lists of `ClientDataStructure` elements from the aggregated client data. + +#### Scenario: Retrieve user folders +- **WHEN** `getUserFolders()` is called +- **THEN** the list of `ClientDataStructure` folders is returned, each containing `id`, `name`, and conversation `streamIds` + +#### Scenario: Retrieve user workspaces +- **WHEN** `getUserWorkspaces()` is called +- **THEN** the list of `ClientDataStructure` workspaces is returned, each containing `id`, `name`, and multi-chat `streamIds` + +### Requirement: Distribution Lists Access +`ClientDataService` and `OboClientDataService` SHALL provide a convenience accessor `getUserDistributionLists()` returning the list of `DistributionLists` from the aggregated client data, each containing recipients of type user, stream, or folder. + +#### Scenario: Retrieve distribution lists +- **WHEN** `getUserDistributionLists()` is called +- **THEN** the list of `DistributionLists` is returned with recipient IDs, types, user names, and folder stream IDs + +### Requirement: OBO Session Support +`ClientDataService` SHALL implement `OboService` such that calling `.obo(AuthSession)` produces an instance scoped to the specified user session, using the user session token for authentication. + +#### Scenario: OBO execution +- **WHEN** `.obo(oboSession)` is invoked with an OBO auth session +- **THEN** subsequent calls execute against `/v5/users/current/clientdata` using the OBO session token + +### Requirement: BDK Entry Point Availability +`SymphonyBdk` and `OboServices` SHALL expose `clientData()` and `getClientDataService()` accessors returning `ClientDataService` and `OboClientDataService` respectively. `ServiceFactory` SHALL provide `getClientDataService()`. + +#### Scenario: Entry point retrieval +- **WHEN** `bdk.clientData()` or `oboServices.clientData()` is invoked +- **THEN** a fully initialized client data service is returned diff --git a/openspec/changes/client-data-service/tasks.md b/openspec/changes/client-data-service/tasks.md new file mode 100644 index 000000000..bf7a87da8 --- /dev/null +++ b/openspec/changes/client-data-service/tasks.md @@ -0,0 +1,26 @@ +## 1. OpenAPI Specification Bump & Code Generation + +- [x] 1.1 Bump `apiBaseUrl` in `symphony-bdk-core/build.gradle` to FINOS `symphony-api-spec` commit `a0b152881a0405b7510cfc7e3a9698e8cb8c566c` +- [x] 1.2 Generate OpenAPI client and models: `ClientDataApi`, `ClientDataResponse`, `ClientDataStructure`, `DistributionLists`, `DistributionListRecipient` +- [x] 1.3 Align `StreamService` and `ServiceFactory` with `MessagesApi` replacing `StreamContextApi` + +## 2. Core Service Implementation + +- [x] 2.1 Create `com.symphony.bdk.core.service.clientdata.OboClientDataService` interface declaring `getClientData()`, `getUserFolders()`, `getUserWorkspaces()`, `getUserDistributionLists()` +- [x] 2.2 Create `com.symphony.bdk.core.service.clientdata.ClientDataService` implementing `OboClientDataService` and `OboService` with `RetryWithRecoveryBuilder` +- [x] 2.3 Add `getClientDataService()` factory method to `ServiceFactory` +- [x] 2.4 Add `clientData()` and `getClientDataService()` accessors to `SymphonyBdk` +- [x] 2.5 Add `clientData()` and `getClientDataService()` accessors to `OboServices` +- [x] 2.6 Add `package-info.java` annotated with `@NullMarked` + +## 3. Spring Boot Auto-Configuration + +- [x] 3.1 Register `@Bean clientDataService` in `BdkServiceConfig` +- [x] 3.2 Register `@Bean oboClientDataService` in `BdkOboServiceConfig` +- [x] 3.3 Update `BdkApiClientsConfig`, `BdkServiceConfig`, and `BdkOboServiceConfig` for `MessagesApi` + +## 4. Testing & Verification + +- [x] 4.1 Unit tests in `ClientDataServiceTest` covering `getClientData`, `getUserFolders`, `getUserWorkspaces`, `getUserDistributionLists`, empty response, OBO mode, and unauthenticated errors +- [x] 4.2 Update `StreamServiceTest`, `ServiceFactoryTest`, and `OboServicesTest` +- [x] 4.3 Run full Gradle build, checks, and test coverage verification diff --git a/openspec/specs/client-data-service/spec.md b/openspec/specs/client-data-service/spec.md new file mode 100644 index 000000000..6d183b02d --- /dev/null +++ b/openspec/specs/client-data-service/spec.md @@ -0,0 +1,49 @@ +# client-data-service Specification + +## Purpose +Provides access to personal client data (custom folders, multi-chat workspaces, and distribution lists) for authenticated bot and On-Behalf-Of (OBO) user sessions via the `GET /v5/users/current/clientdata` Symphony endpoint. + +## Requirements + +### Requirement: Client Data Retrieval +`ClientDataService` and `OboClientDataService` SHALL retrieve the authenticated user's aggregated client data from the `GET /v5/users/current/clientdata` endpoint, returning a `ClientDataResponse` containing the user's folders, workspaces, and distribution lists for application `symphonyPrime`. + +#### Scenario: Successful client data retrieval +- **WHEN** `getClientData()` is called with an active bot or OBO session +- **THEN** the client data endpoint is queried and a populated `ClientDataResponse` is returned + +#### Scenario: Unauthenticated invocation +- **WHEN** `getClientData()` is called on an unauthenticated service instance without an active `AuthSession` +- **THEN** an `IllegalStateException` is thrown before any network call is initiated + +### Requirement: Folders and Workspaces Access +`ClientDataService` and `OboClientDataService` SHALL provide convenience accessors `getUserFolders()` and `getUserWorkspaces()` that return lists of `ClientDataStructure` elements from the aggregated client data. + +#### Scenario: Retrieve user folders +- **WHEN** `getUserFolders()` is called +- **THEN** the list of `ClientDataStructure` folders is returned, each containing `id`, `name`, and conversation `streamIds` + +#### Scenario: Retrieve user workspaces +- **WHEN** `getUserWorkspaces()` is called +- **THEN** the list of `ClientDataStructure` workspaces is returned, each containing `id`, `name`, and multi-chat `streamIds` + +### Requirement: Distribution Lists Access +`ClientDataService` and `OboClientDataService` SHALL provide a convenience accessor `getUserDistributionLists()` returning the list of `DistributionLists` from the aggregated client data, each containing recipients of type user, stream, or folder. + +#### Scenario: Retrieve distribution lists +- **WHEN** `getUserDistributionLists()` is called +- **THEN** the list of `DistributionLists` is returned with recipient IDs, types, user names, and folder stream IDs + +### Requirement: OBO Session Support +`ClientDataService` SHALL implement `OboService` such that calling `.obo(AuthSession)` produces an instance scoped to the specified user session, using the user session token for authentication. + +#### Scenario: OBO execution +- **WHEN** `.obo(oboSession)` is invoked with an OBO auth session +- **THEN** subsequent calls execute against `/v5/users/current/clientdata` using the OBO session token + +### Requirement: BDK Entry Point Availability +`SymphonyBdk` and `OboServices` SHALL expose `clientData()` and `getClientDataService()` accessors returning `ClientDataService` and `OboClientDataService` respectively. `ServiceFactory` SHALL provide `getClientDataService()`. + +#### Scenario: Entry point retrieval +- **WHEN** `bdk.clientData()` or `oboServices.clientData()` is invoked +- **THEN** a fully initialized client data service is returned From 4ae1a189375f85e005b0fadb3f9504efef1bc3b6 Mon Sep 17 00:00:00 2001 From: jameshanna-symphony Date: Wed, 21 Oct 2026 10:23:07 +0100 Subject: [PATCH 17/17] Archieve spec --- .../2026-10-21-client-data-service}/.openspec.yaml | 0 .../2026-10-21-client-data-service}/design.md | 0 .../2026-10-21-client-data-service}/proposal.md | 0 .../specs/client-data-service/spec.md | 0 .../2026-10-21-client-data-service}/tasks.md | 0 5 files changed, 0 insertions(+), 0 deletions(-) rename openspec/changes/{client-data-service => archive/2026-10-21-client-data-service}/.openspec.yaml (100%) rename openspec/changes/{client-data-service => archive/2026-10-21-client-data-service}/design.md (100%) rename openspec/changes/{client-data-service => archive/2026-10-21-client-data-service}/proposal.md (100%) rename openspec/changes/{client-data-service => archive/2026-10-21-client-data-service}/specs/client-data-service/spec.md (100%) rename openspec/changes/{client-data-service => archive/2026-10-21-client-data-service}/tasks.md (100%) diff --git a/openspec/changes/client-data-service/.openspec.yaml b/openspec/changes/archive/2026-10-21-client-data-service/.openspec.yaml similarity index 100% rename from openspec/changes/client-data-service/.openspec.yaml rename to openspec/changes/archive/2026-10-21-client-data-service/.openspec.yaml diff --git a/openspec/changes/client-data-service/design.md b/openspec/changes/archive/2026-10-21-client-data-service/design.md similarity index 100% rename from openspec/changes/client-data-service/design.md rename to openspec/changes/archive/2026-10-21-client-data-service/design.md diff --git a/openspec/changes/client-data-service/proposal.md b/openspec/changes/archive/2026-10-21-client-data-service/proposal.md similarity index 100% rename from openspec/changes/client-data-service/proposal.md rename to openspec/changes/archive/2026-10-21-client-data-service/proposal.md diff --git a/openspec/changes/client-data-service/specs/client-data-service/spec.md b/openspec/changes/archive/2026-10-21-client-data-service/specs/client-data-service/spec.md similarity index 100% rename from openspec/changes/client-data-service/specs/client-data-service/spec.md rename to openspec/changes/archive/2026-10-21-client-data-service/specs/client-data-service/spec.md diff --git a/openspec/changes/client-data-service/tasks.md b/openspec/changes/archive/2026-10-21-client-data-service/tasks.md similarity index 100% rename from openspec/changes/client-data-service/tasks.md rename to openspec/changes/archive/2026-10-21-client-data-service/tasks.md