diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle
index a1a4e47b7..121fcb961 100644
--- a/buildSrc/build.gradle
+++ b/buildSrc/build.gradle
@@ -7,7 +7,7 @@ repositories {
}
dependencies {
- implementation 'org.openapitools:openapi-generator-gradle-plugin:5.4.0'
+ implementation 'org.openapitools:openapi-generator-gradle-plugin:6.6.0'
implementation 'de.undercouch:gradle-download-task:5.0.2'
implementation 'com.github.ben-manes:gradle-versions-plugin:0.42.0'
}
diff --git a/symphony-bdk-core/build.gradle b/symphony-bdk-core/build.gradle
index 337d05ecc..0bbbbaabc 100644
--- a/symphony-bdk-core/build.gradle
+++ b/symphony-bdk-core/build.gradle
@@ -76,13 +76,14 @@ dependencies {
}
// OpenAPI code generation
-def apiBaseUrl = "https://raw.githubusercontent.com/finos/symphony-api-spec/fc80c3204d8a92a0b82d3c951eab7f5cb78a7c53"
+def apiBaseUrl = "https://raw.githubusercontent.com/tzhao-symphony/symphony-api-spec/refs/heads/ADMIN-10454/add_app_users_endpoint/"
def generatedFolder = "$buildDir/generated/openapi"
def apisToGenerate = [
Agent: 'agent/agent-api-public-deprecated.yaml',
Pod : 'pod/pod-api-public-deprecated.yaml',
Auth : 'authenticator/authenticator-api-public-deprecated.yaml',
Login: 'login/login-api-public.yaml',
+ Users: 'users/users-api-public.yaml'
]
sourceSets.main.java.srcDirs += "$generatedFolder/src/main/java"
diff --git a/symphony-bdk-core/src/main/java/com/symphony/bdk/core/ExtAppServiceFactory.java b/symphony-bdk-core/src/main/java/com/symphony/bdk/core/ExtAppServiceFactory.java
new file mode 100644
index 000000000..09915a1d2
--- /dev/null
+++ b/symphony-bdk-core/src/main/java/com/symphony/bdk/core/ExtAppServiceFactory.java
@@ -0,0 +1,46 @@
+package com.symphony.bdk.core;
+
+import com.symphony.bdk.core.auth.ExtAppAuthSession;
+import com.symphony.bdk.core.client.ApiClientFactory;
+import com.symphony.bdk.core.config.model.BdkConfig;
+import com.symphony.bdk.core.retry.RetryWithRecoveryBuilder;
+import com.symphony.bdk.core.service.app.AppUsersService;
+import com.symphony.bdk.gen.api.AppsApi;
+import com.symphony.bdk.http.api.ApiClient;
+
+import lombok.extern.slf4j.Slf4j;
+import org.apiguardian.api.API;
+
+/**
+ * Factory responsible for creating Ext App service instances for Symphony Bdk apps entry point:
+ *
+ * - {@link AppUsersService}
+ *
+ */
+@Slf4j
+@API(status = API.Status.INTERNAL)
+class ExtAppServiceFactory {
+
+ private final ApiClient usersClient;
+ private final ExtAppAuthSession authSession;
+ private final BdkConfig config;
+ private final RetryWithRecoveryBuilder> retryBuilder;
+
+ public ExtAppServiceFactory(ApiClientFactory apiClientFactory, ExtAppAuthSession authSession, BdkConfig config) {
+ this.config = config;
+ this.usersClient = apiClientFactory.getUsersClient();
+ this.authSession = authSession;
+ this.retryBuilder = new RetryWithRecoveryBuilder<>().retryConfig(config.getRetry());
+
+
+ }
+
+ /**
+ * Returns a fully initialized {@link AppUsersService}.
+ *
+ * @return a new {@link AppUsersService} instance.
+ */
+ public AppUsersService getAppService() {
+ return new AppUsersService(config.getApp().getAppId(), new AppsApi(usersClient), this.authSession, this.retryBuilder);
+ }
+}
diff --git a/symphony-bdk-core/src/main/java/com/symphony/bdk/core/ExtAppServices.java b/symphony-bdk-core/src/main/java/com/symphony/bdk/core/ExtAppServices.java
new file mode 100644
index 000000000..e3dc68982
--- /dev/null
+++ b/symphony-bdk-core/src/main/java/com/symphony/bdk/core/ExtAppServices.java
@@ -0,0 +1,30 @@
+package com.symphony.bdk.core;
+
+import com.symphony.bdk.core.auth.ExtAppAuthSession;
+import com.symphony.bdk.core.client.ApiClientFactory;
+import com.symphony.bdk.core.config.model.BdkConfig;
+import com.symphony.bdk.core.service.app.AppUsersService;
+
+import org.apiguardian.api.API;
+
+/**
+ * Entry point for external application services relying on the App session token
+ */
+@API(status = API.Status.STABLE)
+public class ExtAppServices {
+ AppUsersService appUsersService;
+
+ public ExtAppServices(ApiClientFactory apiClientFactory, ExtAppAuthSession authSession, BdkConfig config) {
+ ExtAppServiceFactory extAppServiceFactory = new ExtAppServiceFactory(apiClientFactory, authSession, config);
+ this.appUsersService = extAppServiceFactory.getAppService();
+ }
+
+ /**
+ * Get the {@link AppUsersService}.
+ *
+ * @return an {@link AppUsersService} instance.
+ */
+ public AppUsersService appUsers() {
+ return this.appUsersService;
+ };
+}
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 076d35693..776f11708 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
@@ -48,6 +48,7 @@ public class SymphonyBdk {
private final ExtensionAppAuthenticator extensionAppAuthenticator;
private final AuthSession botSession;
+ private final ExtAppAuthSession extAppAuthSession;
private final UserV2 botInfo;
private final DatafeedLoop datafeedLoop;
private final DatahoseLoop datahoseLoop;
@@ -64,6 +65,9 @@ public class SymphonyBdk {
private final HealthService healthService;
private final ExtensionService extensionService;
+ private final ExtAppServices extAppServices;
+
+
/**
* Returns a new {@link SymphonyBdkBuilder} for fluent initialization.
*
@@ -128,6 +132,15 @@ protected SymphonyBdk(
this.messageService = serviceFactory != null ? serviceFactory.getMessageService() : null;
this.disclaimerService = serviceFactory != null ? serviceFactory.getDisclaimerService() : null;
+ if (config.isOboConfigured()) {
+ ExtAppAuthenticator extAppAuthenticator = authenticatorFactory.getExtAppAuthenticator();
+ this.extAppAuthSession = extAppAuthenticator.authenticateExtApp();
+ this.extAppServices = new ExtAppServices(apiClientFactory, this.extAppAuthSession, this.config);
+ } else {
+ this.extAppServices = null;
+ this.extAppAuthSession = null;
+ }
+
// retrieve bot session info
this.botInfo = sessionService != null ? sessionService.getSession() : null;
@@ -304,6 +317,14 @@ public OboServices obo(AuthSession oboSession) {
return new OboServices(config, oboSession);
}
+ /**
+ * Get an {@link ExtAppServices} gathering all extension app enabled services
+ * @return an {@link ExtAppServices} instance
+ */
+ public ExtAppServices app() {
+ return this.extAppServices;
+ }
+
/**
* Returns the {@link ExtensionAppAuthenticator}.
*
@@ -313,6 +334,17 @@ public ExtensionAppAuthenticator appAuthenticator() {
return this.getExtensionAppAuthenticator();
}
+ /**
+ * Returns the extension app auth session.
+ *
+ * @return extension app auth session.
+ */
+ @API(status = API.Status.EXPERIMENTAL)
+ public ExtAppAuthSession extAppAuthSession() {
+ return Optional.ofNullable(this.extAppAuthSession)
+ .orElseThrow(() -> new IllegalStateException("Cannot get App auth session. Ext app is not configured."));
+ }
+
/**
* Returns the Bot session.
*
diff --git a/symphony-bdk-core/src/main/java/com/symphony/bdk/core/auth/AuthenticatorFactory.java b/symphony-bdk-core/src/main/java/com/symphony/bdk/core/auth/AuthenticatorFactory.java
index 5ee0e53f3..e97b2a3a2 100644
--- a/symphony-bdk-core/src/main/java/com/symphony/bdk/core/auth/AuthenticatorFactory.java
+++ b/symphony-bdk-core/src/main/java/com/symphony/bdk/core/auth/AuthenticatorFactory.java
@@ -43,4 +43,13 @@ public interface AuthenticatorFactory {
*/
@Nonnull
ExtensionAppAuthenticator getExtensionAppAuthenticator() throws AuthInitializationException;
+
+ /**
+ * Creates a new instance of a {@link ExtAppAuthenticator}.
+ *
+ * @return a new {@link ExtAppAuthenticator} instance.
+ * @throws AuthInitializationException if the authenticator cannot be instantiated.
+ */
+ @Nonnull
+ ExtAppAuthenticator getExtAppAuthenticator() throws AuthInitializationException;
}
diff --git a/symphony-bdk-core/src/main/java/com/symphony/bdk/core/auth/ExtAppAuthSession.java b/symphony-bdk-core/src/main/java/com/symphony/bdk/core/auth/ExtAppAuthSession.java
new file mode 100644
index 000000000..6a275990a
--- /dev/null
+++ b/symphony-bdk-core/src/main/java/com/symphony/bdk/core/auth/ExtAppAuthSession.java
@@ -0,0 +1,26 @@
+package com.symphony.bdk.core.auth;
+
+import com.symphony.bdk.core.auth.exception.AuthUnauthorizedException;
+
+import org.apiguardian.api.API;
+
+import javax.annotation.Nullable;
+
+/**
+ * Extension App Authentication session handle. The {@link ExtAppAuthSession#refresh()} will trigger a re-auth against the API endpoints.
+ */
+@API(status = API.Status.STABLE)
+public interface ExtAppAuthSession {
+ /**
+ * Extension app session token.
+ *
+ * @return extension app session token
+ */
+ @Nullable
+ String getAppSession();
+
+ /**
+ * Trigger re-authentication to refresh session token.
+ */
+ void refresh() throws AuthUnauthorizedException;
+}
diff --git a/symphony-bdk-core/src/main/java/com/symphony/bdk/core/auth/ExtAppAuthenticator.java b/symphony-bdk-core/src/main/java/com/symphony/bdk/core/auth/ExtAppAuthenticator.java
new file mode 100644
index 000000000..b342d2868
--- /dev/null
+++ b/symphony-bdk-core/src/main/java/com/symphony/bdk/core/auth/ExtAppAuthenticator.java
@@ -0,0 +1,20 @@
+package com.symphony.bdk.core.auth;
+
+import com.symphony.bdk.core.auth.exception.AuthUnauthorizedException;
+import org.apiguardian.api.API;
+
+import javax.annotation.Nonnull;
+
+/**
+ * Extension App authenticator service.
+ */
+@API(status = API.Status.STABLE)
+public interface ExtAppAuthenticator {
+
+ /**
+ * Authenticates an extension app.
+ *
+ * @return the authentication session.
+ */
+ @Nonnull ExtAppAuthSession authenticateExtApp() throws AuthUnauthorizedException;
+}
diff --git a/symphony-bdk-core/src/main/java/com/symphony/bdk/core/auth/impl/AbstractExtAppAuthenticator.java b/symphony-bdk-core/src/main/java/com/symphony/bdk/core/auth/impl/AbstractExtAppAuthenticator.java
new file mode 100644
index 000000000..53e4b5ffb
--- /dev/null
+++ b/symphony-bdk-core/src/main/java/com/symphony/bdk/core/auth/impl/AbstractExtAppAuthenticator.java
@@ -0,0 +1,41 @@
+package com.symphony.bdk.core.auth.impl;
+
+import com.symphony.bdk.core.auth.ExtAppAuthenticator;
+import com.symphony.bdk.core.auth.OboAuthenticator;
+import com.symphony.bdk.core.auth.exception.AuthUnauthorizedException;
+import com.symphony.bdk.core.config.model.BdkRetryConfig;
+import com.symphony.bdk.http.api.ApiException;
+
+import lombok.extern.slf4j.Slf4j;
+import org.apiguardian.api.API;
+
+/**
+ * Abstract class to factorize the {@link OboAuthenticator} logic between RSA and certificate,
+ * especially the retry logic on top of HTTP calls.
+ */
+@Slf4j
+@API(status = API.Status.INTERNAL)
+public abstract class AbstractExtAppAuthenticator implements ExtAppAuthenticator {
+
+ protected final String appId;
+ private final AuthenticationRetry authenticationRetry;
+
+ protected AbstractExtAppAuthenticator(BdkRetryConfig retryConfig, String appId) {
+ this.appId = appId;
+ this.authenticationRetry = new AuthenticationRetry<>(retryConfig);
+ }
+
+ protected String retrieveAppSessionToken() throws AuthUnauthorizedException {
+ log.debug("Start authenticating app with id : {} ...", appId);
+
+ final String unauthorizedErrorMessage = "Unable to authenticate app with ID : " + appId + ". "
+ + "It usually happens when the app has not been configured or is not activated.";
+
+ return authenticationRetry.executeAndRetry("AbstractExtAppAuthenticator.retrieveAppSessionToken", getBasePath(),
+ this::authenticateAndRetrieveAppSessionToken, unauthorizedErrorMessage);
+ }
+
+ protected abstract String authenticateAndRetrieveAppSessionToken() throws ApiException;
+
+ protected abstract String getBasePath();
+}
diff --git a/symphony-bdk-core/src/main/java/com/symphony/bdk/core/auth/impl/AuthenticatorFactoryImpl.java b/symphony-bdk-core/src/main/java/com/symphony/bdk/core/auth/impl/AuthenticatorFactoryImpl.java
index 5e18bb5aa..92a61920b 100644
--- a/symphony-bdk-core/src/main/java/com/symphony/bdk/core/auth/impl/AuthenticatorFactoryImpl.java
+++ b/symphony-bdk-core/src/main/java/com/symphony/bdk/core/auth/impl/AuthenticatorFactoryImpl.java
@@ -179,6 +179,43 @@ ExtensionAppAuthenticator getExtensionAppAuthenticator() throws AuthInitializati
throw new AuthInitializationException("Neither RSA private key nor certificate is configured.");
}
+ /**
+ * Creates a new instance of a {@link ExtAppAuthenticator} service.
+ *
+ * @return a new {@link ExtAppAuthenticator} instance.
+ */
+ @Nonnull
+ @Override
+ public ExtAppAuthenticator getExtAppAuthenticator() throws AuthInitializationException {
+ if (this.config.getApp().isBothCertificateAndRsaConfigured()) {
+ throw new AuthInitializationException(
+ "Both of certificate and rsa authentication are configured. Only one of them should be provided.");
+ }
+ if (this.config.getApp().isCertificateAuthenticationConfigured()) {
+ if (!this.config.getApp().isCertificateConfigurationValid()) {
+ throw new AuthInitializationException(
+ "Only one of certificate path or content should be configured for app authentication.");
+ }
+ return new ExtAppAuthenticatorCertImpl(
+ this.config.getRetry(),
+ this.config.getApp().getAppId(),
+ this.apiClientFactory.getExtAppSessionAuthClient());
+ }
+ if (this.config.getApp().isRsaAuthenticationConfigured()) {
+ if (!this.config.getApp().isRsaConfigurationValid()) {
+ throw new AuthInitializationException(
+ "Only one of private key path or content should be configured for app authentication.");
+ }
+ return new ExtAppAuthenticatorRsaImpl(
+ this.config.getRetry(),
+ this.config.getApp().getAppId(),
+ this.loadPrivateKeyFromAuthenticationConfig(this.config.getApp()),
+ this.apiClientFactory.getLoginClient()
+ );
+ }
+ throw new AuthInitializationException("Neither RSA private key nor certificate is configured.");
+ }
+
private PrivateKey loadPrivateKeyFromAuthenticationConfig(BdkAuthenticationConfig config)
throws AuthInitializationException {
String privateKeyPath = "";
diff --git a/symphony-bdk-core/src/main/java/com/symphony/bdk/core/auth/impl/ExtAppAuthSessionCertImpl.java b/symphony-bdk-core/src/main/java/com/symphony/bdk/core/auth/impl/ExtAppAuthSessionCertImpl.java
new file mode 100644
index 000000000..9d806e0b9
--- /dev/null
+++ b/symphony-bdk-core/src/main/java/com/symphony/bdk/core/auth/impl/ExtAppAuthSessionCertImpl.java
@@ -0,0 +1,39 @@
+package com.symphony.bdk.core.auth.impl;
+
+import com.symphony.bdk.core.auth.ExtAppAuthSession;
+import com.symphony.bdk.core.auth.exception.AuthUnauthorizedException;
+
+import org.apiguardian.api.API;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * {@link ExtAppAuthSession} impl for Extension App Certificate authentication mode.
+ */
+@API(status = API.Status.INTERNAL)
+public class ExtAppAuthSessionCertImpl implements ExtAppAuthSession {
+
+ String appSession;
+ ExtAppAuthenticatorCertImpl authenticator;
+
+ public ExtAppAuthSessionCertImpl(ExtAppAuthenticatorCertImpl authenticator) {
+ this.authenticator = authenticator;
+ }
+
+ @Nullable
+ @Override
+ public String getAppSession() {
+ return appSession;
+ }
+
+ @Override
+ public void refresh() throws AuthUnauthorizedException {
+ this.appSession = this.authenticator.retrieveAppSessionToken();
+ }
+
+ /**
+ * This method is only visible for testing.
+ */
+ protected ExtAppAuthenticatorCertImpl getAuthenticator() {
+ return authenticator;
+ }
+}
diff --git a/symphony-bdk-core/src/main/java/com/symphony/bdk/core/auth/impl/ExtAppAuthSessionImpl.java b/symphony-bdk-core/src/main/java/com/symphony/bdk/core/auth/impl/ExtAppAuthSessionImpl.java
new file mode 100644
index 000000000..70eb332e7
--- /dev/null
+++ b/symphony-bdk-core/src/main/java/com/symphony/bdk/core/auth/impl/ExtAppAuthSessionImpl.java
@@ -0,0 +1,38 @@
+package com.symphony.bdk.core.auth.impl;
+
+import com.symphony.bdk.core.auth.ExtAppAuthSession;
+import com.symphony.bdk.core.auth.exception.AuthUnauthorizedException;
+
+import org.apiguardian.api.API;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * {@link ExtAppAuthSession} impl for Extension App RSA authentication mode.
+ */
+@API(status = API.Status.INTERNAL)
+public class ExtAppAuthSessionImpl implements ExtAppAuthSession {
+ ExtAppAuthenticatorRsaImpl authenticator;
+ String appSession;
+
+ public ExtAppAuthSessionImpl(ExtAppAuthenticatorRsaImpl authenticator) {
+ this.authenticator = authenticator;
+ }
+
+ @Nullable
+ @Override
+ public String getAppSession() {
+ return appSession;
+ }
+
+ @Override
+ public void refresh() throws AuthUnauthorizedException {
+ this.appSession = this.authenticator.retrieveAppSessionToken();
+ }
+
+ /**
+ * This method is only visible for testing.
+ */
+ protected ExtAppAuthenticatorRsaImpl getAuthenticator() {
+ return authenticator;
+ }
+}
diff --git a/symphony-bdk-core/src/main/java/com/symphony/bdk/core/auth/impl/ExtAppAuthenticatorCertImpl.java b/symphony-bdk-core/src/main/java/com/symphony/bdk/core/auth/impl/ExtAppAuthenticatorCertImpl.java
new file mode 100644
index 000000000..123726323
--- /dev/null
+++ b/symphony-bdk-core/src/main/java/com/symphony/bdk/core/auth/impl/ExtAppAuthenticatorCertImpl.java
@@ -0,0 +1,42 @@
+package com.symphony.bdk.core.auth.impl;
+
+import com.symphony.bdk.core.auth.ExtAppAuthSession;
+import com.symphony.bdk.core.auth.exception.AuthUnauthorizedException;
+
+import com.symphony.bdk.core.config.model.BdkRetryConfig;
+import com.symphony.bdk.gen.api.CertificateAuthenticationApi;
+import com.symphony.bdk.http.api.ApiClient;
+import com.symphony.bdk.http.api.ApiException;
+
+import org.apiguardian.api.API;
+import org.jetbrains.annotations.NotNull;
+
+@API(status = API.Status.INTERNAL)
+public class ExtAppAuthenticatorCertImpl extends AbstractExtAppAuthenticator {
+ private final CertificateAuthenticationApi authenticationApi;
+
+ public ExtAppAuthenticatorCertImpl(BdkRetryConfig retryConfig,
+ String appId,
+ ApiClient loginApiClient) {
+ super(retryConfig, appId);
+ this.authenticationApi = new CertificateAuthenticationApi(loginApiClient);
+ }
+
+ @NotNull
+ @Override
+ public ExtAppAuthSession authenticateExtApp() throws AuthUnauthorizedException {
+ ExtAppAuthSession session = new ExtAppAuthSessionCertImpl(this);
+ session.refresh();
+ return session;
+ }
+
+ @Override
+ protected String authenticateAndRetrieveAppSessionToken() throws ApiException {
+ return this.authenticationApi.v1AppAuthenticatePost().getToken();
+ }
+
+ @Override
+ protected String getBasePath() {
+ return authenticationApi.getApiClient().getBasePath();
+ }
+}
diff --git a/symphony-bdk-core/src/main/java/com/symphony/bdk/core/auth/impl/ExtAppAuthenticatorRsaImpl.java b/symphony-bdk-core/src/main/java/com/symphony/bdk/core/auth/impl/ExtAppAuthenticatorRsaImpl.java
new file mode 100644
index 000000000..d8fbbc5b5
--- /dev/null
+++ b/symphony-bdk-core/src/main/java/com/symphony/bdk/core/auth/impl/ExtAppAuthenticatorRsaImpl.java
@@ -0,0 +1,55 @@
+package com.symphony.bdk.core.auth.impl;
+
+import com.symphony.bdk.core.auth.ExtAppAuthSession;
+import com.symphony.bdk.core.auth.exception.AuthUnauthorizedException;
+
+import com.symphony.bdk.core.auth.jwt.JwtHelper;
+import com.symphony.bdk.core.config.model.BdkRetryConfig;
+
+import com.symphony.bdk.gen.api.AuthenticationApi;
+import com.symphony.bdk.gen.api.model.AuthenticateRequest;
+import com.symphony.bdk.http.api.ApiClient;
+import com.symphony.bdk.http.api.ApiException;
+
+import org.apiguardian.api.API;
+import org.jetbrains.annotations.NotNull;
+
+import java.security.PrivateKey;
+
+@API(status = API.Status.INTERNAL)
+public class ExtAppAuthenticatorRsaImpl extends AbstractExtAppAuthenticator {
+
+ private final AuthenticationApi authenticationApi;
+ private final PrivateKey appPrivateKey;
+
+ public ExtAppAuthenticatorRsaImpl(BdkRetryConfig retryConfig,
+ String appId,
+ PrivateKey appPrivateKey,
+ ApiClient loginApiClient) {
+ super(retryConfig, appId);
+ this.appPrivateKey = appPrivateKey;
+ this.authenticationApi = new AuthenticationApi(loginApiClient);
+ }
+
+ @Override
+ protected String authenticateAndRetrieveAppSessionToken() throws ApiException {
+ final String jwt = JwtHelper.createSignedJwt(appId, JwtHelper.JWT_EXPIRATION_MILLIS, appPrivateKey);
+ final AuthenticateRequest req = new AuthenticateRequest();
+ req.setToken(jwt);
+
+ return this.authenticationApi.pubkeyAppAuthenticatePost(req).getToken();
+ }
+
+ @Override
+ protected String getBasePath() {
+ return authenticationApi.getApiClient().getBasePath();
+ }
+
+ @NotNull
+ @Override
+ public ExtAppAuthSession authenticateExtApp() throws AuthUnauthorizedException {
+ ExtAppAuthSessionImpl extAppAuthSession = new ExtAppAuthSessionImpl(this);
+ extAppAuthSession.refresh();
+ return extAppAuthSession;
+ }
+}
diff --git a/symphony-bdk-core/src/main/java/com/symphony/bdk/core/client/ApiClientFactory.java b/symphony-bdk-core/src/main/java/com/symphony/bdk/core/client/ApiClientFactory.java
index 977e7b499..5b5f50563 100644
--- a/symphony-bdk-core/src/main/java/com/symphony/bdk/core/client/ApiClientFactory.java
+++ b/symphony-bdk-core/src/main/java/com/symphony/bdk/core/client/ApiClientFactory.java
@@ -32,6 +32,7 @@
@API(status = API.Status.EXPERIMENTAL)
public class ApiClientFactory {
+ private static final String USERS_CONTEXT_PATH = "";
private static final String LOGIN_CONTEXT_PATH = "/login";
private static final String POD_CONTEXT_PATH = "/pod";
private static final String AGENT_CONTEXT_PATH = "/agent";
@@ -80,6 +81,10 @@ public ApiClient getPodClient(String contextPath) {
return buildClient(contextPath, this.config.getPod());
}
+ public ApiClient getUsersClient() {
+ return buildClient(USERS_CONTEXT_PATH, this.config.getPod());
+ }
+
/**
* Returns a fully initialized {@link ApiClient} for KeyManager API.
*
diff --git a/symphony-bdk-core/src/main/java/com/symphony/bdk/core/service/app/AppUsersService.java b/symphony-bdk-core/src/main/java/com/symphony/bdk/core/service/app/AppUsersService.java
new file mode 100644
index 000000000..d8d0bf581
--- /dev/null
+++ b/symphony-bdk-core/src/main/java/com/symphony/bdk/core/service/app/AppUsersService.java
@@ -0,0 +1,92 @@
+package com.symphony.bdk.core.service.app;
+
+import com.symphony.bdk.core.auth.ExtAppAuthSession;
+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.gen.api.AppsApi;
+import com.symphony.bdk.gen.api.model.AppUsersResponse;
+
+import com.symphony.bdk.http.api.ApiException;
+
+import org.apiguardian.api.API;
+
+import java.util.List;
+
+/**
+ * Service class for fetching app users.
+ *
+ * This service is used for retrieving information about users that have the application installed:
+ *
+ * - App product information
+ * - Whether the app is enabled
+ *
+ *
+ */
+@API(status = API.Status.STABLE)
+public class AppUsersService {
+
+ private final static Integer DEFAULT_PAGE_SIZE = 100;
+
+ private final AppsApi appsApi;
+ private final ExtAppAuthSession authSession;
+ private final RetryWithRecoveryBuilder> retryBuilder;
+ private final String appId;
+
+
+ public AppUsersService(String appId,
+ AppsApi appsApi,
+ ExtAppAuthSession authSession,
+ RetryWithRecoveryBuilder> retryBuilder) {
+ this.appsApi = appsApi;
+ this.authSession = authSession;
+ this.retryBuilder = RetryWithRecoveryBuilder.copyWithoutRecoveryStrategies(retryBuilder)
+ .recoveryStrategy(ApiException::isUnauthorized, authSession::refresh);;
+ this.appId = appId;
+ }
+
+ /**
+ * Get app users
+ * @param page to be returned
+ * @param size number of result per page
+ * @param sort sorting parameters
+ * @return {@link AppUsersResponse}
+ */
+ public AppUsersResponse listAppUsers(Integer page, Integer size, List sort) {
+ String session = authSession.getAppSession();
+ return executeAndRetry("listAppUsers", appsApi.getApiClient().getBasePath(),
+ () -> appsApi.findAppUsers(session, appId, true, page, size, sort));
+ }
+
+ /**
+ * Get app users
+ * @param page to be returned
+ * @param size number of result per page
+ * @return {@link AppUsersResponse}
+ */
+ public AppUsersResponse listAppUsers(Integer page, Integer size) {
+ return listAppUsers(page, size, null);
+ }
+
+ /**
+ * Get app users
+ * @param page to be returned
+ * @return {@link AppUsersResponse}
+ */
+ public AppUsersResponse listAppUsers(Integer page) {
+ return listAppUsers(page, DEFAULT_PAGE_SIZE, null);
+ }
+
+ /**
+ * Get app users
+ * @return {@link AppUsersResponse}
+ */
+ public AppUsersResponse listAppUsers() {
+ return listAppUsers(null, DEFAULT_PAGE_SIZE, null);
+ }
+
+ private T executeAndRetry(String name, String address, SupplierWithApiException supplier) {
+ return RetryWithRecovery.executeAndRetry(retryBuilder, name, address, supplier);
+ }
+}
diff --git a/symphony-bdk-core/src/test/java/com/symphony/bdk/core/ExtAppServiceFactoryTest.java b/symphony-bdk-core/src/test/java/com/symphony/bdk/core/ExtAppServiceFactoryTest.java
new file mode 100644
index 000000000..09a4582e0
--- /dev/null
+++ b/symphony-bdk-core/src/test/java/com/symphony/bdk/core/ExtAppServiceFactoryTest.java
@@ -0,0 +1,31 @@
+package com.symphony.bdk.core;
+
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import com.symphony.bdk.core.auth.ExtAppAuthSession;
+import com.symphony.bdk.core.client.ApiClientFactory;
+import com.symphony.bdk.core.config.model.BdkConfig;
+import com.symphony.bdk.core.service.app.AppUsersService;
+import com.symphony.bdk.http.api.ApiClient;
+
+import org.junit.jupiter.api.Test;
+
+class ExtAppServiceFactoryTest {
+
+ @Test
+ void testGetAppService() {
+ final ApiClientFactory apiClientFactory = mock(ApiClientFactory.class);
+ final ExtAppAuthSession authSession = mock(ExtAppAuthSession.class);
+ final BdkConfig config = new BdkConfig();
+ config.getApp().setAppId("testApp");
+
+ when(apiClientFactory.getUsersClient()).thenReturn(mock(ApiClient.class));
+
+ final ExtAppServiceFactory serviceFactory = new ExtAppServiceFactory(apiClientFactory, authSession, config);
+ final AppUsersService appUsersService = serviceFactory.getAppService();
+
+ assertNotNull(appUsersService);
+ }
+}
diff --git a/symphony-bdk-core/src/test/java/com/symphony/bdk/core/ExtAppServicesTest.java b/symphony-bdk-core/src/test/java/com/symphony/bdk/core/ExtAppServicesTest.java
new file mode 100644
index 000000000..564506e88
--- /dev/null
+++ b/symphony-bdk-core/src/test/java/com/symphony/bdk/core/ExtAppServicesTest.java
@@ -0,0 +1,33 @@
+package com.symphony.bdk.core;
+
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.mockito.Mockito.mock;
+
+import com.symphony.bdk.core.auth.ExtAppAuthSession;
+import com.symphony.bdk.core.client.ApiClientFactory;
+import com.symphony.bdk.core.config.model.BdkConfig;
+import com.symphony.bdk.core.config.model.BdkExtAppConfig;
+import com.symphony.bdk.core.service.app.AppUsersService;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+class ExtAppServicesTest {
+
+ private ExtAppServices extAppServices;
+
+ @BeforeEach
+ void setUp() {
+ final BdkConfig config = new BdkConfig();
+ config.setApp(new BdkExtAppConfig());
+ config.getApp().setAppId("test-app");
+
+ this.extAppServices = new ExtAppServices(mock(ApiClientFactory.class), mock(ExtAppAuthSession.class), config);
+ }
+
+ @Test
+ void testAppUsers() {
+ AppUsersService appUsersService = this.extAppServices.appUsers();
+ assertNotNull(appUsersService);
+ }
+}
diff --git a/symphony-bdk-core/src/test/java/com/symphony/bdk/core/SymphonyBdkTest.java b/symphony-bdk-core/src/test/java/com/symphony/bdk/core/SymphonyBdkTest.java
index 8a4a1a997..1d7eeeb3b 100644
--- a/symphony-bdk-core/src/test/java/com/symphony/bdk/core/SymphonyBdkTest.java
+++ b/symphony-bdk-core/src/test/java/com/symphony/bdk/core/SymphonyBdkTest.java
@@ -68,6 +68,7 @@ void setUp() throws BdkConfigException, AuthUnauthorizedException, AuthInitializ
this.mockApiClient.onPost(LOGIN_PUBKEY_AUTHENTICATE, "{ \"token\": \"1234\", \"name\": \"sessionToken\" }");
this.mockApiClient.onPost(RELAY_PUBKEY_AUTHENTICATE, "{ \"token\": \"1234\", \"name\": \"keyManagerToken\" }");
+ this.mockApiClient.onPost(LOGIN_PUBKEY_APP_AUTHENTICATE, "{ \"token\": \"1234\", \"name\": \"sessionToken\" }");
this.mockApiClient.onGet(V2_SESSION_INFO, JsonHelper.readFromClasspath("/res_response/bot_info.json"));
this.symphonyBdk = SymphonyBdk.builder()
@@ -197,6 +198,17 @@ void extAppAuthenticateTest() throws AuthUnauthorizedException {
assertEquals(authSession.expireAt(), 1539636528288L);
}
+ @Test
+ void getAppServices() {
+ assertNotNull(this.symphonyBdk.app());
+ assertNotNull(this.symphonyBdk.app().appUsers());
+ }
+
+ @Test
+ void getAppSession() {
+ assertNotNull(this.symphonyBdk.extAppAuthSession());
+ }
+
@Test
void botSessionTest() {
assertNotNull(this.symphonyBdk.botSession());
diff --git a/symphony-bdk-core/src/test/java/com/symphony/bdk/core/auth/AuthenticatorFactoryTest.java b/symphony-bdk-core/src/test/java/com/symphony/bdk/core/auth/AuthenticatorFactoryTest.java
index 5c1b6e1b2..2331a3d1e 100644
--- a/symphony-bdk-core/src/test/java/com/symphony/bdk/core/auth/AuthenticatorFactoryTest.java
+++ b/symphony-bdk-core/src/test/java/com/symphony/bdk/core/auth/AuthenticatorFactoryTest.java
@@ -10,6 +10,8 @@
import com.symphony.bdk.core.auth.impl.AuthenticatorFactoryImpl;
import com.symphony.bdk.core.auth.impl.BotAuthenticatorCertImpl;
import com.symphony.bdk.core.auth.impl.BotAuthenticatorRsaImpl;
+import com.symphony.bdk.core.auth.impl.ExtAppAuthenticatorCertImpl;
+import com.symphony.bdk.core.auth.impl.ExtAppAuthenticatorRsaImpl;
import com.symphony.bdk.core.auth.impl.ExtensionAppAuthenticatorCertImpl;
import com.symphony.bdk.core.auth.impl.ExtensionAppAuthenticatorRsaImpl;
import com.symphony.bdk.core.auth.impl.OboAuthenticatorCertImpl;
@@ -52,6 +54,7 @@ public static void setup() {
when(DUMMY_API_CLIENT_FACTORY.getRelayClient()).thenReturn(DUMMY_API_CLIENT);
when(DUMMY_API_CLIENT_FACTORY.getSessionAuthClient()).thenReturn(DUMMY_API_CLIENT);
when(DUMMY_API_CLIENT_FACTORY.getKeyAuthClient()).thenReturn(DUMMY_API_CLIENT);
+ when(DUMMY_API_CLIENT_FACTORY.getExtAppSessionAuthClient()).thenReturn(DUMMY_API_CLIENT);
}
@Test
@@ -211,6 +214,33 @@ void testGetExtAppAuthenticatorWithValidCertificatePath(@TempDir Path tempDir) t
assertEquals(ExtensionAppAuthenticatorCertImpl.class, extAppAuthenticator.getClass());
}
+ @Test
+ void testGetNewExtAppAuthenticatorWithValidPrivateKey(@TempDir Path tempDir) throws AuthInitializationException {
+
+ final BdkConfig config = createRsaConfig(() -> {
+ final Path privateKeyPath = tempDir.resolve(UUID.randomUUID().toString() + "-privateKey.pem");
+ writeContentToPath(RSA_PRIVATE_KEY, privateKeyPath);
+ return privateKeyPath.toAbsolutePath().toString();
+ });
+
+ final AuthenticatorFactory factory = new AuthenticatorFactoryImpl(config, DUMMY_API_CLIENT_FACTORY);
+ final ExtAppAuthenticator extAppAuth = factory.getExtAppAuthenticator();
+
+ assertEquals(ExtAppAuthenticatorRsaImpl.class, extAppAuth.getClass());
+ }
+
+ @Test
+ void testGetNewExtAppAuthenticatorWithValidCertificatePath() throws AuthInitializationException {
+ final BdkConfig config = new BdkConfig();
+ config.getApp().getCertificate().setPath("/path/to/cert/file.p12");
+ config.getApp().getCertificate().setPassword("password");
+
+ final AuthenticatorFactory factory = new AuthenticatorFactoryImpl(config, DUMMY_API_CLIENT_FACTORY);
+ final ExtAppAuthenticator extAppAuthenticator = factory.getExtAppAuthenticator();
+
+ assertEquals(ExtAppAuthenticatorCertImpl.class, extAppAuthenticator.getClass());
+ }
+
@Test
void testGetAuthenticatorWithBothCertificatePathAndContentConfigured() {
final BdkConfig config = new BdkConfig();
@@ -225,6 +255,7 @@ void testGetAuthenticatorWithBothCertificatePathAndContentConfigured() {
assertThrows(AuthInitializationException.class, factory::getBotAuthenticator);
assertThrows(AuthInitializationException.class, factory::getExtensionAppAuthenticator);
assertThrows(AuthInitializationException.class, factory::getOboAuthenticator);
+ assertThrows(AuthInitializationException.class, factory::getExtAppAuthenticator);
}
@Test
@@ -239,6 +270,7 @@ void testGetAuthenticatorWithBothPrivateKeyPathAndContentConfigured() {
assertThrows(AuthInitializationException.class, factory::getBotAuthenticator);
assertThrows(AuthInitializationException.class, factory::getExtensionAppAuthenticator);
assertThrows(AuthInitializationException.class, factory::getOboAuthenticator);
+ assertThrows(AuthInitializationException.class, factory::getExtAppAuthenticator);
}
@Test
@@ -255,6 +287,20 @@ void testGetExtAppAuthenticatorWithInvalidPrivateKey(@TempDir Path tempDir) {
assertThrows(AuthInitializationException.class, factory::getExtensionAppAuthenticator);
}
+ @Test
+ void testGetNewExtAppAuthenticatorWithInvalidPrivateKey(@TempDir Path tempDir) {
+
+ final BdkConfig config = createRsaConfig(() -> {
+ final Path privateKeyPath = tempDir.resolve(UUID.randomUUID().toString() + "-privateKey.pem");
+ writeContentToPath("invalid-private-key-content", privateKeyPath);
+ return privateKeyPath.toAbsolutePath().toString();
+ });
+
+ final AuthenticatorFactory factory = new AuthenticatorFactoryImpl(config, DUMMY_API_CLIENT_FACTORY);
+
+ assertThrows(AuthInitializationException.class, factory::getExtAppAuthenticator);
+ }
+
@Test
void testGetExtAppAuthenticatorWithNotFoundPrivateKey(@TempDir Path tempDir) {
@@ -265,6 +311,16 @@ void testGetExtAppAuthenticatorWithNotFoundPrivateKey(@TempDir Path tempDir) {
assertThrows(AuthInitializationException.class, factory::getExtensionAppAuthenticator);
}
+ @Test
+ void testGetNewExtAppAuthenticatorWithNotFoundPrivateKey(@TempDir Path tempDir) {
+
+ final BdkConfig config = createRsaConfig(() -> tempDir.resolve(UUID.randomUUID().toString() + "-privateKey.pem").toAbsolutePath().toString());
+
+ final AuthenticatorFactory factory = new AuthenticatorFactoryImpl(config, DUMMY_API_CLIENT_FACTORY);
+
+ assertThrows(AuthInitializationException.class, factory::getExtAppAuthenticator);
+ }
+
@Test
void testGetAuthenticationRsaAndCertificateNotConfigured() {
@@ -275,6 +331,7 @@ void testGetAuthenticationRsaAndCertificateNotConfigured() {
assertThrows(AuthInitializationException.class, factory::getBotAuthenticator);
assertThrows(AuthInitializationException.class, factory::getOboAuthenticator);
assertThrows(AuthInitializationException.class, factory::getExtensionAppAuthenticator);
+ assertThrows(AuthInitializationException.class, factory::getExtAppAuthenticator);
}
@Test
@@ -308,6 +365,7 @@ void testGetAppAuthenticatorBothRsaAndCertificateConfigured(@TempDir Path tempDi
assertThrows(AuthInitializationException.class, factory::getExtensionAppAuthenticator);
assertThrows(AuthInitializationException.class, factory::getOboAuthenticator);
+ assertThrows(AuthInitializationException.class, factory::getExtAppAuthenticator);
}
@Test
@@ -327,6 +385,7 @@ void testGetAuthenticatorRsaInvalid(@TempDir Path tempDir) {
assertThrows(AuthInitializationException.class, factory::getBotAuthenticator);
assertThrows(AuthInitializationException.class, factory::getExtensionAppAuthenticator);
assertThrows(AuthInitializationException.class, factory::getOboAuthenticator);
+ assertThrows(AuthInitializationException.class, factory::getExtAppAuthenticator);
}
@Test
diff --git a/symphony-bdk-core/src/test/java/com/symphony/bdk/core/auth/impl/AbstractExtAppAuthenticatorTest.java b/symphony-bdk-core/src/test/java/com/symphony/bdk/core/auth/impl/AbstractExtAppAuthenticatorTest.java
new file mode 100644
index 000000000..b0e86635b
--- /dev/null
+++ b/symphony-bdk-core/src/test/java/com/symphony/bdk/core/auth/impl/AbstractExtAppAuthenticatorTest.java
@@ -0,0 +1,98 @@
+package com.symphony.bdk.core.auth.impl;
+
+import static com.symphony.bdk.core.test.BdkRetryConfigTestHelper.ofMinimalInterval;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import com.symphony.bdk.core.auth.ExtAppAuthSession;
+import com.symphony.bdk.core.auth.exception.AuthUnauthorizedException;
+import com.symphony.bdk.core.config.model.BdkRetryConfig;
+import com.symphony.bdk.http.api.ApiException;
+import com.symphony.bdk.http.api.ApiRuntimeException;
+
+import jakarta.ws.rs.ProcessingException;
+import org.jetbrains.annotations.NotNull;
+import org.junit.jupiter.api.Test;
+
+import java.net.ConnectException;
+
+class AbstractExtAppAuthenticatorTest {
+
+ private static class TestExtAppAuthenticator extends AbstractExtAppAuthenticator {
+ public TestExtAppAuthenticator(BdkRetryConfig retryConfig) {
+ super(retryConfig, "appId");
+ }
+
+ @Override
+ protected String authenticateAndRetrieveAppSessionToken() throws ApiException {
+ return null;
+ }
+
+ @Override
+ protected String getBasePath() {
+ return "localhost.symphony.com";
+ }
+
+ @NotNull
+ @Override
+ public ExtAppAuthSession authenticateExtApp() throws AuthUnauthorizedException {
+ return null;
+ }
+ }
+
+ @Test
+ void testRetrieveAppSessionTokenSuccess() throws ApiException, AuthUnauthorizedException {
+ final String appSessionToken = "appSessionToken";
+ final AbstractExtAppAuthenticator authenticator = spy(new TestExtAppAuthenticator(ofMinimalInterval()));
+ doReturn(appSessionToken).when(authenticator).authenticateAndRetrieveAppSessionToken();
+
+ assertEquals(appSessionToken, authenticator.retrieveAppSessionToken());
+ verify(authenticator, times(1)).authenticateAndRetrieveAppSessionToken();
+ }
+
+ @Test
+ void testRetrieveAppSessionTokenShouldRetry() throws ApiException, AuthUnauthorizedException {
+ final String appSessionToken = "appSessionToken";
+ final AbstractExtAppAuthenticator authenticator = spy(new TestExtAppAuthenticator(ofMinimalInterval()));
+ doThrow(new ApiException(429, ""))
+ .doThrow(new ApiException(503, ""))
+ .doThrow(new ProcessingException(new ConnectException()))
+ .doReturn(appSessionToken)
+ .when(authenticator).authenticateAndRetrieveAppSessionToken();
+
+ assertEquals(appSessionToken, authenticator.retrieveAppSessionToken());
+ verify(authenticator, times(4)).authenticateAndRetrieveAppSessionToken();
+ }
+
+ @Test
+ void testRetrieveAppSessionTokenRetriesExhausted() throws ApiException {
+ final AbstractExtAppAuthenticator authenticator = spy(new TestExtAppAuthenticator(ofMinimalInterval(2)));
+ doThrow(new ApiException(503, "")).when(authenticator).authenticateAndRetrieveAppSessionToken();
+
+ assertThrows(ApiRuntimeException.class, authenticator::retrieveAppSessionToken);
+ verify(authenticator, times(2)).authenticateAndRetrieveAppSessionToken();
+ }
+
+ @Test
+ void testRetrieveAppSessionTokenUnauthorized() throws ApiException {
+ final AbstractExtAppAuthenticator authenticator = spy(new TestExtAppAuthenticator(ofMinimalInterval()));
+ doThrow(new ApiException(401, "")).when(authenticator).authenticateAndRetrieveAppSessionToken();
+
+ assertThrows(AuthUnauthorizedException.class, authenticator::retrieveAppSessionToken);
+ verify(authenticator, times(1)).authenticateAndRetrieveAppSessionToken();
+ }
+
+ @Test
+ void testRetrieveAppSessionTokenUnexpectedApiException() throws ApiException {
+ final AbstractExtAppAuthenticator authenticator = spy(new TestExtAppAuthenticator(ofMinimalInterval()));
+ doThrow(new ApiException(404, "")).when(authenticator).authenticateAndRetrieveAppSessionToken();
+
+ assertThrows(ApiRuntimeException.class, authenticator::retrieveAppSessionToken);
+ verify(authenticator, times(1)).authenticateAndRetrieveAppSessionToken();
+ }
+}
diff --git a/symphony-bdk-core/src/test/java/com/symphony/bdk/core/auth/impl/ExtAppAuthSessionCertImplTest.java b/symphony-bdk-core/src/test/java/com/symphony/bdk/core/auth/impl/ExtAppAuthSessionCertImplTest.java
new file mode 100644
index 000000000..3ac49acd2
--- /dev/null
+++ b/symphony-bdk-core/src/test/java/com/symphony/bdk/core/auth/impl/ExtAppAuthSessionCertImplTest.java
@@ -0,0 +1,34 @@
+package com.symphony.bdk.core.auth.impl;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import com.symphony.bdk.core.auth.exception.AuthUnauthorizedException;
+
+import org.junit.jupiter.api.Test;
+
+class ExtAppAuthSessionCertImplTest {
+
+ @Test
+ void testRefresh() throws AuthUnauthorizedException {
+ final ExtAppAuthenticatorCertImpl authenticator = mock(ExtAppAuthenticatorCertImpl.class);
+ final ExtAppAuthSessionCertImpl session = new ExtAppAuthSessionCertImpl(authenticator);
+
+ when(authenticator.retrieveAppSessionToken()).thenReturn("appSessionToken");
+
+ session.refresh();
+ assertEquals("appSessionToken", session.getAppSession());
+ }
+
+ @Test
+ void testRefreshAuthUnauthorized() throws AuthUnauthorizedException {
+ final ExtAppAuthenticatorCertImpl authenticator = mock(ExtAppAuthenticatorCertImpl.class);
+ final ExtAppAuthSessionCertImpl session = new ExtAppAuthSessionCertImpl(authenticator);
+
+ when(authenticator.retrieveAppSessionToken()).thenThrow(new AuthUnauthorizedException(""));
+
+ assertThrows(AuthUnauthorizedException.class, session::refresh);
+ }
+}
diff --git a/symphony-bdk-core/src/test/java/com/symphony/bdk/core/auth/impl/ExtAppAuthSessionImplTest.java b/symphony-bdk-core/src/test/java/com/symphony/bdk/core/auth/impl/ExtAppAuthSessionImplTest.java
new file mode 100644
index 000000000..7a42f0418
--- /dev/null
+++ b/symphony-bdk-core/src/test/java/com/symphony/bdk/core/auth/impl/ExtAppAuthSessionImplTest.java
@@ -0,0 +1,34 @@
+package com.symphony.bdk.core.auth.impl;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import com.symphony.bdk.core.auth.exception.AuthUnauthorizedException;
+
+import org.junit.jupiter.api.Test;
+
+class ExtAppAuthSessionImplTest {
+
+ @Test
+ void testRefresh() throws AuthUnauthorizedException {
+ final ExtAppAuthenticatorRsaImpl authenticator = mock(ExtAppAuthenticatorRsaImpl.class);
+ final ExtAppAuthSessionImpl session = new ExtAppAuthSessionImpl(authenticator);
+
+ when(authenticator.retrieveAppSessionToken()).thenReturn("appSessionToken");
+
+ session.refresh();
+ assertEquals("appSessionToken", session.getAppSession());
+ }
+
+ @Test
+ void testRefreshAuthUnauthorized() throws AuthUnauthorizedException {
+ final ExtAppAuthenticatorRsaImpl authenticator = mock(ExtAppAuthenticatorRsaImpl.class);
+ final ExtAppAuthSessionImpl session = new ExtAppAuthSessionImpl(authenticator);
+
+ when(authenticator.retrieveAppSessionToken()).thenThrow(new AuthUnauthorizedException(""));
+
+ assertThrows(AuthUnauthorizedException.class, session::refresh);
+ }
+}
diff --git a/symphony-bdk-core/src/test/java/com/symphony/bdk/core/auth/impl/ExtAppAuthenticatorCertImplTest.java b/symphony-bdk-core/src/test/java/com/symphony/bdk/core/auth/impl/ExtAppAuthenticatorCertImplTest.java
new file mode 100644
index 000000000..e845a3e01
--- /dev/null
+++ b/symphony-bdk-core/src/test/java/com/symphony/bdk/core/auth/impl/ExtAppAuthenticatorCertImplTest.java
@@ -0,0 +1,63 @@
+package com.symphony.bdk.core.auth.impl;
+
+import static com.symphony.bdk.core.test.BdkRetryConfigTestHelper.ofMinimalInterval;
+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 com.symphony.bdk.core.auth.ExtAppAuthSession;
+import com.symphony.bdk.core.auth.exception.AuthUnauthorizedException;
+import com.symphony.bdk.core.test.BdkMockServer;
+import com.symphony.bdk.core.test.BdkMockServerExtension;
+import com.symphony.bdk.http.api.ApiRuntimeException;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+
+@ExtendWith(BdkMockServerExtension.class)
+class ExtAppAuthenticatorCertImplTest {
+
+ private ExtAppAuthenticatorCertImpl authenticator;
+
+ @BeforeEach
+ void init(final BdkMockServer mockServer) {
+ this.authenticator = new ExtAppAuthenticatorCertImpl(
+ ofMinimalInterval(1),
+ "appId",
+ mockServer.newApiClient("/login")
+ );
+ }
+
+ @Test
+ void testAuthenticateExtApp(final BdkMockServer mockServer) throws AuthUnauthorizedException {
+ mockServer.onPost("/login/v1/app/authenticate", res -> res.withBody("{ \"token\": \"1234\" }"));
+
+ final ExtAppAuthSession session = this.authenticator.authenticateExtApp();
+ assertNotNull(session);
+ assertEquals(ExtAppAuthSessionCertImpl.class, session.getClass());
+ assertEquals(this.authenticator, ((ExtAppAuthSessionCertImpl) session).getAuthenticator());
+ }
+
+ @Test
+ void testRetrieveAppSessionToken(final BdkMockServer mockServer) throws AuthUnauthorizedException {
+ mockServer.onPost("/login/v1/app/authenticate", res -> res.withBody("{ \"token\": \"1234\" }"));
+
+ final String appSessionToken = this.authenticator.retrieveAppSessionToken();
+ assertEquals("1234", appSessionToken);
+ }
+
+ @Test
+ void testAuthUnauthorizedException(final BdkMockServer mockServer) {
+ mockServer.onPost("/login/v1/app/authenticate", res -> res.withStatusCode(401));
+
+ assertThrows(AuthUnauthorizedException.class, () -> this.authenticator.retrieveAppSessionToken());
+ }
+
+ @Test
+ void testUnknownApiException(final BdkMockServer mockServer) {
+ mockServer.onPost("/login/v1/app/authenticate", res -> res.withStatusCode(503));
+
+ assertThrows(ApiRuntimeException.class, () -> this.authenticator.retrieveAppSessionToken());
+ }
+}
diff --git a/symphony-bdk-core/src/test/java/com/symphony/bdk/core/auth/impl/ExtAppAuthenticatorRsaImplTest.java b/symphony-bdk-core/src/test/java/com/symphony/bdk/core/auth/impl/ExtAppAuthenticatorRsaImplTest.java
new file mode 100644
index 000000000..24fc4cc4f
--- /dev/null
+++ b/symphony-bdk-core/src/test/java/com/symphony/bdk/core/auth/impl/ExtAppAuthenticatorRsaImplTest.java
@@ -0,0 +1,69 @@
+package com.symphony.bdk.core.auth.impl;
+
+import static com.symphony.bdk.core.test.BdkRetryConfigTestHelper.ofMinimalInterval;
+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 com.symphony.bdk.core.auth.ExtAppAuthSession;
+import com.symphony.bdk.core.auth.exception.AuthUnauthorizedException;
+import com.symphony.bdk.core.test.BdkMockServer;
+import com.symphony.bdk.core.test.BdkMockServerExtension;
+import com.symphony.bdk.core.test.RsaTestHelper;
+import com.symphony.bdk.http.api.ApiRuntimeException;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+
+import java.security.PrivateKey;
+
+@ExtendWith(BdkMockServerExtension.class)
+class ExtAppAuthenticatorRsaImplTest {
+
+ private static final PrivateKey PRIVATE_KEY = RsaTestHelper.generateKeyPair().getPrivate();
+
+ private ExtAppAuthenticatorRsaImpl authenticator;
+
+ @BeforeEach
+ void init(final BdkMockServer mockServer) {
+ this.authenticator = new ExtAppAuthenticatorRsaImpl(
+ ofMinimalInterval(1),
+ "appId",
+ PRIVATE_KEY,
+ mockServer.newApiClient("/login")
+ );
+ }
+
+ @Test
+ void testAuthenticateExtApp(final BdkMockServer mockServer) throws AuthUnauthorizedException {
+ mockServer.onPost("/login/pubkey/app/authenticate", res -> res.withBody("{ \"token\": \"1234\" }"));
+
+ final ExtAppAuthSession session = this.authenticator.authenticateExtApp();
+ assertNotNull(session);
+ assertEquals(ExtAppAuthSessionImpl.class, session.getClass());
+ assertEquals(this.authenticator, ((ExtAppAuthSessionImpl) session).getAuthenticator());
+ }
+
+ @Test
+ void testRetrieveAppSessionToken(final BdkMockServer mockServer) throws AuthUnauthorizedException {
+ mockServer.onPost("/login/pubkey/app/authenticate", res -> res.withBody("{ \"token\": \"1234\" }"));
+
+ final String appSessionToken = this.authenticator.retrieveAppSessionToken();
+ assertEquals("1234", appSessionToken);
+ }
+
+ @Test
+ void testAuthUnauthorizedException(final BdkMockServer mockServer) {
+ mockServer.onPost("/login/pubkey/app/authenticate", res -> res.withStatusCode(401));
+
+ assertThrows(AuthUnauthorizedException.class, () -> this.authenticator.retrieveAppSessionToken());
+ }
+
+ @Test
+ void testUnknownApiException(final BdkMockServer mockServer) {
+ mockServer.onPost("/login/pubkey/app/authenticate", res -> res.withStatusCode(503));
+
+ assertThrows(ApiRuntimeException.class, () -> this.authenticator.retrieveAppSessionToken());
+ }
+}
diff --git a/symphony-bdk-core/src/test/java/com/symphony/bdk/core/client/ApiClientFactoryTest.java b/symphony-bdk-core/src/test/java/com/symphony/bdk/core/client/ApiClientFactoryTest.java
index 92d8cb72f..4a6c2ab6a 100644
--- a/symphony-bdk-core/src/test/java/com/symphony/bdk/core/client/ApiClientFactoryTest.java
+++ b/symphony-bdk-core/src/test/java/com/symphony/bdk/core/client/ApiClientFactoryTest.java
@@ -39,6 +39,13 @@ void testGetLoginClient() {
assertEquals("https://pod-host:443/login", loginClient.getBasePath());
}
+ @Test
+ void testGetUsersClient() {
+ final ApiClient usersClient = this.factory.getUsersClient();
+ assertEquals(ApiClientJersey2.class, usersClient.getClass());
+ assertEquals("https://pod-host:443", usersClient.getBasePath());
+ }
+
@Test
void testGetRelayClient() {
final ApiClient relayClient = this.factory.getRelayClient();
diff --git a/symphony-bdk-core/src/test/java/com/symphony/bdk/core/service/app/AppUsersServiceTest.java b/symphony-bdk-core/src/test/java/com/symphony/bdk/core/service/app/AppUsersServiceTest.java
new file mode 100644
index 000000000..a78c1351c
--- /dev/null
+++ b/symphony-bdk-core/src/test/java/com/symphony/bdk/core/service/app/AppUsersServiceTest.java
@@ -0,0 +1,102 @@
+package com.symphony.bdk.core.service.app;
+
+import com.symphony.bdk.core.auth.AuthSession;
+import com.symphony.bdk.core.auth.ExtAppAuthSession;
+import com.symphony.bdk.core.retry.RetryWithRecoveryBuilder;
+import com.symphony.bdk.core.service.application.ApplicationService;
+import com.symphony.bdk.core.test.JsonHelper;
+import com.symphony.bdk.core.test.MockApiClient;
+import com.symphony.bdk.gen.api.AppEntitlementApi;
+import com.symphony.bdk.gen.api.ApplicationApi;
+import com.symphony.bdk.gen.api.AppsApi;
+import com.symphony.bdk.gen.api.model.AppUsersResponse;
+import com.symphony.bdk.http.api.ApiClient;
+
+import com.symphony.bdk.http.api.ApiException;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.io.IOException;
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+public class AppUsersServiceTest {
+
+ private AppsApi appsApi;
+ private MockApiClient mockApiClient;
+ private AppUsersService service;
+
+ private static final String SESSION_TOKEN = "app-session-token";
+ private static final String APP_ID = "my_app-id";
+ private static final String LIST_APP_USERS_PATH = "/v5/users/apps/{appId}";
+
+ @BeforeEach
+ void init() {
+ this.mockApiClient = new MockApiClient();
+ ExtAppAuthSession authSession = mock(ExtAppAuthSession.class);
+ ApiClient podClient = mockApiClient.getApiClient("");
+ AppsApi applicationApi = new AppsApi(podClient);
+ this.appsApi = spy(applicationApi);
+ this.service = new AppUsersService(APP_ID, this.appsApi, authSession,
+ new RetryWithRecoveryBuilder<>());
+
+ when(authSession.getAppSession()).thenReturn(SESSION_TOKEN);
+ }
+
+ @Test
+ void listAppUsers() throws IOException {
+ mockApiClient.onGet(LIST_APP_USERS_PATH.replace("{appId}", APP_ID),
+ JsonHelper.readFromClasspath("/app/list_app_users.json"));
+ AppUsersResponse response = this.service.listAppUsers();
+ assertEquals(9, response.getUsers().size());
+ assertEquals("premium", response.getUsers().get(0).getProduct().getType());
+ assertEquals("default", response.getUsers().get(1).getProduct().getType());
+ assertEquals(9, response.getPage().getTotalElements());
+ assertEquals(1, response.getPage().getTotalPages());
+ assertEquals(100, response.getPage().getSize());
+ assertEquals(0, response.getPage().getNumber());
+ }
+
+ @Test
+ void listAppUsersWithPage() throws IOException, ApiException {
+ mockApiClient.onGet(LIST_APP_USERS_PATH.replace("{appId}", APP_ID),
+ JsonHelper.readFromClasspath("/app/list_app_users.json"));
+ Integer PAGE = 2;
+
+ this.service.listAppUsers(PAGE);
+
+ verify(this.appsApi).findAppUsers(eq(SESSION_TOKEN), eq(APP_ID), eq(true), eq(PAGE), eq(100), eq(null));
+ }
+
+ @Test
+ void listAppUsersWithPageAndSize() throws IOException, ApiException {
+ mockApiClient.onGet(LIST_APP_USERS_PATH.replace("{appId}", APP_ID),
+ JsonHelper.readFromClasspath("/app/list_app_users.json"));
+ Integer PAGE = 2;
+ Integer PAGE_SIZE = 10;
+
+ this.service.listAppUsers(PAGE, PAGE_SIZE);
+
+ verify(this.appsApi).findAppUsers(eq(SESSION_TOKEN), eq(APP_ID), eq(true), eq(PAGE), eq(PAGE_SIZE), eq(null));
+ }
+
+ @Test
+ void listAppUsersWithPageAndSizeAndSort() throws IOException, ApiException {
+ mockApiClient.onGet(LIST_APP_USERS_PATH.replace("{appId}", APP_ID),
+ JsonHelper.readFromClasspath("/app/list_app_users.json"));
+ Integer PAGE = 2;
+ Integer PAGE_SIZE = 10;
+ List SORT_PARAMETERS = List.of("userId");
+
+ this.service.listAppUsers(PAGE, PAGE_SIZE, SORT_PARAMETERS);
+
+ verify(this.appsApi).findAppUsers(eq(SESSION_TOKEN), eq(APP_ID), eq(true), eq(PAGE), eq(PAGE_SIZE), eq(SORT_PARAMETERS));
+ }
+}
diff --git a/symphony-bdk-core/src/test/resources/app/list_app_users.json b/symphony-bdk-core/src/test/resources/app/list_app_users.json
new file mode 100644
index 000000000..c232547e1
--- /dev/null
+++ b/symphony-bdk-core/src/test/resources/app/list_app_users.json
@@ -0,0 +1,110 @@
+{
+ "users": [
+ {
+ "userId": 82806969466881,
+ "enable": true,
+ "listed": false,
+ "product": {
+ "name": "premium",
+ "type": "premium",
+ "subscribed": true
+ }
+ },
+ {
+ "userId": 82806969466882,
+ "enable": true,
+ "listed": false,
+ "product": {
+ "name": "Standard",
+ "type": "default",
+ "subscribed": true
+ }
+ },
+ {
+ "userId": 82806969466883,
+ "enable": true,
+ "listed": false,
+ "product": {
+ "name": "Standard",
+ "type": "default",
+ "subscribed": true
+ }
+ },
+ {
+ "userId": 82806969466884,
+ "enable": true,
+ "listed": false,
+ "product": {
+ "name": "Standard",
+ "type": "default",
+ "subscribed": true
+ }
+ },
+ {
+ "userId": 82806969466885,
+ "enable": true,
+ "listed": false,
+ "product": {
+ "name": "Standard",
+ "type": "default",
+ "subscribed": true
+ }
+ },
+ {
+ "userId": 82806969466886,
+ "enable": true,
+ "listed": false,
+ "product": {
+ "name": "Standard",
+ "type": "default",
+ "subscribed": true
+ }
+ },
+ {
+ "userId": 82806969466887,
+ "enable": true,
+ "listed": false,
+ "product": {
+ "name": "Standard",
+ "type": "default",
+ "subscribed": true
+ }
+ },
+ {
+ "userId": 82806969466888,
+ "enable": true,
+ "listed": false,
+ "product": {
+ "name": "Standard",
+ "type": "default",
+ "subscribed": true
+ }
+ },
+ {
+ "userId": 82806969466889,
+ "enable": true,
+ "listed": false,
+ "product": {
+ "name": "Standard",
+ "type": "default",
+ "subscribed": true
+ }
+ }
+ ],
+ "page": {
+ "size": 100,
+ "number": 0,
+ "totalElements": 9,
+ "totalPages": 1,
+ "first": true,
+ "last": true,
+ "hasNext": false,
+ "hasPrevious": false,
+ "sort": [
+ {
+ "property": "userId",
+ "direction": "ASC"
+ }
+ ]
+ }
+}
diff --git a/symphony-bdk-extensions/symphony-group-extension/build.gradle b/symphony-bdk-extensions/symphony-group-extension/build.gradle
index fe4fab8a4..2384fe6ec 100644
--- a/symphony-bdk-extensions/symphony-group-extension/build.gradle
+++ b/symphony-bdk-extensions/symphony-group-extension/build.gradle
@@ -43,6 +43,11 @@ openApiGenerate {
inputSpec = "$buildDir/profile-manager-api.yaml"
apiPackage = 'com.symphony.bdk.ext.group.gen.api'
modelPackage = 'com.symphony.bdk.ext.group.gen.api.model'
+ globalProperties = [
+ models: "",
+ apis: "",
+ supportingFiles: "false"
+ ]
}
tasks.openApiGenerate.dependsOn tasks.downloadFile
diff --git a/templates/api.mustache b/templates/api.mustache
index 5f885b634..3807194ca 100644
--- a/templates/api.mustache
+++ b/templates/api.mustache
@@ -51,6 +51,11 @@ public class {{classname}} {
{{#allParams}}
* @param {{paramName}} {{description}}{{#required}} (required){{/required}}{{^required}} (optional{{#defaultValue}}, default to {{.}}{{/defaultValue}}){{/required}}
{{/allParams}}
+ {{#vendorExtensions.x-spring-paginated}}
+ * @param page Page number (0-based) (optional)
+ * @param size Number of records per page (optional)
+ * @param sort Sorting criteria in the format: property,asc|desc (optional)
+ {{/vendorExtensions.x-spring-paginated}}
{{#returnType}}
* @return {{returnType}}
{{/returnType}}
@@ -75,8 +80,8 @@ public class {{classname}} {
{{#isDeprecated}}
@Deprecated
{{/isDeprecated}}
- public {{#returnType}}{{{returnType}}} {{/returnType}}{{^returnType}}void {{/returnType}}{{operationId}}({{#allParams}}{{{dataType}}} {{paramName}}{{^-last}}, {{/-last}}{{/allParams}}) throws ApiException {
- {{#returnType}}return {{/returnType}}{{operationId}}WithHttpInfo({{#allParams}}{{paramName}}{{^-last}}, {{/-last}}{{/allParams}}){{#returnType}}.getData(){{/returnType}};
+ public {{#returnType}}{{{returnType}}} {{/returnType}}{{^returnType}}void {{/returnType}}{{operationId}}({{#allParams}}{{{dataType}}} {{paramName}}{{^-last}}, {{/-last}}{{/allParams}}{{#vendorExtensions.x-spring-paginated}}{{#hasParams}}, {{/hasParams}}Integer page, Integer size, java.util.List sort{{/vendorExtensions.x-spring-paginated}}) throws ApiException {
+ {{#returnType}}return {{/returnType}}{{operationId}}WithHttpInfo({{#allParams}}{{paramName}}{{^-last}}, {{/-last}}{{/allParams}}{{#vendorExtensions.x-spring-paginated}}{{#hasParams}}, {{/hasParams}}page, size, sort{{/vendorExtensions.x-spring-paginated}}){{#returnType}}.getData(){{/returnType}};
}
{{/vendorExtensions.x-group-parameters}}
@@ -87,6 +92,11 @@ public class {{classname}} {
{{#allParams}}
* @param {{paramName}} {{description}}{{#required}} (required){{/required}}{{^required}} (optional{{#defaultValue}}, default to {{.}}{{/defaultValue}}){{/required}}
{{/allParams}}
+ {{#vendorExtensions.x-spring-paginated}}
+ * @param page Page number (0-based) (optional)
+ * @param size Number of records per page (optional)
+ * @param sort Sorting criteria in the format: property,asc|desc (optional)
+ {{/vendorExtensions.x-spring-paginated}}
* @return ApiResponse<{{#returnType}}{{returnType}}{{/returnType}}{{^returnType}}Void{{/returnType}}>
* @throws ApiException if fails to make API call
{{#responses.0}}
@@ -109,7 +119,7 @@ public class {{classname}} {
{{#isDeprecated}}
@Deprecated
{{/isDeprecated}}
- public{{/vendorExtensions.x-group-parameters}}{{#vendorExtensions.x-group-parameters}}private{{/vendorExtensions.x-group-parameters}} ApiResponse<{{#returnType}}{{{returnType}}}{{/returnType}}{{^returnType}}Void{{/returnType}}> {{operationId}}WithHttpInfo({{#allParams}}{{{dataType}}} {{paramName}}{{^-last}}, {{/-last}}{{/allParams}}) throws ApiException {
+ public{{/vendorExtensions.x-group-parameters}}{{#vendorExtensions.x-group-parameters}}private{{/vendorExtensions.x-group-parameters}} ApiResponse<{{#returnType}}{{{returnType}}}{{/returnType}}{{^returnType}}Void{{/returnType}}> {{operationId}}WithHttpInfo({{#allParams}}{{{dataType}}} {{paramName}}{{^-last}}, {{/-last}}{{/allParams}}{{#vendorExtensions.x-spring-paginated}}{{#hasParams}}, {{/hasParams}}Integer page, Integer size, java.util.List sort{{/vendorExtensions.x-spring-paginated}}) throws ApiException {
Object localVarPostBody = {{#bodyParam}}{{paramName}}{{/bodyParam}}{{^bodyParam}}null{{/bodyParam}};
{{#allParams}}{{#required}}
// verify the required parameter '{{paramName}}' is set
@@ -131,6 +141,18 @@ public class {{classname}} {
localVarQueryParams.addAll(apiClient.parameterToPairs("{{#collectionFormat}}{{{collectionFormat}}}{{/collectionFormat}}", "{{baseName}}", {{paramName}}));
{{/queryParams}}
+ {{#vendorExtensions.x-spring-paginated}}
+ if (page != null) {
+ localVarQueryParams.addAll(apiClient.parameterToPairs("", "page", page));
+ }
+ if (size != null) {
+ localVarQueryParams.addAll(apiClient.parameterToPairs("", "size", size));
+ }
+ if (sort != null) {
+ localVarQueryParams.addAll(apiClient.parameterToPairs("multi", "sort", sort));
+ }
+ {{/vendorExtensions.x-spring-paginated}}
+
{{#headerParams}}if ({{paramName}} != null)
localVarHeaderParams.put("{{baseName}}", apiClient.parameterToString({{paramName}}));
{{/headerParams}}
@@ -170,6 +192,12 @@ public class {{classname}} {
private {{#isRequired}}final {{/isRequired}}{{{dataType}}} {{paramName}};
{{/allParams}}
+ {{#vendorExtensions.x-spring-paginated}}
+ private Integer page;
+ private Integer size;
+ private java.util.List sort;
+ {{/vendorExtensions.x-spring-paginated}}
+
private API{{operationId}}Request({{#pathParams}}{{{dataType}}} {{paramName}}{{^-last}}, {{/-last}}{{/pathParams}}) {
{{#pathParams}}
this.{{paramName}} = {{paramName}};
@@ -190,6 +218,38 @@ public class {{classname}} {
{{/isPathParam}}
{{/allParams}}
+ {{#vendorExtensions.x-spring-paginated}}
+ /**
+ * Set page
+ * @param page Page number (0-based) (optional)
+ * @return API{{operationId}}Request
+ */
+ public API{{operationId}}Request page(Integer page) {
+ this.page = page;
+ return this;
+ }
+
+ /**
+ * Set size
+ * @param size Number of records per page (optional)
+ * @return API{{operationId}}Request
+ */
+ public API{{operationId}}Request size(Integer size) {
+ this.size = size;
+ return this;
+ }
+
+ /**
+ * Set sort
+ * @param sort Sorting criteria in the format: property,asc|desc (optional)
+ * @return API{{operationId}}Request
+ */
+ public API{{operationId}}Request sort(java.util.List sort) {
+ this.sort = sort;
+ return this;
+ }
+ {{/vendorExtensions.x-spring-paginated}}
+
/**
* Execute {{operationId}} request
{{#returnType}}* @return {{.}}{{/returnType}}
@@ -230,7 +290,7 @@ public class {{classname}} {
@Deprecated
{{/isDeprecated}}
public ApiResponse<{{#returnType}}{{{.}}}{{/returnType}}{{^returnType}}Void{{/returnType}}> executeWithHttpInfo() throws ApiException {
- return {{operationId}}WithHttpInfo({{#allParams}}{{paramName}}{{^-last}}, {{/-last}}{{/allParams}});
+ return {{operationId}}WithHttpInfo({{#allParams}}{{paramName}}{{^-last}}, {{/-last}}{{/allParams}}{{#vendorExtensions.x-spring-paginated}}{{#hasParams}}, {{/hasParams}}page, size, sort{{/vendorExtensions.x-spring-paginated}});
}
}
diff --git a/templates/pojo.mustache b/templates/pojo.mustache
index 616652520..d3fe39470 100644
--- a/templates/pojo.mustache
+++ b/templates/pojo.mustache
@@ -1,3 +1,6 @@
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+
/**
* {{#description}}{{.}}{{/description}}{{^description}}{{classname}}{{/description}}
*/{{#description}}
@@ -67,12 +70,7 @@ public class {{classname}} {{#parent}}extends {{{parent}}} {{/parent}}{{#vendorE
{{/isContainer}}
{{/vendorExtensions.x-is-jackson-optional-nullable}}
{{^vendorExtensions.x-is-jackson-optional-nullable}}
- {{#isContainer}}
- private {{{datatypeWithEnum}}} {{name}}{{#required}}{{#defaultValue}} = {{{.}}}{{/defaultValue}}{{/required}}{{^required}} = null{{/required}};
- {{/isContainer}}
- {{^isContainer}}
- private {{{datatypeWithEnum}}} {{name}}{{#defaultValue}} = {{{.}}}{{/defaultValue}};
- {{/isContainer}}
+ private {{{datatypeWithEnum}}} {{name}}{{#isContainer}} = new {{#isArray}}{{#uniqueItems}}LinkedHashSet{{/uniqueItems}}{{^uniqueItems}}ArrayList{{/uniqueItems}}{{/isArray}}{{#isMap}}HashMap{{/isMap}}<>(){{/isContainer}}{{^isContainer}}{{#defaultValue}} = {{{.}}}{{/defaultValue}}{{/isContainer}};
{{/vendorExtensions.x-is-jackson-optional-nullable}}
{{/vars}}
@@ -101,7 +99,7 @@ public class {{classname}} {{#parent}}extends {{{parent}}} {{/parent}}{{#vendorE
{{^isReadOnly}}
{{#vendorExtensions.x-enum-as-string}}
- public static final Set {{{nameInSnakeCase}}}_VALUES = new HashSet<>(Arrays.asList(
+ public static final Set {{{nameInSnakeCase}}}_VALUES = new LinkedHashSet<>(Arrays.asList(
{{#allowableValues}}{{#enumVars}}{{{value}}}{{^-last}}, {{/-last}}{{/enumVars}}{{/allowableValues}}
));
@@ -138,7 +136,7 @@ public class {{classname}} {{#parent}}extends {{{parent}}} {{/parent}}{{#vendorE
{{^vendorExtensions.x-is-jackson-optional-nullable}}
{{^required}}
if (this.{{name}} == null) {
- this.{{name}} = {{{defaultValue}}};
+ this.{{name}} = new {{#uniqueItems}}LinkedHashSet{{/uniqueItems}}{{^uniqueItems}}ArrayList{{/uniqueItems}}<>();
}
{{/required}}
this.{{name}}.add({{name}}Item);
@@ -163,7 +161,7 @@ public class {{classname}} {{#parent}}extends {{{parent}}} {{/parent}}{{#vendorE
{{^vendorExtensions.x-is-jackson-optional-nullable}}
{{^required}}
if (this.{{name}} == null) {
- this.{{name}} = {{{defaultValue}}};
+ this.{{name}} = new HashMap<>();
}
{{/required}}
this.{{name}}.put(key, {{name}}Item);