From 9ea1a37bd5e76495d6f42b5af0da7355cad6fcc2 Mon Sep 17 00:00:00 2001 From: Patrick Hobusch Date: Sat, 4 Jul 2026 11:43:36 +0200 Subject: [PATCH 01/18] Add _all endpoint applying a complete configuration in one request PUT / accepts a product-specific _AllModel and applies each present sub-field through the existing services. The outcome of every sub-field is reported in the response's status map; the overall HTTP status aggregates them (200 all success, 207 partial success, uniform client error code, or 500 on any server error). License keys in responses are redacted. Commons provides the shared framework: _AllResource/_AllService abstractions, ServiceResult, _AllModelStatus, ServiceResultUtil (including group-aware sub-entity status keys such as settings/custom-html), and LicenseKeyRedactor. Each product contributes its _AllModel, _AllServiceImpl, _AllResourceImpl (with product-typed OpenAPI response schemas), composite settings services, and Spring wiring. Composite settings services are not exposed as beans where their type would make by-type injection of the narrow settings interfaces ambiguous in the REST layer. Covered by unit tests for the framework, the composite default methods and the product services, plus functional tests for the _all endpoint (single- and multi-entity payloads, empty model, authentication and authorization) and a Confluence settings-branding functional test guarding the REST DI wiring. --- .../commons/constants/BootstrAPI.java | 16 +- .../model/AbstractAuthenticationIdpModel.java | 2 +- .../commons/model/AuthenticationModel.java | 25 +++ .../commons/model/MailServerModel.java | 26 +++ .../SettingsBrandingColorSchemeModel.java | 2 +- ...gsModel.java => SettingsGeneralModel.java} | 12 +- .../commons/model/type/ServiceResult.java | 26 +++ .../commons/model/type/_AllModelAccessor.java | 9 + .../commons/model/type/_AllModelStatus.java | 40 ++++ .../AbstractSettingsGeneralResourceImpl.java | 31 +++ .../rest/AbstractSettingsResourceImpl.java | 31 --- .../rest/_AbstractAllResourceImpl.java | 85 ++++++++ ...urce.java => SettingsGeneralResource.java} | 8 +- .../commons/rest/api/_AllResource.java | 44 ++++ .../service/_AbstractAllServiceImpl.java | 89 ++++++++ .../service/api/AuthenticationService.java | 39 ++++ .../commons/service/api/LicensesService.java | 14 ++ .../service/api/MailServerService.java | 35 +++ ...rvice.java => SettingsGeneralService.java} | 4 +- .../commons/service/api/_AllService.java | 16 ++ .../commons/util/LicenseKeyRedactor.java | 64 ++++++ .../commons/util/ServiceResultUtil.java | 97 +++++++++ ...est.java => SettingsGeneralModelTest.java} | 0 ....java => SettingsGeneralResourceTest.java} | 22 +- .../rest/_AbstractAllResourceImplTest.java | 65 ++++++ .../impl/TestSettingsGeneralResourceImpl.java | 12 ++ .../rest/impl/TestSettingsResourceImpl.java | 12 -- .../service/_AbstractAllServiceImplTest.java | 200 ++++++++++++++++++ .../api/AuthenticationServiceTest.java | 88 ++++++++ .../service/api/MailServerServiceTest.java | 80 +++++++ .../commons/util/LicenseKeyRedactorTest.java | 44 ++++ .../commons/util/ServiceResultUtilTest.java | 111 ++++++++++ ...tractSettingsGeneralResourceFuncTest.java} | 12 +- .../rest/Abstract_AllResourceFuncTest.java | 121 +++++++++++ confluence/Apis/AllApi.md | 36 ++++ confluence/Apis/SettingsApi.md | 10 +- confluence/Models/AuthenticationModel.md | 10 + confluence/Models/MailServerModel.md | 10 + confluence/Models/SettingsGeneralModel.md | 13 ++ confluence/Models/SettingsModel.md | 9 +- confluence/Models/_AllModel.md | 16 ++ confluence/Models/_AllModelStatus.md | 11 + confluence/README.md | 6 + .../confluence/config/ServiceConfig.java | 35 ++- .../confluence/model/SettingsModel.java | 35 +++ .../confluence/model/_AllModel.java | 52 +++++ ....java => SettingsGeneralResourceImpl.java} | 8 +- .../confluence/rest/_AllResourceImpl.java | 64 ++++++ .../ConfluenceSettingsServiceImpl.java | 90 ++++++++ .../service/LicensesServiceImpl.java | 14 ++ .../service/SettingsServiceImpl.java | 23 +- .../confluence/service/_AllServiceImpl.java | 82 +++++++ .../api/ConfluenceSettingsService.java | 67 +++++- .../confluence/rest/_AllResourceTest.java | 88 ++++++++ .../ConfluenceSettingsServiceTest.java | 107 ++++++++++ .../service/SettingsServiceTest.java | 14 +- .../service/_AllServiceImplTest.java | 172 +++++++++++++++ .../SettingsBrandingResourceFuncTest.java | 46 ++++ .../rest/SettingsGeneralResourceFuncTest.java | 12 ++ .../rest/SettingsResourceFuncTest.java | 12 -- .../confluence/rest/_AllResourceFuncTest.java | 51 +++++ crowd/Apis/AllApi.md | 36 ++++ crowd/Apis/SettingsApi.md | 10 +- crowd/Models/MailServerModel.md | 10 + crowd/Models/MailServerPopModel.md | 16 ++ crowd/Models/SettingsGeneralModel.md | 13 ++ crowd/Models/SettingsModel.md | 7 +- crowd/Models/_AllModel.md | 18 ++ crowd/Models/_AllModelStatus.md | 11 + crowd/README.md | 6 + .../crowd/config/ServiceConfig.java | 20 ++ .../model/SettingsBrandingLoginPageModel.java | 6 +- .../{AllModel.java => SettingsModel.java} | 15 +- .../bootstrapi/crowd/model/_AllModel.java | 57 +++++ ....java => SettingsGeneralResourceImpl.java} | 8 +- .../crowd/rest/_AllResourceImpl.java | 64 ++++++ .../service/CrowdSettingsServiceImpl.java | 49 +++++ .../crowd/service/LicensesServiceImpl.java | 14 ++ .../crowd/service/SettingsServiceImpl.java | 8 +- .../crowd/service/_AllServiceImpl.java | 99 +++++++++ .../crowd/service/api/AllService.java | 10 - .../api/CrowdSettingsGeneralService.java | 6 +- .../service/api/CrowdSettingsService.java | 46 ++++ .../crowd/rest/_AllResourceTest.java | 93 ++++++++ .../service/CrowdSettingsServiceTest.java | 92 ++++++++ .../crowd/service/DirectoriesServiceTest.java | 1 - .../crowd/service/SettingsServiceTest.java | 6 +- .../crowd/service/_AllServiceImplTest.java | 194 +++++++++++++++++ .../crowd/rest/FuncTestExamples.java | 31 +++ .../rest/SettingsGeneralResourceFuncTest.java | 13 ++ .../crowd/rest/SettingsResourceFuncTest.java | 16 -- .../crowd/rest/_AllResourceFuncTest.java | 55 +++++ jira/Apis/AllApi.md | 36 ++++ jira/Apis/SettingsApi.md | 10 +- jira/Models/AuthenticationModel.md | 10 + jira/Models/MailServerModel.md | 10 + jira/Models/SettingsGeneralModel.md | 13 ++ jira/Models/SettingsModel.md | 8 +- jira/Models/_AllModel.md | 16 ++ jira/Models/_AllModelStatus.md | 11 + jira/README.md | 6 + .../bootstrapi/jira/config/ServiceConfig.java | 13 ++ .../bootstrapi/jira/model/SettingsModel.java | 31 +++ .../bootstrapi/jira/model/_AllModel.java | 52 +++++ ....java => SettingsGeneralResourceImpl.java} | 8 +- .../jira/rest/_AllResourceImpl.java | 64 ++++++ .../jira/service/LicensesServiceImpl.java | 14 ++ .../jira/service/SettingsServiceImpl.java | 10 +- .../jira/service/_AllServiceImpl.java | 82 +++++++ .../jira/service/api/JiraSettingsService.java | 51 ++++- .../jira/rest/_AllResourceTest.java | 88 ++++++++ .../jira/service/JiraSettingsServiceTest.java | 81 ++++++- .../jira/service/LicensesServiceTest.java | 35 +++ .../jira/service/SettingsServiceTest.java | 12 +- .../jira/service/_AllServiceImplTest.java | 172 +++++++++++++++ .../rest/SettingsGeneralResourceFuncTest.java | 5 + .../jira/rest/SettingsResourceFuncTest.java | 5 - .../jira/rest/_AllResourceFuncTest.java | 46 ++++ 118 files changed, 4265 insertions(+), 229 deletions(-) create mode 100644 commons/src/main/java/com/deftdevs/bootstrapi/commons/model/AuthenticationModel.java create mode 100644 commons/src/main/java/com/deftdevs/bootstrapi/commons/model/MailServerModel.java rename commons/src/main/java/com/deftdevs/bootstrapi/commons/model/{SettingsModel.java => SettingsGeneralModel.java} (75%) create mode 100644 commons/src/main/java/com/deftdevs/bootstrapi/commons/model/type/ServiceResult.java create mode 100644 commons/src/main/java/com/deftdevs/bootstrapi/commons/model/type/_AllModelAccessor.java create mode 100644 commons/src/main/java/com/deftdevs/bootstrapi/commons/model/type/_AllModelStatus.java create mode 100644 commons/src/main/java/com/deftdevs/bootstrapi/commons/rest/AbstractSettingsGeneralResourceImpl.java delete mode 100644 commons/src/main/java/com/deftdevs/bootstrapi/commons/rest/AbstractSettingsResourceImpl.java create mode 100644 commons/src/main/java/com/deftdevs/bootstrapi/commons/rest/_AbstractAllResourceImpl.java rename commons/src/main/java/com/deftdevs/bootstrapi/commons/rest/api/{SettingsResource.java => SettingsGeneralResource.java} (86%) create mode 100644 commons/src/main/java/com/deftdevs/bootstrapi/commons/rest/api/_AllResource.java create mode 100644 commons/src/main/java/com/deftdevs/bootstrapi/commons/service/_AbstractAllServiceImpl.java rename commons/src/main/java/com/deftdevs/bootstrapi/commons/service/api/{SettingsService.java => SettingsGeneralService.java} (72%) create mode 100644 commons/src/main/java/com/deftdevs/bootstrapi/commons/service/api/_AllService.java create mode 100644 commons/src/main/java/com/deftdevs/bootstrapi/commons/util/LicenseKeyRedactor.java create mode 100644 commons/src/main/java/com/deftdevs/bootstrapi/commons/util/ServiceResultUtil.java rename commons/src/test/java/com/deftdevs/bootstrapi/commons/model/{SettingsModelTest.java => SettingsGeneralModelTest.java} (100%) rename commons/src/test/java/com/deftdevs/bootstrapi/commons/rest/{SettingsResourceTest.java => SettingsGeneralResourceTest.java} (56%) create mode 100644 commons/src/test/java/com/deftdevs/bootstrapi/commons/rest/_AbstractAllResourceImplTest.java create mode 100644 commons/src/test/java/com/deftdevs/bootstrapi/commons/rest/impl/TestSettingsGeneralResourceImpl.java delete mode 100644 commons/src/test/java/com/deftdevs/bootstrapi/commons/rest/impl/TestSettingsResourceImpl.java create mode 100644 commons/src/test/java/com/deftdevs/bootstrapi/commons/service/_AbstractAllServiceImplTest.java create mode 100644 commons/src/test/java/com/deftdevs/bootstrapi/commons/service/api/AuthenticationServiceTest.java create mode 100644 commons/src/test/java/com/deftdevs/bootstrapi/commons/service/api/MailServerServiceTest.java create mode 100644 commons/src/test/java/com/deftdevs/bootstrapi/commons/util/LicenseKeyRedactorTest.java create mode 100644 commons/src/test/java/com/deftdevs/bootstrapi/commons/util/ServiceResultUtilTest.java rename commons/src/test/java/it/com/deftdevs/bootstrapi/commons/rest/{AbstractSettingsResourceFuncTest.java => AbstractSettingsGeneralResourceFuncTest.java} (84%) create mode 100644 commons/src/test/java/it/com/deftdevs/bootstrapi/commons/rest/Abstract_AllResourceFuncTest.java create mode 100644 confluence/Apis/AllApi.md create mode 100644 confluence/Models/AuthenticationModel.md create mode 100644 confluence/Models/MailServerModel.md create mode 100644 confluence/Models/SettingsGeneralModel.md create mode 100644 confluence/Models/_AllModel.md create mode 100644 confluence/Models/_AllModelStatus.md create mode 100644 confluence/src/main/java/com/deftdevs/bootstrapi/confluence/model/SettingsModel.java create mode 100644 confluence/src/main/java/com/deftdevs/bootstrapi/confluence/model/_AllModel.java rename confluence/src/main/java/com/deftdevs/bootstrapi/confluence/rest/{SettingsResourceImpl.java => SettingsGeneralResourceImpl.java} (69%) create mode 100644 confluence/src/main/java/com/deftdevs/bootstrapi/confluence/rest/_AllResourceImpl.java create mode 100644 confluence/src/main/java/com/deftdevs/bootstrapi/confluence/service/ConfluenceSettingsServiceImpl.java create mode 100644 confluence/src/main/java/com/deftdevs/bootstrapi/confluence/service/_AllServiceImpl.java create mode 100644 confluence/src/test/java/com/deftdevs/bootstrapi/confluence/rest/_AllResourceTest.java create mode 100644 confluence/src/test/java/com/deftdevs/bootstrapi/confluence/service/ConfluenceSettingsServiceTest.java create mode 100644 confluence/src/test/java/com/deftdevs/bootstrapi/confluence/service/_AllServiceImplTest.java create mode 100644 confluence/src/test/java/it/com/deftdevs/bootstrapi/confluence/rest/SettingsBrandingResourceFuncTest.java create mode 100644 confluence/src/test/java/it/com/deftdevs/bootstrapi/confluence/rest/SettingsGeneralResourceFuncTest.java delete mode 100644 confluence/src/test/java/it/com/deftdevs/bootstrapi/confluence/rest/SettingsResourceFuncTest.java create mode 100644 confluence/src/test/java/it/com/deftdevs/bootstrapi/confluence/rest/_AllResourceFuncTest.java create mode 100644 crowd/Apis/AllApi.md create mode 100644 crowd/Models/MailServerModel.md create mode 100644 crowd/Models/MailServerPopModel.md create mode 100644 crowd/Models/SettingsGeneralModel.md create mode 100644 crowd/Models/_AllModel.md create mode 100644 crowd/Models/_AllModelStatus.md rename crowd/src/main/java/com/deftdevs/bootstrapi/crowd/model/{AllModel.java => SettingsModel.java} (53%) create mode 100644 crowd/src/main/java/com/deftdevs/bootstrapi/crowd/model/_AllModel.java rename crowd/src/main/java/com/deftdevs/bootstrapi/crowd/rest/{SettingsResourceImpl.java => SettingsGeneralResourceImpl.java} (69%) create mode 100644 crowd/src/main/java/com/deftdevs/bootstrapi/crowd/rest/_AllResourceImpl.java create mode 100644 crowd/src/main/java/com/deftdevs/bootstrapi/crowd/service/CrowdSettingsServiceImpl.java create mode 100644 crowd/src/main/java/com/deftdevs/bootstrapi/crowd/service/_AllServiceImpl.java delete mode 100644 crowd/src/main/java/com/deftdevs/bootstrapi/crowd/service/api/AllService.java create mode 100644 crowd/src/main/java/com/deftdevs/bootstrapi/crowd/service/api/CrowdSettingsService.java create mode 100644 crowd/src/test/java/com/deftdevs/bootstrapi/crowd/rest/_AllResourceTest.java create mode 100644 crowd/src/test/java/com/deftdevs/bootstrapi/crowd/service/CrowdSettingsServiceTest.java create mode 100644 crowd/src/test/java/com/deftdevs/bootstrapi/crowd/service/_AllServiceImplTest.java create mode 100644 crowd/src/test/java/it/com/deftdevs/bootstrapi/crowd/rest/FuncTestExamples.java create mode 100644 crowd/src/test/java/it/com/deftdevs/bootstrapi/crowd/rest/SettingsGeneralResourceFuncTest.java delete mode 100644 crowd/src/test/java/it/com/deftdevs/bootstrapi/crowd/rest/SettingsResourceFuncTest.java create mode 100644 crowd/src/test/java/it/com/deftdevs/bootstrapi/crowd/rest/_AllResourceFuncTest.java create mode 100644 jira/Apis/AllApi.md create mode 100644 jira/Models/AuthenticationModel.md create mode 100644 jira/Models/MailServerModel.md create mode 100644 jira/Models/SettingsGeneralModel.md create mode 100644 jira/Models/_AllModel.md create mode 100644 jira/Models/_AllModelStatus.md create mode 100644 jira/src/main/java/com/deftdevs/bootstrapi/jira/model/SettingsModel.java create mode 100644 jira/src/main/java/com/deftdevs/bootstrapi/jira/model/_AllModel.java rename jira/src/main/java/com/deftdevs/bootstrapi/jira/rest/{SettingsResourceImpl.java => SettingsGeneralResourceImpl.java} (69%) create mode 100644 jira/src/main/java/com/deftdevs/bootstrapi/jira/rest/_AllResourceImpl.java create mode 100644 jira/src/main/java/com/deftdevs/bootstrapi/jira/service/_AllServiceImpl.java create mode 100644 jira/src/test/java/com/deftdevs/bootstrapi/jira/rest/_AllResourceTest.java create mode 100644 jira/src/test/java/com/deftdevs/bootstrapi/jira/service/_AllServiceImplTest.java create mode 100644 jira/src/test/java/it/com/deftdevs/bootstrapi/jira/rest/SettingsGeneralResourceFuncTest.java delete mode 100644 jira/src/test/java/it/com/deftdevs/bootstrapi/jira/rest/SettingsResourceFuncTest.java create mode 100644 jira/src/test/java/it/com/deftdevs/bootstrapi/jira/rest/_AllResourceFuncTest.java diff --git a/commons/src/main/java/com/deftdevs/bootstrapi/commons/constants/BootstrAPI.java b/commons/src/main/java/com/deftdevs/bootstrapi/commons/constants/BootstrAPI.java index 644ae427..8792b473 100644 --- a/commons/src/main/java/com/deftdevs/bootstrapi/commons/constants/BootstrAPI.java +++ b/commons/src/main/java/com/deftdevs/bootstrapi/commons/constants/BootstrAPI.java @@ -2,7 +2,9 @@ public class BootstrAPI { - public static final String ALL = "all"; + public static final String _ALL = "_all"; + public static final String _ROOT = "/"; + public static final String APPLICATION = "application"; public static final String APPLICATIONS = "applications"; public static final String APPLICATION_LINK = "application-link"; @@ -38,29 +40,39 @@ public class BootstrAPI { public static final String MAIL_SERVERS = "mail-servers"; public static final String MAIL_SERVER_POP = "pop"; public static final String MAIL_SERVER_SMTP = "smtp"; + public static final String MAIL_TEMPLATES = "mail-templates"; public static final String PERMISSION = "permission"; public static final String PERMISSIONS = "permissions"; public static final String PERMISSION_ANONYMOUS_ACCESS = "anonymous-access"; public static final String PERMISSIONS_GLOBAL = "global"; public static final String PING = "ping"; + public static final String SESSION_CONFIG = "session-config"; public static final String SETTINGS = "settings"; public static final String SETTINGS_BANNER = "banner"; public static final String SETTINGS_BRANDING = "branding"; public static final String SETTINGS_BRANDING_LOGIN_PAGE = "login-page"; public static final String SETTINGS_BRANDING_LOGO = "logo"; public static final String SETTINGS_CUSTOM_HTML = "custom-html"; + public static final String SETTINGS_GENERAL = "general"; public static final String SETTINGS_SECURITY = "security"; public static final String USER = "user"; public static final String USERS = "users"; + public static final String TRUSTED_PROXIES = "trusted-proxies"; public static final String USER_PASSWORD = "password"; public static final String MEDIA_TYPE_IMAGE = "image/*"; public static final String ERROR_COLLECTION_RESPONSE_DESCRIPTION = "Returns a list of error messages."; + public static final String _ALL_PUT_SUMMARY = "Apply a complete configuration"; + public static final String _ALL_PUT_RESPONSE_DESCRIPTION = "Returns the updated configuration. The per-sub-field outcome is reported in the" + + " 'status' map (2xx for success, 4xx/5xx for failure with a human-readable 'message' and optional 'details')." + + " License keys in the response are redacted (e.g. 'AAAB...wxyz#a1b2')."; + public static final String _ALL_PUT_PARTIAL_RESPONSE_DESCRIPTION = "Configuration applied partially. Inspect the per-sub-field 'status' map" + + " in the response body to see which fields succeeded and which failed."; public static final String SETTINGS_GENERAL_GET_SUMMARY = "Get the general settings"; public static final String SETTINGS_GENERAL_GET_RESPONSE_DESCRIPTION = "Returns the general settings"; public static final String SETTINGS_GENERAL_PUT_SUMMARY = "Set the general settings"; - public static final String SETTINGS_GENERAL_PUT_RESPONSE_DESCRIPTION = "Returns the general security settings"; + public static final String SETTINGS_GENERAL_PUT_RESPONSE_DESCRIPTION = "Returns the updated general settings"; public static final String SETTINGS_SECURITY_GET_SUMMARY = "Get the security settings"; public static final String SETTINGS_SECURITY_GET_RESPONSE_DESCRIPTION = "Returns the security settings"; public static final String SETTINGS_SECURITY_PUT_SUMMARY = "Set the security settings"; diff --git a/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/AbstractAuthenticationIdpModel.java b/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/AbstractAuthenticationIdpModel.java index ba5828d8..86ee05b1 100644 --- a/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/AbstractAuthenticationIdpModel.java +++ b/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/AbstractAuthenticationIdpModel.java @@ -17,7 +17,7 @@ @SuperBuilder @NoArgsConstructor @AllArgsConstructor -@XmlRootElement +@XmlRootElement(name = "authentication-idps") @JsonTypeInfo( use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, diff --git a/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/AuthenticationModel.java b/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/AuthenticationModel.java new file mode 100644 index 00000000..909488f4 --- /dev/null +++ b/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/AuthenticationModel.java @@ -0,0 +1,25 @@ +package com.deftdevs.bootstrapi.commons.model; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; +import java.util.Map; + +import static com.deftdevs.bootstrapi.commons.constants.BootstrAPI.AUTHENTICATION; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@XmlRootElement(name = AUTHENTICATION) +public class AuthenticationModel { + + @XmlElement + private Map idps; + + @XmlElement + private AuthenticationSsoModel sso; + +} diff --git a/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/MailServerModel.java b/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/MailServerModel.java new file mode 100644 index 00000000..5329276e --- /dev/null +++ b/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/MailServerModel.java @@ -0,0 +1,26 @@ +package com.deftdevs.bootstrapi.commons.model; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; + +import static com.deftdevs.bootstrapi.commons.constants.BootstrAPI.MAIL_SERVER; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@XmlRootElement(name = MAIL_SERVER) +public class MailServerModel { + + @XmlElement + private MailServerSmtpModel smtp; + + @XmlElement + private MailServerPopModel pop; + +} diff --git a/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/SettingsBrandingColorSchemeModel.java b/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/SettingsBrandingColorSchemeModel.java index 29fe6548..66740ab0 100644 --- a/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/SettingsBrandingColorSchemeModel.java +++ b/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/SettingsBrandingColorSchemeModel.java @@ -16,7 +16,7 @@ @Builder @NoArgsConstructor @AllArgsConstructor -@XmlRootElement(name = SETTINGS + "-" + SETTINGS_BRANDING + "-" + COLOR_SCHEME) +@XmlRootElement(name = SETTINGS + "-" + SETTINGS_BRANDING) @XmlAccessorType(XmlAccessType.FIELD) public class SettingsBrandingColorSchemeModel { diff --git a/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/SettingsModel.java b/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/SettingsGeneralModel.java similarity index 75% rename from commons/src/main/java/com/deftdevs/bootstrapi/commons/model/SettingsModel.java rename to commons/src/main/java/com/deftdevs/bootstrapi/commons/model/SettingsGeneralModel.java index 1689a097..db8e9dbe 100644 --- a/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/SettingsModel.java +++ b/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/SettingsGeneralModel.java @@ -1,6 +1,5 @@ package com.deftdevs.bootstrapi.commons.model; -import com.deftdevs.bootstrapi.commons.constants.BootstrAPI; import lombok.AllArgsConstructor; import lombok.Data; import lombok.Builder; @@ -13,13 +12,16 @@ import javax.xml.bind.annotation.XmlRootElement; import java.net.URI; +import static com.deftdevs.bootstrapi.commons.constants.BootstrAPI.SETTINGS; +import static com.deftdevs.bootstrapi.commons.constants.BootstrAPI.SETTINGS_GENERAL; + @Data @Builder @NoArgsConstructor @AllArgsConstructor -@XmlRootElement(name = BootstrAPI.SETTINGS) +@XmlRootElement(name = SETTINGS + "-" + SETTINGS_GENERAL) @XmlAccessorType(XmlAccessType.FIELD) -public class SettingsModel { +public class SettingsGeneralModel { @XmlElement private URI baseUrl; @@ -46,7 +48,7 @@ public String getMode() { // Example instances for documentation and tests - public static final SettingsModel EXAMPLE_1 = SettingsModel.builder() + public static final SettingsGeneralModel EXAMPLE_1 = SettingsGeneralModel.builder() .title("Example") .baseUrl(URI.create("https://example.com")) .mode("private") @@ -54,7 +56,7 @@ public String getMode() { .externalUserManagement(true) .build(); - public static final SettingsModel EXAMPLE_1_NO_MODE = SettingsModel.builder() + public static final SettingsGeneralModel EXAMPLE_1_NO_MODE = SettingsGeneralModel.builder() .title("Example") .baseUrl(URI.create("https://example.com")) .mode(null) diff --git a/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/type/ServiceResult.java b/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/type/ServiceResult.java new file mode 100644 index 00000000..53b0f1fb --- /dev/null +++ b/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/type/ServiceResult.java @@ -0,0 +1,26 @@ +package com.deftdevs.bootstrapi.commons.model.type; + +import java.util.Map; + +public class ServiceResult { + + private final T model; + private final Map status; + + public ServiceResult( + final T model, + final Map status) { + + this.model = model; + this.status = status; + } + + public T getModel() { + return model; + } + + public Map getStatus() { + return status; + } + +} diff --git a/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/type/_AllModelAccessor.java b/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/type/_AllModelAccessor.java new file mode 100644 index 00000000..c586adff --- /dev/null +++ b/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/type/_AllModelAccessor.java @@ -0,0 +1,9 @@ +package com.deftdevs.bootstrapi.commons.model.type; + +import java.util.Map; + +public interface _AllModelAccessor { + + Map getStatus(); + +} diff --git a/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/type/_AllModelStatus.java b/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/type/_AllModelStatus.java new file mode 100644 index 00000000..42d73b65 --- /dev/null +++ b/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/type/_AllModelStatus.java @@ -0,0 +1,40 @@ +package com.deftdevs.bootstrapi.commons.model.type; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.ws.rs.core.Response; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@XmlRootElement(name = "status") +public class _AllModelStatus { + + @XmlElement + private int status; + + @XmlElement + private String message; + + @XmlElement + private String details; + + public static _AllModelStatus success() { + return new _AllModelStatus(Response.Status.OK.getStatusCode(), "Success", null); + } + + public static _AllModelStatus error(Response.Status status, String message, String details) { + final int code = status != null + ? status.getStatusCode() + : Response.Status.INTERNAL_SERVER_ERROR.getStatusCode(); + return new _AllModelStatus(code, message, details); + } + + public static _AllModelStatus error(int statusCode, String message, String details) { + return new _AllModelStatus(statusCode, message, details); + } +} diff --git a/commons/src/main/java/com/deftdevs/bootstrapi/commons/rest/AbstractSettingsGeneralResourceImpl.java b/commons/src/main/java/com/deftdevs/bootstrapi/commons/rest/AbstractSettingsGeneralResourceImpl.java new file mode 100644 index 00000000..b93da638 --- /dev/null +++ b/commons/src/main/java/com/deftdevs/bootstrapi/commons/rest/AbstractSettingsGeneralResourceImpl.java @@ -0,0 +1,31 @@ +package com.deftdevs.bootstrapi.commons.rest; + +import com.deftdevs.bootstrapi.commons.model.SettingsGeneralModel; +import com.deftdevs.bootstrapi.commons.rest.api.SettingsGeneralResource; +import com.deftdevs.bootstrapi.commons.service.api.SettingsGeneralService; + +import javax.ws.rs.core.Response; + +public abstract class AbstractSettingsGeneralResourceImpl> + implements SettingsGeneralResource { + + private final S settingsService; + + public AbstractSettingsGeneralResourceImpl( + final S settingsService) { + + this.settingsService = settingsService; + } + + @Override + public Response getSettings() { + final B settingsModel = settingsService.getSettingsGeneral(); + return Response.ok(settingsModel).build(); + } + + @Override + public Response setSettings(B settingsModel) { + final B updatedSettingsGeneralModel = settingsService.setSettingsGeneral(settingsModel); + return Response.ok(updatedSettingsGeneralModel).build(); + } +} diff --git a/commons/src/main/java/com/deftdevs/bootstrapi/commons/rest/AbstractSettingsResourceImpl.java b/commons/src/main/java/com/deftdevs/bootstrapi/commons/rest/AbstractSettingsResourceImpl.java deleted file mode 100644 index c1c4c405..00000000 --- a/commons/src/main/java/com/deftdevs/bootstrapi/commons/rest/AbstractSettingsResourceImpl.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.deftdevs.bootstrapi.commons.rest; - -import com.deftdevs.bootstrapi.commons.model.SettingsModel; -import com.deftdevs.bootstrapi.commons.rest.api.SettingsResource; -import com.deftdevs.bootstrapi.commons.service.api.SettingsService; - -import javax.ws.rs.core.Response; - -public abstract class AbstractSettingsResourceImpl> - implements SettingsResource { - - private final S settingsService; - - public AbstractSettingsResourceImpl( - final S settingsService) { - - this.settingsService = settingsService; - } - - @Override - public Response getSettings() { - final B settingsModel = settingsService.getSettingsGeneral(); - return Response.ok(settingsModel).build(); - } - - @Override - public Response setSettings(B settingsModel) { - final B updatedSettingsModel = settingsService.setSettingsGeneral(settingsModel); - return Response.ok(updatedSettingsModel).build(); - } -} diff --git a/commons/src/main/java/com/deftdevs/bootstrapi/commons/rest/_AbstractAllResourceImpl.java b/commons/src/main/java/com/deftdevs/bootstrapi/commons/rest/_AbstractAllResourceImpl.java new file mode 100644 index 00000000..45a5febf --- /dev/null +++ b/commons/src/main/java/com/deftdevs/bootstrapi/commons/rest/_AbstractAllResourceImpl.java @@ -0,0 +1,85 @@ +package com.deftdevs.bootstrapi.commons.rest; + +import com.deftdevs.bootstrapi.commons.model.type._AllModelAccessor; +import com.deftdevs.bootstrapi.commons.model.type._AllModelStatus; +import com.deftdevs.bootstrapi.commons.rest.api._AllResource; +import com.deftdevs.bootstrapi.commons.service.api._AllService; + +import javax.ws.rs.core.Response; +import java.util.Map; + +public abstract class _AbstractAllResourceImpl<_AllModel extends _AllModelAccessor> + implements _AllResource<_AllModel> { + + static final int MULTI_STATUS = 207; + + private final _AllService<_AllModel> allService; + + public _AbstractAllResourceImpl( + final _AllService<_AllModel> allService) { + + this.allService = allService; + } + + @Override + public Response setAll( + final _AllModel allModel) { + + final _AllModel result = allService.setAll(allModel); + final int overallStatus = computeOverallStatus(result.getStatus()); + return Response.status(overallStatus).entity(result).build(); + } + + /** + * Aggregates per-sub-field statuses into a single HTTP response code: + *
    + *
  • All successful (or empty) → 200 OK.
  • + *
  • All entries share the same status code → that code.
  • + *
  • Any 5xx → 500 Internal Server Error.
  • + *
  • Mixed 2xx/4xx (or multiple distinct 4xx) → 207 Multi-Status.
  • + *
+ * 207 is used to signal "partial success" — callers must inspect the + * per-field {@code status} map in the response body to see which + * fields succeeded and which failed. + */ + static int computeOverallStatus( + final Map statusMap) { + + if (statusMap == null || statusMap.isEmpty()) { + return Response.Status.OK.getStatusCode(); + } + + boolean anyServerError = false; + boolean anyClientError = false; + boolean anySuccess = false; + Integer firstClientErrorCode = null; + boolean clientErrorCodesDiffer = false; + + for (_AllModelStatus entry : statusMap.values()) { + final int code = entry.getStatus(); + if (code >= 500) { + anyServerError = true; + } else if (code >= 400) { + anyClientError = true; + if (firstClientErrorCode == null) { + firstClientErrorCode = code; + } else if (firstClientErrorCode != code) { + clientErrorCodesDiffer = true; + } + } else if (code >= 200 && code < 300) { + anySuccess = true; + } + } + + if (anyServerError) { + return Response.Status.INTERNAL_SERVER_ERROR.getStatusCode(); + } + if (anyClientError) { + if (anySuccess || clientErrorCodesDiffer) { + return MULTI_STATUS; + } + return firstClientErrorCode; + } + return Response.Status.OK.getStatusCode(); + } +} diff --git a/commons/src/main/java/com/deftdevs/bootstrapi/commons/rest/api/SettingsResource.java b/commons/src/main/java/com/deftdevs/bootstrapi/commons/rest/api/SettingsGeneralResource.java similarity index 86% rename from commons/src/main/java/com/deftdevs/bootstrapi/commons/rest/api/SettingsResource.java rename to commons/src/main/java/com/deftdevs/bootstrapi/commons/rest/api/SettingsGeneralResource.java index 8e415842..6bfcbe8a 100644 --- a/commons/src/main/java/com/deftdevs/bootstrapi/commons/rest/api/SettingsResource.java +++ b/commons/src/main/java/com/deftdevs/bootstrapi/commons/rest/api/SettingsGeneralResource.java @@ -2,7 +2,7 @@ import com.deftdevs.bootstrapi.commons.constants.BootstrAPI; import com.deftdevs.bootstrapi.commons.model.ErrorCollection; -import com.deftdevs.bootstrapi.commons.model.SettingsModel; +import com.deftdevs.bootstrapi.commons.model.SettingsGeneralModel; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; @@ -12,14 +12,14 @@ import javax.ws.rs.PUT; import javax.ws.rs.core.Response; -public interface SettingsResource { +public interface SettingsGeneralResource { @GET @Operation( summary = BootstrAPI.SETTINGS_GENERAL_GET_SUMMARY, responses = { @ApiResponse( - responseCode = "200", content = @Content(schema = @Schema(implementation = SettingsModel.class)), + responseCode = "200", content = @Content(schema = @Schema(implementation = SettingsGeneralModel.class)), description = BootstrAPI.SETTINGS_GENERAL_GET_RESPONSE_DESCRIPTION ), @ApiResponse( @@ -35,7 +35,7 @@ public interface SettingsResource { summary = BootstrAPI.SETTINGS_GENERAL_PUT_SUMMARY, responses = { @ApiResponse( - responseCode = "200", content = @Content(schema = @Schema(implementation = SettingsModel.class)), + responseCode = "200", content = @Content(schema = @Schema(implementation = SettingsGeneralModel.class)), description = BootstrAPI.SETTINGS_GENERAL_PUT_RESPONSE_DESCRIPTION ), @ApiResponse( diff --git a/commons/src/main/java/com/deftdevs/bootstrapi/commons/rest/api/_AllResource.java b/commons/src/main/java/com/deftdevs/bootstrapi/commons/rest/api/_AllResource.java new file mode 100644 index 00000000..62d097a0 --- /dev/null +++ b/commons/src/main/java/com/deftdevs/bootstrapi/commons/rest/api/_AllResource.java @@ -0,0 +1,44 @@ +package com.deftdevs.bootstrapi.commons.rest.api; + +import com.deftdevs.bootstrapi.commons.constants.BootstrAPI; +import com.deftdevs.bootstrapi.commons.model.ErrorCollection; +import com.deftdevs.bootstrapi.commons.model.type._AllModelAccessor; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; + +import javax.validation.constraints.NotNull; +import javax.ws.rs.PUT; +import javax.ws.rs.core.Response; + +public interface _AllResource<_AllModel extends _AllModelAccessor> { + + /** + * Product implementations should override this method and re-declare the + * {@code @Operation} annotation with their concrete {@code _AllModel} as the + * response schema, so the generated OpenAPI documentation shows the product + * model instead of an empty response body. + */ + @PUT + @Operation( + summary = BootstrAPI._ALL_PUT_SUMMARY, + responses = { + @ApiResponse( + responseCode = "200", + description = BootstrAPI._ALL_PUT_RESPONSE_DESCRIPTION + ), + @ApiResponse( + responseCode = "207", + description = BootstrAPI._ALL_PUT_PARTIAL_RESPONSE_DESCRIPTION + ), + @ApiResponse( + responseCode = "default", content = @Content(schema = @Schema(implementation = ErrorCollection.class)), + description = BootstrAPI.ERROR_COLLECTION_RESPONSE_DESCRIPTION + ), + } + ) + Response setAll( + @NotNull final _AllModel bean); + +} diff --git a/commons/src/main/java/com/deftdevs/bootstrapi/commons/service/_AbstractAllServiceImpl.java b/commons/src/main/java/com/deftdevs/bootstrapi/commons/service/_AbstractAllServiceImpl.java new file mode 100644 index 00000000..1f6c938c --- /dev/null +++ b/commons/src/main/java/com/deftdevs/bootstrapi/commons/service/_AbstractAllServiceImpl.java @@ -0,0 +1,89 @@ +package com.deftdevs.bootstrapi.commons.service; + +import com.deftdevs.bootstrapi.commons.model.type._AllModelAccessor; +import com.deftdevs.bootstrapi.commons.model.type._AllModelStatus; +import com.deftdevs.bootstrapi.commons.model.type.ServiceResult; +import com.deftdevs.bootstrapi.commons.service.api._AllService; +import com.deftdevs.bootstrapi.commons.util.ServiceResultUtil; + +import java.util.Map; +import java.util.function.Consumer; +import java.util.function.Function; + +public abstract class _AbstractAllServiceImpl<_AllModel extends _AllModelAccessor> implements _AllService<_AllModel> { + + protected void setEntityWithStatus( + final I input, + final Function> updateFunction, + final Consumer resultConsumer, + final Map statusMap) { + + if (input == null) { + return; + } + + try { + final ServiceResult serviceResult = updateFunction.apply(input); + resultConsumer.accept(serviceResult.getModel()); + statusMap.putAll(serviceResult.getStatus()); + } catch (Exception e) { + // The composite updateFunction normally produces fine-grained + // sub-field status entries itself. If it threw before doing so + // (e.g. NPE on a sub-field accessor, or a cast failure on a + // generic parameter), attribute the failure to the top-level + // group derived from the input model's @XmlRootElement name. + final String fallbackKey = ServiceResultUtil.entityType(input.getClass()); + statusMap.put(fallbackKey, ServiceResultUtil.toErrorStatus(fallbackKey, e)); + } + } + + protected _AllModelStatus setEntity( + final String entityType, + final I input, + final Function updateFunction, + final Consumer resultConsumer, + final Map statusMap) { + + if (input == null) { + return null; + } + + try { + final O updatedEntity = updateFunction.apply(input); + resultConsumer.accept(updatedEntity); + final _AllModelStatus status = _AllModelStatus.success(); + statusMap.put(entityType, status); + return status; + } catch (Exception e) { + final _AllModelStatus status = ServiceResultUtil.toErrorStatus(entityType, e); + statusMap.put(entityType, status); + return status; + } + } + + @SuppressWarnings("unchecked") + protected _AllModelStatus setEntities( + final String entityType, + final Map entityMap, + final Function, Map> updateFunction, + final Consumer> resultConsumer, + final Map statusMap) { + + if (entityMap == null || entityMap.isEmpty()) { + return null; + } + + try { + final Map updatedEntities = updateFunction.apply(entityMap); + resultConsumer.accept((Map) updatedEntities); + final _AllModelStatus status = _AllModelStatus.success(); + statusMap.put(entityType, status); + return status; + } catch (Exception e) { + final _AllModelStatus status = ServiceResultUtil.toErrorStatus(entityType, e); + statusMap.put(entityType, status); + return status; + } + } + +} diff --git a/commons/src/main/java/com/deftdevs/bootstrapi/commons/service/api/AuthenticationService.java b/commons/src/main/java/com/deftdevs/bootstrapi/commons/service/api/AuthenticationService.java index 871806a1..a248a904 100644 --- a/commons/src/main/java/com/deftdevs/bootstrapi/commons/service/api/AuthenticationService.java +++ b/commons/src/main/java/com/deftdevs/bootstrapi/commons/service/api/AuthenticationService.java @@ -1,12 +1,51 @@ package com.deftdevs.bootstrapi.commons.service.api; import com.deftdevs.bootstrapi.commons.model.AbstractAuthenticationIdpModel; +import com.deftdevs.bootstrapi.commons.model.AuthenticationModel; import com.deftdevs.bootstrapi.commons.model.AuthenticationSsoModel; +import com.deftdevs.bootstrapi.commons.model.type._AllModelStatus; +import com.deftdevs.bootstrapi.commons.model.type.ServiceResult; +import com.deftdevs.bootstrapi.commons.util.ServiceResultUtil; +import java.util.LinkedHashMap; import java.util.Map; public interface AuthenticationService { + @SuppressWarnings("unchecked") + default AuthenticationModel getAuthentication() { + return new AuthenticationModel( + (Map) getAuthenticationIdps(), + getAuthenticationSso()); + } + + @SuppressWarnings("unchecked") + default ServiceResult setAuthentication(final AuthenticationModel authenticationModel) { + final AuthenticationModel result = new AuthenticationModel(); + final Map status = new LinkedHashMap<>(); + + if (authenticationModel.getIdps() != null) { + final String key = ServiceResultUtil.subEntityKey(AbstractAuthenticationIdpModel.class); + try { + result.setIdps((Map) setAuthenticationIdps( + (Map) authenticationModel.getIdps())); + status.put(key, _AllModelStatus.success()); + } catch (Exception e) { + status.put(key, ServiceResultUtil.toErrorStatus(key, e)); + } + } + if (authenticationModel.getSso() != null) { + final String key = ServiceResultUtil.subEntityKey(AuthenticationSsoModel.class); + try { + result.setSso(setAuthenticationSso((SB) authenticationModel.getSso())); + status.put(key, _AllModelStatus.success()); + } catch (Exception e) { + status.put(key, ServiceResultUtil.toErrorStatus(key, e)); + } + } + return new ServiceResult<>(result, status); + } + Map getAuthenticationIdps(); Map setAuthenticationIdps( diff --git a/commons/src/main/java/com/deftdevs/bootstrapi/commons/service/api/LicensesService.java b/commons/src/main/java/com/deftdevs/bootstrapi/commons/service/api/LicensesService.java index 8f07d738..6c39a758 100644 --- a/commons/src/main/java/com/deftdevs/bootstrapi/commons/service/api/LicensesService.java +++ b/commons/src/main/java/com/deftdevs/bootstrapi/commons/service/api/LicensesService.java @@ -3,6 +3,7 @@ import com.deftdevs.bootstrapi.commons.model.LicenseModel; import java.util.List; +import java.util.Map; public interface LicensesService { @@ -21,6 +22,19 @@ public interface LicensesService { List setLicenses( List licenseKeys); + /** + * Set licenses from a map keyed by license key. + *

+ * Input values may be {@code null} (key-only entries are valid); the map keys are the + * license keys to apply. The returned map is keyed by a redacted form of each input + * key so that full license keys are not echoed in the response. + * + * @param licenseInputs map of license key to (optional) input model; values are ignored + * @return map of redacted-key to applied {@link LicenseModel} + */ + Map setLicenses( + Map licenseInputs); + /** * Add a single license * diff --git a/commons/src/main/java/com/deftdevs/bootstrapi/commons/service/api/MailServerService.java b/commons/src/main/java/com/deftdevs/bootstrapi/commons/service/api/MailServerService.java index e38f5c5c..160fcfbd 100644 --- a/commons/src/main/java/com/deftdevs/bootstrapi/commons/service/api/MailServerService.java +++ b/commons/src/main/java/com/deftdevs/bootstrapi/commons/service/api/MailServerService.java @@ -1,11 +1,46 @@ package com.deftdevs.bootstrapi.commons.service.api; +import com.deftdevs.bootstrapi.commons.model.MailServerModel; import com.deftdevs.bootstrapi.commons.model.MailServerPopModel; import com.deftdevs.bootstrapi.commons.model.MailServerSmtpModel; +import com.deftdevs.bootstrapi.commons.model.type._AllModelStatus; +import com.deftdevs.bootstrapi.commons.model.type.ServiceResult; +import com.deftdevs.bootstrapi.commons.util.ServiceResultUtil; +import java.util.LinkedHashMap; +import java.util.Map; public interface MailServerService { + default MailServerModel getMailServer() { + return new MailServerModel(getMailServerSmtp(), getMailServerPop()); + } + + default ServiceResult setMailServer(final MailServerModel mailServerModel) { + final MailServerModel result = new MailServerModel(); + final Map status = new LinkedHashMap<>(); + + if (mailServerModel.getSmtp() != null) { + final String key = ServiceResultUtil.subEntityKey(MailServerSmtpModel.class); + try { + result.setSmtp(setMailServerSmtp(mailServerModel.getSmtp())); + status.put(key, _AllModelStatus.success()); + } catch (Exception e) { + status.put(key, ServiceResultUtil.toErrorStatus(key, e)); + } + } + if (mailServerModel.getPop() != null) { + final String key = ServiceResultUtil.subEntityKey(MailServerPopModel.class); + try { + result.setPop(setMailServerPop(mailServerModel.getPop())); + status.put(key, _AllModelStatus.success()); + } catch (Exception e) { + status.put(key, ServiceResultUtil.toErrorStatus(key, e)); + } + } + return new ServiceResult<>(result, status); + } + /** * Get the smtp mailserver settings. * diff --git a/commons/src/main/java/com/deftdevs/bootstrapi/commons/service/api/SettingsService.java b/commons/src/main/java/com/deftdevs/bootstrapi/commons/service/api/SettingsGeneralService.java similarity index 72% rename from commons/src/main/java/com/deftdevs/bootstrapi/commons/service/api/SettingsService.java rename to commons/src/main/java/com/deftdevs/bootstrapi/commons/service/api/SettingsGeneralService.java index 7bbf8fa2..bb41c8e0 100644 --- a/commons/src/main/java/com/deftdevs/bootstrapi/commons/service/api/SettingsService.java +++ b/commons/src/main/java/com/deftdevs/bootstrapi/commons/service/api/SettingsGeneralService.java @@ -1,9 +1,9 @@ package com.deftdevs.bootstrapi.commons.service.api; -import com.deftdevs.bootstrapi.commons.model.SettingsModel; +import com.deftdevs.bootstrapi.commons.model.SettingsGeneralModel; -public interface SettingsService { +public interface SettingsGeneralService { /** * Get the settings. diff --git a/commons/src/main/java/com/deftdevs/bootstrapi/commons/service/api/_AllService.java b/commons/src/main/java/com/deftdevs/bootstrapi/commons/service/api/_AllService.java new file mode 100644 index 00000000..9afbbbf9 --- /dev/null +++ b/commons/src/main/java/com/deftdevs/bootstrapi/commons/service/api/_AllService.java @@ -0,0 +1,16 @@ +package com.deftdevs.bootstrapi.commons.service.api; + +import com.deftdevs.bootstrapi.commons.model.type._AllModelAccessor; + +public interface _AllService<_AllModel extends _AllModelAccessor> { + + /** + * Apply a complete configuration. + * + * @param allModel the configuration to apply + * @return the updated configuration with status + */ + _AllModel setAll( + _AllModel allModel); + +} diff --git a/commons/src/main/java/com/deftdevs/bootstrapi/commons/util/LicenseKeyRedactor.java b/commons/src/main/java/com/deftdevs/bootstrapi/commons/util/LicenseKeyRedactor.java new file mode 100644 index 00000000..ade5c82c --- /dev/null +++ b/commons/src/main/java/com/deftdevs/bootstrapi/commons/util/LicenseKeyRedactor.java @@ -0,0 +1,64 @@ +package com.deftdevs.bootstrapi.commons.util; + +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +/** + * Redacts license keys for inclusion in HTTP responses. + *

+ * License keys are sensitive and should not be echoed back in full to clients, + * since responses may be captured by intermediaries (proxies, logs, etc.) that + * do not capture the original request body. Redacted keys allow a caller to + * correlate a response entry with the key they sent without leaking the full + * value. + *

+ * Format: {@code ...#}, where {@code hash4} is the first + * four hex characters of the SHA-1 digest of the full key. The hash suffix + * disambiguates keys that happen to share the same prefix and suffix (e.g. + * Atlassian license keys, which all start with the same product header). + *

+ * Keys shorter than nine characters are hashed entirely and emitted as + * {@code ***#} to avoid leaking the full key when redaction would be + * effectively a no-op. + */ +public final class LicenseKeyRedactor { + + private static final int VISIBLE_PREFIX_LENGTH = 4; + private static final int VISIBLE_SUFFIX_LENGTH = 4; + private static final int HASH_LENGTH = 4; + private static final int MIN_REDACTABLE_LENGTH = VISIBLE_PREFIX_LENGTH + VISIBLE_SUFFIX_LENGTH + 1; + + private LicenseKeyRedactor() { + } + + public static String redact(final String key) { + if (key == null) { + return null; + } + final String hash = shortSha1(key); + if (key.length() < MIN_REDACTABLE_LENGTH) { + return "***#" + hash; + } + return key.substring(0, VISIBLE_PREFIX_LENGTH) + + "..." + + key.substring(key.length() - VISIBLE_SUFFIX_LENGTH) + + "#" + + hash; + } + + private static String shortSha1(final String input) { + try { + final MessageDigest md = MessageDigest.getInstance("SHA-1"); // NOSONAR: Hash is just used for license collisions + final byte[] digest = md.digest(input.getBytes(StandardCharsets.UTF_8)); + final StringBuilder hex = new StringBuilder(HASH_LENGTH); + for (int i = 0; i < (HASH_LENGTH + 1) / 2; i++) { + hex.append(String.format("%02x", digest[i] & 0xff)); + } + return hex.substring(0, HASH_LENGTH); + } catch (NoSuchAlgorithmException e) { + throw new IllegalStateException("SHA-1 not available", e); + } + } + +} diff --git a/commons/src/main/java/com/deftdevs/bootstrapi/commons/util/ServiceResultUtil.java b/commons/src/main/java/com/deftdevs/bootstrapi/commons/util/ServiceResultUtil.java new file mode 100644 index 00000000..24d1f9e0 --- /dev/null +++ b/commons/src/main/java/com/deftdevs/bootstrapi/commons/util/ServiceResultUtil.java @@ -0,0 +1,97 @@ +package com.deftdevs.bootstrapi.commons.util; + +import com.deftdevs.bootstrapi.commons.model.type._AllModelStatus; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.Response; +import javax.xml.bind.annotation.XmlRootElement; + +public class ServiceResultUtil { + + private static final Logger log = LoggerFactory.getLogger(ServiceResultUtil.class); + + /** + * Returns the raw {@code @XmlRootElement(name = ...)} of a model class. + * Used as the top-level status-map key for non-composite entries + * (e.g. {@code mail-templates}, {@code session-config}). Dashes in the + * name are preserved — composite "group/sub" keys are produced by the + * {@link #subEntityKey(Class)} overload instead. + */ + public static String entityType(final Class entityClass) { + final XmlRootElement rootElement = entityClass.getAnnotation(XmlRootElement.class); + if (rootElement == null || "##default".equals(rootElement.name())) { + throw new IllegalArgumentException( + entityClass.getName() + " must have @XmlRootElement with explicit name"); + } + return rootElement.name(); + } + + /** + * Returns the status-map key for a sub-field of a composite group, + * derived from the leaf model's {@code @XmlRootElement(name = ...)} + * by replacing the last dash with a slash. The convention assumes + * sub-field models are named {@code -}, where the group + * itself may contain dashes but the sub-name may not + * (e.g. {@code mail-server-smtp} → {@code mail-server/smtp}). + * Single-dash names map straightforwardly + * (e.g. {@code settings-general} → {@code settings/general}, + * {@code authentication-idps} → {@code authentication/idps}, + * {@code permissions-global} → {@code permissions/global}). + * If the sub-name itself contains dashes (e.g. {@code settings-custom-html}), + * use {@link #subEntityKey(Class, Class)} instead. + * + * @throws IllegalArgumentException if the model name contains no dash + * (i.e. the model is not a sub-field model — use {@link #entityType(Class)} instead) + */ + public static String subEntityKey(final Class entityClass) { + final String name = entityType(entityClass); + final int dash = name.lastIndexOf('-'); + if (dash < 0) { + throw new IllegalArgumentException( + entityClass.getName() + " is not a sub-field model: name '" + name + + "' has no dash separator"); + } + return name.substring(0, dash) + "/" + name.substring(dash + 1); + } + + /** + * Returns the status-map key {@code /} for a sub-field model + * whose {@code @XmlRootElement} name is prefixed by the group model's + * {@code @XmlRootElement} name. Unlike {@link #subEntityKey(Class)}, this + * works for sub-names that themselves contain dashes + * (e.g. group {@code settings} + leaf {@code settings-custom-html} + * → {@code settings/custom-html}). + * + * @throws IllegalArgumentException if the leaf model's name does not + * start with the group model's name followed by a dash + */ + public static String subEntityKey(final Class groupClass, final Class entityClass) { + final String groupName = entityType(groupClass); + final String name = entityType(entityClass); + if (!name.startsWith(groupName + "-")) { + throw new IllegalArgumentException( + entityClass.getName() + " is not a sub-field model of group '" + groupName + + "': name '" + name + "' does not start with '" + groupName + "-'"); + } + return groupName + "/" + name.substring(groupName.length() + 1); + } + + public static _AllModelStatus toErrorStatus(final String entityType, final Exception e) { + final String message = String.format("Failed to apply %s configuration", entityType); + if (e instanceof WebApplicationException) { + final Response response = ((WebApplicationException) e).getResponse(); + final int statusCode = response != null + ? response.getStatus() + : Response.Status.INTERNAL_SERVER_ERROR.getStatusCode(); + return _AllModelStatus.error(statusCode, message, e.getMessage()); + } + log.warn("Unexpected error applying {} configuration", entityType, e); + return _AllModelStatus.error(Response.Status.INTERNAL_SERVER_ERROR, message, null); + } + + private ServiceResultUtil() { + } + +} diff --git a/commons/src/test/java/com/deftdevs/bootstrapi/commons/model/SettingsModelTest.java b/commons/src/test/java/com/deftdevs/bootstrapi/commons/model/SettingsGeneralModelTest.java similarity index 100% rename from commons/src/test/java/com/deftdevs/bootstrapi/commons/model/SettingsModelTest.java rename to commons/src/test/java/com/deftdevs/bootstrapi/commons/model/SettingsGeneralModelTest.java diff --git a/commons/src/test/java/com/deftdevs/bootstrapi/commons/rest/SettingsResourceTest.java b/commons/src/test/java/com/deftdevs/bootstrapi/commons/rest/SettingsGeneralResourceTest.java similarity index 56% rename from commons/src/test/java/com/deftdevs/bootstrapi/commons/rest/SettingsResourceTest.java rename to commons/src/test/java/com/deftdevs/bootstrapi/commons/rest/SettingsGeneralResourceTest.java index 588bd515..327076d4 100644 --- a/commons/src/test/java/com/deftdevs/bootstrapi/commons/rest/SettingsResourceTest.java +++ b/commons/src/test/java/com/deftdevs/bootstrapi/commons/rest/SettingsGeneralResourceTest.java @@ -1,8 +1,8 @@ package com.deftdevs.bootstrapi.commons.rest; -import com.deftdevs.bootstrapi.commons.model.SettingsModel; -import com.deftdevs.bootstrapi.commons.rest.impl.TestSettingsResourceImpl; -import com.deftdevs.bootstrapi.commons.service.api.SettingsService; +import com.deftdevs.bootstrapi.commons.model.SettingsGeneralModel; +import com.deftdevs.bootstrapi.commons.rest.impl.TestSettingsGeneralResourceImpl; +import com.deftdevs.bootstrapi.commons.service.api.SettingsGeneralService; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -15,40 +15,40 @@ import static org.mockito.Mockito.doReturn; @ExtendWith(MockitoExtension.class) -class SettingsResourceTest { +class SettingsGeneralResourceTest { @Mock - private SettingsService settingsService; + private SettingsGeneralService settingsService; - private TestSettingsResourceImpl resource; + private TestSettingsGeneralResourceImpl resource; @BeforeEach public void setup() { - resource = new TestSettingsResourceImpl(settingsService); + resource = new TestSettingsGeneralResourceImpl(settingsService); } @Test void testGetSettings() { - final SettingsModel bean = SettingsModel.EXAMPLE_1; + final SettingsGeneralModel bean = SettingsGeneralModel.EXAMPLE_1; doReturn(bean).when(settingsService).getSettingsGeneral(); final Response response = resource.getSettings(); assertEquals(200, response.getStatus()); - final SettingsModel settingsModel = (SettingsModel) response.getEntity(); + final SettingsGeneralModel settingsModel = (SettingsGeneralModel) response.getEntity(); assertEquals(settingsModel, bean); } @Test void testSetSettings() { - final SettingsModel bean = SettingsModel.EXAMPLE_1; + final SettingsGeneralModel bean = SettingsGeneralModel.EXAMPLE_1; doReturn(bean).when(settingsService).setSettingsGeneral(bean); final Response response = resource.setSettings(bean); assertEquals(200, response.getStatus()); - final SettingsModel settingsModel = (SettingsModel) response.getEntity(); + final SettingsGeneralModel settingsModel = (SettingsGeneralModel) response.getEntity(); assertEquals(settingsModel, bean); } diff --git a/commons/src/test/java/com/deftdevs/bootstrapi/commons/rest/_AbstractAllResourceImplTest.java b/commons/src/test/java/com/deftdevs/bootstrapi/commons/rest/_AbstractAllResourceImplTest.java new file mode 100644 index 00000000..f7742bb9 --- /dev/null +++ b/commons/src/test/java/com/deftdevs/bootstrapi/commons/rest/_AbstractAllResourceImplTest.java @@ -0,0 +1,65 @@ +package com.deftdevs.bootstrapi.commons.rest; + +import com.deftdevs.bootstrapi.commons.model.type._AllModelStatus; +import org.junit.jupiter.api.Test; + +import javax.ws.rs.core.Response; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class _AbstractAllResourceImplTest { + + @Test + void emptyMapYieldsOk() { + assertEquals(200, _AbstractAllResourceImpl.computeOverallStatus(Collections.emptyMap())); + } + + @Test + void nullMapYieldsOk() { + assertEquals(200, _AbstractAllResourceImpl.computeOverallStatus(null)); + } + + @Test + void allSuccessYieldsOk() { + final Map map = new LinkedHashMap<>(); + map.put("a", _AllModelStatus.success()); + map.put("b", _AllModelStatus.success()); + assertEquals(200, _AbstractAllResourceImpl.computeOverallStatus(map)); + } + + @Test + void anyServerErrorYields500() { + final Map map = new LinkedHashMap<>(); + map.put("a", _AllModelStatus.success()); + map.put("b", _AllModelStatus.error(Response.Status.BAD_REQUEST, "x", null)); + map.put("c", _AllModelStatus.error(Response.Status.INTERNAL_SERVER_ERROR, "x", null)); + assertEquals(500, _AbstractAllResourceImpl.computeOverallStatus(map)); + } + + @Test + void mixedSuccessAndClientErrorYields207() { + final Map map = new LinkedHashMap<>(); + map.put("a", _AllModelStatus.success()); + map.put("b", _AllModelStatus.error(Response.Status.BAD_REQUEST, "x", null)); + assertEquals(_AbstractAllResourceImpl.MULTI_STATUS, _AbstractAllResourceImpl.computeOverallStatus(map)); + } + + @Test + void allSameClientErrorBubblesThat() { + final Map map = new LinkedHashMap<>(); + map.put("a", _AllModelStatus.error(Response.Status.BAD_REQUEST, "x", null)); + map.put("b", _AllModelStatus.error(Response.Status.BAD_REQUEST, "y", null)); + assertEquals(400, _AbstractAllResourceImpl.computeOverallStatus(map)); + } + + @Test + void differentClientErrorsYield207() { + final Map map = new LinkedHashMap<>(); + map.put("a", _AllModelStatus.error(Response.Status.BAD_REQUEST, "x", null)); + map.put("b", _AllModelStatus.error(Response.Status.CONFLICT, "y", null)); + assertEquals(_AbstractAllResourceImpl.MULTI_STATUS, _AbstractAllResourceImpl.computeOverallStatus(map)); + } +} diff --git a/commons/src/test/java/com/deftdevs/bootstrapi/commons/rest/impl/TestSettingsGeneralResourceImpl.java b/commons/src/test/java/com/deftdevs/bootstrapi/commons/rest/impl/TestSettingsGeneralResourceImpl.java new file mode 100644 index 00000000..135137c6 --- /dev/null +++ b/commons/src/test/java/com/deftdevs/bootstrapi/commons/rest/impl/TestSettingsGeneralResourceImpl.java @@ -0,0 +1,12 @@ +package com.deftdevs.bootstrapi.commons.rest.impl; + +import com.deftdevs.bootstrapi.commons.rest.AbstractSettingsGeneralResourceImpl; +import com.deftdevs.bootstrapi.commons.service.api.SettingsGeneralService; + +public class TestSettingsGeneralResourceImpl extends AbstractSettingsGeneralResourceImpl { + + public TestSettingsGeneralResourceImpl(SettingsGeneralService settingsService) { + super(settingsService); + } + +} diff --git a/commons/src/test/java/com/deftdevs/bootstrapi/commons/rest/impl/TestSettingsResourceImpl.java b/commons/src/test/java/com/deftdevs/bootstrapi/commons/rest/impl/TestSettingsResourceImpl.java deleted file mode 100644 index db9c2e44..00000000 --- a/commons/src/test/java/com/deftdevs/bootstrapi/commons/rest/impl/TestSettingsResourceImpl.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.deftdevs.bootstrapi.commons.rest.impl; - -import com.deftdevs.bootstrapi.commons.rest.AbstractSettingsResourceImpl; -import com.deftdevs.bootstrapi.commons.service.api.SettingsService; - -public class TestSettingsResourceImpl extends AbstractSettingsResourceImpl { - - public TestSettingsResourceImpl(SettingsService settingsService) { - super(settingsService); - } - -} diff --git a/commons/src/test/java/com/deftdevs/bootstrapi/commons/service/_AbstractAllServiceImplTest.java b/commons/src/test/java/com/deftdevs/bootstrapi/commons/service/_AbstractAllServiceImplTest.java new file mode 100644 index 00000000..0668496e --- /dev/null +++ b/commons/src/test/java/com/deftdevs/bootstrapi/commons/service/_AbstractAllServiceImplTest.java @@ -0,0 +1,200 @@ +package com.deftdevs.bootstrapi.commons.service; + +import com.deftdevs.bootstrapi.commons.model.SettingsGeneralModel; +import com.deftdevs.bootstrapi.commons.model.type.ServiceResult; +import com.deftdevs.bootstrapi.commons.model.type._AllModelAccessor; +import com.deftdevs.bootstrapi.commons.model.type._AllModelStatus; +import org.junit.jupiter.api.Test; + +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.Response; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.concurrent.atomic.AtomicReference; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class _AbstractAllServiceImplTest { + + private static class TestAllModel implements _AllModelAccessor { + + private final Map status = new HashMap<>(); + + @Override + public Map getStatus() { + return status; + } + } + + private static class TestAllService extends _AbstractAllServiceImpl { + + @Override + public TestAllModel setAll(final TestAllModel allModel) { + return allModel; + } + } + + private final TestAllService service = new TestAllService(); + + // setEntity + + @Test + void setEntitySkipsNullInput() { + final Map statusMap = new HashMap<>(); + + final _AllModelStatus status = service.setEntity("entity", null, + input -> input, result -> {}, statusMap); + + assertNull(status); + assertTrue(statusMap.isEmpty()); + } + + @Test + void setEntitySuccessRecordsStatusAndResult() { + final Map statusMap = new HashMap<>(); + final AtomicReference result = new AtomicReference<>(); + + final _AllModelStatus status = service.setEntity("entity", "input", + input -> input + "-updated", result::set, statusMap); + + assertEquals(Response.Status.OK.getStatusCode(), status.getStatus()); + assertEquals("input-updated", result.get()); + assertSame(status, statusMap.get("entity")); + } + + @Test + void setEntityUsesWebApplicationExceptionStatusCode() { + final Map statusMap = new HashMap<>(); + + final _AllModelStatus status = service.setEntity("entity", "input", + input -> { + throw new WebApplicationException("conflict", Response.Status.CONFLICT); + }, + result -> {}, statusMap); + + assertEquals(Response.Status.CONFLICT.getStatusCode(), status.getStatus()); + assertEquals("Failed to apply entity configuration", status.getMessage()); + assertSame(status, statusMap.get("entity")); + } + + @Test + void setEntityMapsUnexpectedExceptionTo500WithoutDetails() { + final Map statusMap = new HashMap<>(); + + final _AllModelStatus status = service.setEntity("entity", "input", + input -> { + throw new IllegalStateException("internal details that must not leak"); + }, + result -> {}, statusMap); + + assertEquals(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode(), status.getStatus()); + assertNull(status.getDetails()); + } + + // setEntities + + @Test + void setEntitiesSkipsNullAndEmptyMap() { + final Map statusMap = new HashMap<>(); + + assertNull(service.setEntities("entities", null, + entities -> entities, result -> {}, statusMap)); + assertNull(service.setEntities("entities", Collections.emptyMap(), + entities -> entities, result -> {}, statusMap)); + assertTrue(statusMap.isEmpty()); + } + + @Test + void setEntitiesSuccessRecordsStatusAndResult() { + final Map statusMap = new HashMap<>(); + final AtomicReference> result = new AtomicReference<>(); + final Map entities = Collections.singletonMap("key", "value"); + + final _AllModelStatus status = service.setEntities("entities", entities, + input -> input, result::set, statusMap); + + assertEquals(Response.Status.OK.getStatusCode(), status.getStatus()); + assertEquals(entities, result.get()); + assertSame(status, statusMap.get("entities")); + } + + @Test + void setEntitiesRecordsErrorStatusOnException() { + final Map statusMap = new HashMap<>(); + final Map entities = Collections.singletonMap("key", "value"); + + final _AllModelStatus status = service.setEntities("entities", entities, + input -> { + throw new WebApplicationException(Response.Status.BAD_REQUEST); + }, + result -> {}, statusMap); + + assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), status.getStatus()); + assertSame(status, statusMap.get("entities")); + } + + // setEntityWithStatus + + @Test + void setEntityWithStatusSkipsNullInput() { + final Map statusMap = new HashMap<>(); + + service.setEntityWithStatus(null, + input -> new ServiceResult<>(input, Collections.emptyMap()), + result -> {}, statusMap); + + assertTrue(statusMap.isEmpty()); + } + + @Test + void setEntityWithStatusMergesSubFieldStatuses() { + final Map statusMap = new HashMap<>(); + final AtomicReference result = new AtomicReference<>(); + + final Map subStatus = new LinkedHashMap<>(); + subStatus.put("settings/general", _AllModelStatus.success()); + subStatus.put("settings/security", _AllModelStatus.error(Response.Status.BAD_REQUEST, "bad", null)); + + service.setEntityWithStatus("input", + input -> new ServiceResult<>(input + "-updated", subStatus), + result::set, statusMap); + + assertEquals("input-updated", result.get()); + assertEquals(2, statusMap.size()); + assertEquals(Response.Status.OK.getStatusCode(), statusMap.get("settings/general").getStatus()); + assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), statusMap.get("settings/security").getStatus()); + } + + @Test + void setEntityWithStatusFallsBackToRootElementNameOnThrow() { + final Map statusMap = new HashMap<>(); + + service.setEntityWithStatus(SettingsGeneralModel.EXAMPLE_1, + input -> { + throw new IllegalStateException("boom"); + }, + result -> {}, statusMap); + + // fallback key is the input model's @XmlRootElement name + final _AllModelStatus status = statusMap.get("settings-general"); + assertEquals(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode(), status.getStatus()); + } + + @Test + void setEntityWithStatusUsesWebApplicationExceptionStatusCodeOnThrow() { + final Map statusMap = new HashMap<>(); + + service.setEntityWithStatus(SettingsGeneralModel.EXAMPLE_1, + input -> { + throw new WebApplicationException(Response.Status.CONFLICT); + }, + result -> {}, statusMap); + + assertEquals(Response.Status.CONFLICT.getStatusCode(), statusMap.get("settings-general").getStatus()); + } +} diff --git a/commons/src/test/java/com/deftdevs/bootstrapi/commons/service/api/AuthenticationServiceTest.java b/commons/src/test/java/com/deftdevs/bootstrapi/commons/service/api/AuthenticationServiceTest.java new file mode 100644 index 00000000..050052df --- /dev/null +++ b/commons/src/test/java/com/deftdevs/bootstrapi/commons/service/api/AuthenticationServiceTest.java @@ -0,0 +1,88 @@ +package com.deftdevs.bootstrapi.commons.service.api; + +import com.deftdevs.bootstrapi.commons.exception.web.BadRequestException; +import com.deftdevs.bootstrapi.commons.model.AbstractAuthenticationIdpModel; +import com.deftdevs.bootstrapi.commons.model.AuthenticationModel; +import com.deftdevs.bootstrapi.commons.model.AuthenticationSsoModel; +import com.deftdevs.bootstrapi.commons.model.type.ServiceResult; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import javax.ws.rs.core.Response; +import java.util.Collections; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.CALLS_REAL_METHODS; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; + +class AuthenticationServiceTest { + + private AuthenticationService authenticationService; + + private AbstractAuthenticationIdpModel idpModel; + private AuthenticationSsoModel ssoModel; + private Map idpModels; + + @BeforeEach + @SuppressWarnings("unchecked") + void setup() { + authenticationService = mock(AuthenticationService.class, CALLS_REAL_METHODS); + + idpModel = mock(AbstractAuthenticationIdpModel.class); + ssoModel = new AuthenticationSsoModel(); + idpModels = Collections.singletonMap("idp", idpModel); + } + + @Test + void testGetAuthentication() { + doReturn(idpModels).when(authenticationService).getAuthenticationIdps(); + doReturn(ssoModel).when(authenticationService).getAuthenticationSso(); + + final AuthenticationModel authenticationModel = authenticationService.getAuthentication(); + + assertEquals(idpModels, authenticationModel.getIdps()); + assertEquals(ssoModel, authenticationModel.getSso()); + } + + @Test + void testSetAuthenticationAppliesIdpsAndSso() { + doReturn(idpModels).when(authenticationService).setAuthenticationIdps(idpModels); + doReturn(ssoModel).when(authenticationService).setAuthenticationSso(ssoModel); + + final ServiceResult result = authenticationService.setAuthentication( + new AuthenticationModel(idpModels, ssoModel)); + + assertEquals(idpModels, result.getModel().getIdps()); + assertEquals(ssoModel, result.getModel().getSso()); + assertEquals(Response.Status.OK.getStatusCode(), result.getStatus().get("authentication/idps").getStatus()); + assertEquals(Response.Status.OK.getStatusCode(), result.getStatus().get("authentication/sso").getStatus()); + } + + @Test + void testSetAuthenticationSkipsNullSubFields() { + final ServiceResult result = + authenticationService.setAuthentication(new AuthenticationModel()); + + assertTrue(result.getStatus().isEmpty()); + } + + @Test + void testSetAuthenticationRecordsPerSubFieldFailure() { + doReturn(idpModels).when(authenticationService).setAuthenticationIdps(idpModels); + doThrow(new BadRequestException("invalid sso config")) + .when(authenticationService).setAuthenticationSso(ssoModel); + + final ServiceResult result = authenticationService.setAuthentication( + new AuthenticationModel(idpModels, ssoModel)); + + assertEquals(idpModels, result.getModel().getIdps()); + assertNull(result.getModel().getSso()); + assertEquals(Response.Status.OK.getStatusCode(), result.getStatus().get("authentication/idps").getStatus()); + assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), result.getStatus().get("authentication/sso").getStatus()); + } +} diff --git a/commons/src/test/java/com/deftdevs/bootstrapi/commons/service/api/MailServerServiceTest.java b/commons/src/test/java/com/deftdevs/bootstrapi/commons/service/api/MailServerServiceTest.java new file mode 100644 index 00000000..63b4a32c --- /dev/null +++ b/commons/src/test/java/com/deftdevs/bootstrapi/commons/service/api/MailServerServiceTest.java @@ -0,0 +1,80 @@ +package com.deftdevs.bootstrapi.commons.service.api; + +import com.deftdevs.bootstrapi.commons.exception.web.BadRequestException; +import com.deftdevs.bootstrapi.commons.model.MailServerModel; +import com.deftdevs.bootstrapi.commons.model.MailServerPopModel; +import com.deftdevs.bootstrapi.commons.model.MailServerSmtpModel; +import com.deftdevs.bootstrapi.commons.model.type.ServiceResult; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import javax.ws.rs.core.Response; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.CALLS_REAL_METHODS; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; + +class MailServerServiceTest { + + private MailServerService mailServerService; + + @BeforeEach + void setup() { + mailServerService = mock(MailServerService.class, CALLS_REAL_METHODS); + } + + @Test + void testGetMailServer() { + doReturn(MailServerSmtpModel.EXAMPLE_1).when(mailServerService).getMailServerSmtp(); + doReturn(MailServerPopModel.EXAMPLE_1).when(mailServerService).getMailServerPop(); + + final MailServerModel mailServerModel = mailServerService.getMailServer(); + + assertEquals(MailServerSmtpModel.EXAMPLE_1, mailServerModel.getSmtp()); + assertEquals(MailServerPopModel.EXAMPLE_1, mailServerModel.getPop()); + } + + @Test + void testSetMailServerAppliesSmtpAndPop() { + doReturn(MailServerSmtpModel.EXAMPLE_1).when(mailServerService).setMailServerSmtp(MailServerSmtpModel.EXAMPLE_1); + doReturn(MailServerPopModel.EXAMPLE_1).when(mailServerService).setMailServerPop(MailServerPopModel.EXAMPLE_1); + + final ServiceResult result = mailServerService.setMailServer( + new MailServerModel(MailServerSmtpModel.EXAMPLE_1, MailServerPopModel.EXAMPLE_1)); + + assertEquals(MailServerSmtpModel.EXAMPLE_1, result.getModel().getSmtp()); + assertEquals(MailServerPopModel.EXAMPLE_1, result.getModel().getPop()); + assertEquals(Response.Status.OK.getStatusCode(), result.getStatus().get("mail-server/smtp").getStatus()); + assertEquals(Response.Status.OK.getStatusCode(), result.getStatus().get("mail-server/pop").getStatus()); + } + + @Test + void testSetMailServerSkipsNullSubFields() { + final ServiceResult result = mailServerService.setMailServer(new MailServerModel()); + + assertTrue(result.getStatus().isEmpty()); + verify(mailServerService, never()).setMailServerSmtp(MailServerSmtpModel.EXAMPLE_1); + verify(mailServerService, never()).setMailServerPop(MailServerPopModel.EXAMPLE_1); + } + + @Test + void testSetMailServerRecordsPerSubFieldFailure() { + doThrow(new BadRequestException("invalid smtp config")) + .when(mailServerService).setMailServerSmtp(MailServerSmtpModel.EXAMPLE_1); + doReturn(MailServerPopModel.EXAMPLE_1).when(mailServerService).setMailServerPop(MailServerPopModel.EXAMPLE_1); + + final ServiceResult result = mailServerService.setMailServer( + new MailServerModel(MailServerSmtpModel.EXAMPLE_1, MailServerPopModel.EXAMPLE_1)); + + assertNull(result.getModel().getSmtp()); + assertEquals(MailServerPopModel.EXAMPLE_1, result.getModel().getPop()); + assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), result.getStatus().get("mail-server/smtp").getStatus()); + assertEquals(Response.Status.OK.getStatusCode(), result.getStatus().get("mail-server/pop").getStatus()); + } +} diff --git a/commons/src/test/java/com/deftdevs/bootstrapi/commons/util/LicenseKeyRedactorTest.java b/commons/src/test/java/com/deftdevs/bootstrapi/commons/util/LicenseKeyRedactorTest.java new file mode 100644 index 00000000..55cd1c81 --- /dev/null +++ b/commons/src/test/java/com/deftdevs/bootstrapi/commons/util/LicenseKeyRedactorTest.java @@ -0,0 +1,44 @@ +package com.deftdevs.bootstrapi.commons.util; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +class LicenseKeyRedactorTest { + + @Test + void redactsNullAsNull() { + assertNull(LicenseKeyRedactor.redact(null)); + } + + @Test + void redactsShortKeyAsObfuscatedWithHash() { + final String redacted = LicenseKeyRedactor.redact("abc"); + assertEquals("***#a9993e36".substring(0, "***#".length() + 4), redacted); + } + + @Test + void redactsLongKeyShowingFirstAndLastFourPlusHash() { + final String key = "AAAA1234567890ZZZZ"; + final String redacted = LicenseKeyRedactor.redact(key); + assertEquals("AAAA...ZZZZ#", redacted.substring(0, "AAAA...ZZZZ#".length())); + assertEquals(4, redacted.length() - "AAAA...ZZZZ#".length()); + } + + @Test + void differentKeysWithSamePrefixAndSuffixGetDifferentHashes() { + final String a = LicenseKeyRedactor.redact("AAAA0000ZZZZ"); + final String b = LicenseKeyRedactor.redact("AAAA1111ZZZZ"); + assertEquals(a.substring(0, "AAAA...ZZZZ#".length()), b.substring(0, "AAAA...ZZZZ#".length())); + assertNotEquals(a, b); + } + + @Test + void redactionIsDeterministic() { + final String key = "AAAA-some-very-long-license-key-payload-ZZZZ"; + assertEquals(LicenseKeyRedactor.redact(key), LicenseKeyRedactor.redact(key)); + } + +} diff --git a/commons/src/test/java/com/deftdevs/bootstrapi/commons/util/ServiceResultUtilTest.java b/commons/src/test/java/com/deftdevs/bootstrapi/commons/util/ServiceResultUtilTest.java new file mode 100644 index 00000000..813acb24 --- /dev/null +++ b/commons/src/test/java/com/deftdevs/bootstrapi/commons/util/ServiceResultUtilTest.java @@ -0,0 +1,111 @@ +package com.deftdevs.bootstrapi.commons.util; + +import com.deftdevs.bootstrapi.commons.model.type._AllModelStatus; +import org.junit.jupiter.api.Test; + +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.Response; +import javax.xml.bind.annotation.XmlRootElement; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class ServiceResultUtilTest { + + @XmlRootElement(name = "leaf") + private static class LeafModel { + } + + @XmlRootElement(name = "settings-general") + private static class SubFieldSingleDash { + } + + @XmlRootElement(name = "mail-server-smtp") + private static class SubFieldMultiDash { + } + + @XmlRootElement(name = "settings") + private static class GroupModel { + } + + @XmlRootElement(name = "settings-custom-html") + private static class SubFieldWithDashedSubName { + } + + @XmlRootElement + private static class UnnamedModel { + } + + private static class UnannotatedModel { + } + + @Test + void entityTypeReturnsRawName() { + assertEquals("leaf", ServiceResultUtil.entityType(LeafModel.class)); + } + + @Test + void entityTypeRejectsMissingAnnotation() { + assertThrows(IllegalArgumentException.class, + () -> ServiceResultUtil.entityType(UnannotatedModel.class)); + } + + @Test + void entityTypeRejectsDefaultName() { + assertThrows(IllegalArgumentException.class, + () -> ServiceResultUtil.entityType(UnnamedModel.class)); + } + + @Test + void subEntityKeySplitsOnLastDash() { + assertEquals("settings/general", ServiceResultUtil.subEntityKey(SubFieldSingleDash.class)); + assertEquals("mail-server/smtp", ServiceResultUtil.subEntityKey(SubFieldMultiDash.class)); + } + + @Test + void subEntityKeyRejectsNonCompositeName() { + assertThrows(IllegalArgumentException.class, + () -> ServiceResultUtil.subEntityKey(LeafModel.class)); + } + + @Test + void groupAwareSubEntityKeyStripsGroupPrefix() { + assertEquals("settings/custom-html", + ServiceResultUtil.subEntityKey(GroupModel.class, SubFieldWithDashedSubName.class)); + assertEquals("settings/general", + ServiceResultUtil.subEntityKey(GroupModel.class, SubFieldSingleDash.class)); + } + + @Test + void groupAwareSubEntityKeyRejectsNonMatchingGroup() { + assertThrows(IllegalArgumentException.class, + () -> ServiceResultUtil.subEntityKey(GroupModel.class, SubFieldMultiDash.class)); + } + + @Test + void toErrorStatusWithStandardWaeUsesItsStatusCode() { + final WebApplicationException e = new WebApplicationException("bad input", 400); + final _AllModelStatus status = ServiceResultUtil.toErrorStatus("settings", e); + assertEquals(400, status.getStatus()); + assertEquals("bad input", status.getDetails()); + assertTrue(status.getMessage().contains("settings")); + } + + @Test + void toErrorStatusWithNonStandardCodeDoesNotNpe() { + final WebApplicationException e = new WebApplicationException("rate limited", + Response.status(429).build()); + final _AllModelStatus status = ServiceResultUtil.toErrorStatus("settings", e); + assertEquals(429, status.getStatus()); + } + + @Test + void toErrorStatusForNonWaeIsGenericAndDropsDetails() { + final _AllModelStatus status = ServiceResultUtil.toErrorStatus("settings", + new RuntimeException("internal secret")); + assertEquals(500, status.getStatus()); + assertNull(status.getDetails()); + } +} diff --git a/commons/src/test/java/it/com/deftdevs/bootstrapi/commons/rest/AbstractSettingsResourceFuncTest.java b/commons/src/test/java/it/com/deftdevs/bootstrapi/commons/rest/AbstractSettingsGeneralResourceFuncTest.java similarity index 84% rename from commons/src/test/java/it/com/deftdevs/bootstrapi/commons/rest/AbstractSettingsResourceFuncTest.java rename to commons/src/test/java/it/com/deftdevs/bootstrapi/commons/rest/AbstractSettingsGeneralResourceFuncTest.java index 91266a6f..88cb147f 100644 --- a/commons/src/test/java/it/com/deftdevs/bootstrapi/commons/rest/AbstractSettingsResourceFuncTest.java +++ b/commons/src/test/java/it/com/deftdevs/bootstrapi/commons/rest/AbstractSettingsGeneralResourceFuncTest.java @@ -1,7 +1,7 @@ package it.com.deftdevs.bootstrapi.commons.rest; import com.deftdevs.bootstrapi.commons.constants.BootstrAPI; -import com.deftdevs.bootstrapi.commons.model.SettingsModel; +import com.deftdevs.bootstrapi.commons.model.SettingsGeneralModel; import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.jupiter.api.Test; @@ -11,7 +11,7 @@ import static org.junit.jupiter.api.Assertions.*; -public abstract class AbstractSettingsResourceFuncTest { +public abstract class AbstractSettingsGeneralResourceFuncTest { private final ObjectMapper objectMapper = new ObjectMapper(); @@ -21,7 +21,7 @@ void testGetSettings() throws Exception { .request(); assertEquals(Response.Status.OK.getStatusCode(), settingsResponse.statusCode()); - final SettingsModel settingsModel = objectMapper.readValue(settingsResponse.body(), SettingsModel.class); + final SettingsGeneralModel settingsModel = objectMapper.readValue(settingsResponse.body(), SettingsGeneralModel.class); assertNotNull(settingsModel.getTitle()); } @@ -31,7 +31,7 @@ void testSetSettings() throws Exception { .request(HttpMethod.PUT, getExampleModel()); assertEquals(Response.Status.OK.getStatusCode(), settingsResponse.statusCode()); - final SettingsModel settingsModel = objectMapper.readValue(settingsResponse.body(), SettingsModel.class); + final SettingsGeneralModel settingsModel = objectMapper.readValue(settingsResponse.body(), SettingsGeneralModel.class); assertEquals(getExampleModel(), settingsModel); } @@ -73,7 +73,7 @@ void testSetSettingsUnauthorized() throws Exception { assertEquals(Response.Status.FORBIDDEN.getStatusCode(), settingsResponse.statusCode()); } - protected SettingsModel getExampleModel() { - return SettingsModel.EXAMPLE_1; + protected SettingsGeneralModel getExampleModel() { + return SettingsGeneralModel.EXAMPLE_1; } } diff --git a/commons/src/test/java/it/com/deftdevs/bootstrapi/commons/rest/Abstract_AllResourceFuncTest.java b/commons/src/test/java/it/com/deftdevs/bootstrapi/commons/rest/Abstract_AllResourceFuncTest.java new file mode 100644 index 00000000..e3d46d99 --- /dev/null +++ b/commons/src/test/java/it/com/deftdevs/bootstrapi/commons/rest/Abstract_AllResourceFuncTest.java @@ -0,0 +1,121 @@ +package it.com.deftdevs.bootstrapi.commons.rest; + +import com.deftdevs.bootstrapi.commons.model.SettingsGeneralModel; +import com.deftdevs.bootstrapi.commons.util.ServiceResultUtil; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.Test; + +import javax.ws.rs.HttpMethod; +import javax.ws.rs.core.Response; +import java.net.http.HttpResponse; +import java.util.Collection; +import java.util.Collections; +import java.util.Set; +import java.util.TreeSet; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Functional tests for the {@code PUT /} (apply complete configuration) endpoint. + *

+ * The happy-path test only submits the general settings sub-field, since that + * is the only configuration that can be applied safely and deterministically + * on all product test instances. The per-sub-field status aggregation and + * partial-failure behavior are covered by unit tests. + */ +public abstract class Abstract_AllResourceFuncTest { + + /** The _all endpoint is the REST root itself. */ + private static final String ALL_PATH = ""; + + protected static final String SETTINGS_GENERAL_KEY = + ServiceResultUtil.subEntityKey(SettingsGeneralModel.class); + + private final ObjectMapper objectMapper = new ObjectMapper(); + + @Test + void testSetAllAppliesGeneralSettings() throws Exception { + final HttpResponse allResponse = HttpRequestHelper.builder(ALL_PATH) + .request(HttpMethod.PUT, getExampleAllModel()); + assertEquals(Response.Status.OK.getStatusCode(), allResponse.statusCode()); + + final JsonNode allNode = objectMapper.readTree(allResponse.body()); + + final JsonNode generalStatusNode = allNode.path("status").path(SETTINGS_GENERAL_KEY); + assertFalse(generalStatusNode.isMissingNode(), + "expected a '" + SETTINGS_GENERAL_KEY + "' entry in the status map, got: " + allNode.path("status")); + assertEquals(Response.Status.OK.getStatusCode(), generalStatusNode.path("status").asInt()); + + final JsonNode generalNode = allNode.path("settings").path("general"); + assertEquals(getExampleSettingsGeneralModel().getTitle(), generalNode.path("title").asText()); + } + + @Test + void testSetAllEmptyModelIsOkWithEmptyStatus() throws Exception { + final HttpResponse allResponse = HttpRequestHelper.builder(ALL_PATH) + .request(HttpMethod.PUT, Collections.emptyMap()); + assertEquals(Response.Status.OK.getStatusCode(), allResponse.statusCode()); + + final JsonNode allNode = objectMapper.readTree(allResponse.body()); + assertTrue(allNode.path("status").isEmpty(), + "expected an empty status map, got: " + allNode.path("status")); + } + + /** + * PUTs the given payload to the _all endpoint, expects an overall 200 and + * a {@code status} map containing exactly one success entry per given key. + * Keys are usually derived with {@code ServiceResultUtil} so tests share + * the production key-derivation logic instead of repeating literals. + */ + protected void assertSetAllApplied( + final Object allModel, + final Collection expectedSuccessStatusKeys) throws Exception { + + final HttpResponse allResponse = HttpRequestHelper.builder(ALL_PATH) + .request(HttpMethod.PUT, allModel); + assertEquals(Response.Status.OK.getStatusCode(), allResponse.statusCode(), allResponse.body()); + + final JsonNode statusNode = objectMapper.readTree(allResponse.body()).path("status"); + + final Set actualKeys = new TreeSet<>(); + statusNode.fieldNames().forEachRemaining(actualKeys::add); + assertEquals(new TreeSet<>(expectedSuccessStatusKeys), actualKeys, + "status map keys do not match the submitted sub-fields"); + + for (final String key : expectedSuccessStatusKeys) { + assertEquals(Response.Status.OK.getStatusCode(), statusNode.path(key).path("status").asInt(), + "expected a success entry for '" + key + "' in the status map, got: " + statusNode); + } + } + + @Test + void testSetAllUnauthenticated() throws Exception { + final HttpResponse allResponse = HttpRequestHelper.builder(ALL_PATH) + .username("wrong") + .password("password") + .request(HttpMethod.PUT, getExampleAllModel()); + assertEquals(Response.Status.UNAUTHORIZED.getStatusCode(), allResponse.statusCode()); + } + + @Test + void testSetAllUnauthorized() throws Exception { + final HttpResponse allResponse = HttpRequestHelper.builder(ALL_PATH) + .username("user") + .password("user") + .request(HttpMethod.PUT, getExampleAllModel()); + assertEquals(Response.Status.FORBIDDEN.getStatusCode(), allResponse.statusCode()); + } + + /** + * The product-specific {@code _AllModel} payload to apply. Must contain + * (at least) the general settings returned by {@link #getExampleSettingsGeneralModel()}. + */ + protected abstract Object getExampleAllModel(); + + protected SettingsGeneralModel getExampleSettingsGeneralModel() { + return SettingsGeneralModel.EXAMPLE_1; + } +} diff --git a/confluence/Apis/AllApi.md b/confluence/Apis/AllApi.md new file mode 100644 index 00000000..8174af80 --- /dev/null +++ b/confluence/Apis/AllApi.md @@ -0,0 +1,36 @@ +# AllApi + +All URIs are relative to *https://CONFLUENCE_URL/rest/bootstrapi/1* + +| Method | HTTP request | Description | +|------------- | ------------- | -------------| +| [**setAll**](AllApi.md#setAll) | **PUT** / | Apply a complete configuration | + + + +# **setAll** +> _AllModel setAll(\_AllModel) + +Apply a complete configuration + + Returns the updated configuration. The per-sub-field outcome is reported in the 'status' map (2xx for success, 4xx/5xx for failure with a human-readable 'message' and optional 'details'). License keys in the response are redacted (e.g. 'AAAB...wxyz#a1b2'). + +### Parameters + +|Name | Type | Description | Notes | +|------------- | ------------- | ------------- | -------------| +| **\_AllModel** | [**_AllModel**](../Models/_AllModel.md)| | | + +### Return type + +[**_AllModel**](../Models/_AllModel.md) + +### Authorization + +[basicAuth](../README.md#basicAuth) + +### HTTP request headers + +- **Content-Type**: application/json +- **Accept**: application/json + diff --git a/confluence/Apis/SettingsApi.md b/confluence/Apis/SettingsApi.md index ced46831..723e19c3 100644 --- a/confluence/Apis/SettingsApi.md +++ b/confluence/Apis/SettingsApi.md @@ -108,7 +108,7 @@ This endpoint does not need any parameter. # **getSettings** -> SettingsModel getSettings() +> SettingsGeneralModel getSettings() Get the general settings @@ -117,7 +117,7 @@ This endpoint does not need any parameter. ### Return type -[**SettingsModel**](../Models/SettingsModel.md) +[**SettingsGeneralModel**](../Models/SettingsGeneralModel.md) ### Authorization @@ -252,7 +252,7 @@ Set the custom HTML # **setSettings** -> SettingsModel setSettings(SettingsModel) +> SettingsGeneralModel setSettings(SettingsGeneralModel) Set the general settings @@ -260,11 +260,11 @@ Set the general settings |Name | Type | Description | Notes | |------------- | ------------- | ------------- | -------------| -| **SettingsModel** | [**SettingsModel**](../Models/SettingsModel.md)| | [optional] | +| **SettingsGeneralModel** | [**SettingsGeneralModel**](../Models/SettingsGeneralModel.md)| | [optional] | ### Return type -[**SettingsModel**](../Models/SettingsModel.md) +[**SettingsGeneralModel**](../Models/SettingsGeneralModel.md) ### Authorization diff --git a/confluence/Models/AuthenticationModel.md b/confluence/Models/AuthenticationModel.md new file mode 100644 index 00000000..b873bdc6 --- /dev/null +++ b/confluence/Models/AuthenticationModel.md @@ -0,0 +1,10 @@ +# AuthenticationModel +## Properties + +| Name | Type | Description | Notes | +|------------ | ------------- | ------------- | -------------| +| **idps** | [**Map**](AbstractAuthenticationIdpModel.md) | | [optional] [default to null] | +| **sso** | [**AuthenticationSsoModel**](AuthenticationSsoModel.md) | | [optional] [default to null] | + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + diff --git a/confluence/Models/MailServerModel.md b/confluence/Models/MailServerModel.md new file mode 100644 index 00000000..34c39bf7 --- /dev/null +++ b/confluence/Models/MailServerModel.md @@ -0,0 +1,10 @@ +# MailServerModel +## Properties + +| Name | Type | Description | Notes | +|------------ | ------------- | ------------- | -------------| +| **smtp** | [**MailServerSmtpModel**](MailServerSmtpModel.md) | | [optional] [default to null] | +| **pop** | [**MailServerPopModel**](MailServerPopModel.md) | | [optional] [default to null] | + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + diff --git a/confluence/Models/SettingsGeneralModel.md b/confluence/Models/SettingsGeneralModel.md new file mode 100644 index 00000000..8ebe677b --- /dev/null +++ b/confluence/Models/SettingsGeneralModel.md @@ -0,0 +1,13 @@ +# SettingsGeneralModel +## Properties + +| Name | Type | Description | Notes | +|------------ | ------------- | ------------- | -------------| +| **baseUrl** | **URI** | | [optional] [default to null] | +| **mode** | **String** | | [optional] [default to null] | +| **title** | **String** | | [optional] [default to null] | +| **contactMessage** | **String** | | [optional] [default to null] | +| **externalUserManagement** | **Boolean** | | [optional] [default to null] | + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + diff --git a/confluence/Models/SettingsModel.md b/confluence/Models/SettingsModel.md index f4eb0e7f..a9c90eac 100644 --- a/confluence/Models/SettingsModel.md +++ b/confluence/Models/SettingsModel.md @@ -3,11 +3,10 @@ | Name | Type | Description | Notes | |------------ | ------------- | ------------- | -------------| -| **baseUrl** | **URI** | | [optional] [default to null] | -| **mode** | **String** | | [optional] [default to null] | -| **title** | **String** | | [optional] [default to null] | -| **contactMessage** | **String** | | [optional] [default to null] | -| **externalUserManagement** | **Boolean** | | [optional] [default to null] | +| **general** | [**SettingsGeneralModel**](SettingsGeneralModel.md) | | [optional] [default to null] | +| **security** | [**SettingsSecurityModel**](SettingsSecurityModel.md) | | [optional] [default to null] | +| **branding** | [**SettingsBrandingColorSchemeModel**](SettingsBrandingColorSchemeModel.md) | | [optional] [default to null] | +| **customHtml** | [**SettingsCustomHtmlModel**](SettingsCustomHtmlModel.md) | | [optional] [default to null] | [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) diff --git a/confluence/Models/_AllModel.md b/confluence/Models/_AllModel.md new file mode 100644 index 00000000..3a2e4dee --- /dev/null +++ b/confluence/Models/_AllModel.md @@ -0,0 +1,16 @@ +# _AllModel +## Properties + +| Name | Type | Description | Notes | +|------------ | ------------- | ------------- | -------------| +| **settings** | [**SettingsModel**](SettingsModel.md) | | [optional] [default to null] | +| **directories** | [**Map**](AbstractDirectoryModel.md) | | [optional] [default to null] | +| **applicationLinks** | [**Map**](ApplicationLinkModel.md) | | [optional] [default to null] | +| **authentication** | [**AuthenticationModel**](AuthenticationModel.md) | | [optional] [default to null] | +| **licenses** | [**Map**](LicenseModel.md) | | [optional] [default to null] | +| **mailServer** | [**MailServerModel**](MailServerModel.md) | | [optional] [default to null] | +| **permissionsGlobal** | [**PermissionsGlobalModel**](PermissionsGlobalModel.md) | | [optional] [default to null] | +| **status** | [**Map**](_AllModelStatus.md) | | [optional] [default to null] | + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + diff --git a/confluence/Models/_AllModelStatus.md b/confluence/Models/_AllModelStatus.md new file mode 100644 index 00000000..ed699765 --- /dev/null +++ b/confluence/Models/_AllModelStatus.md @@ -0,0 +1,11 @@ +# _AllModelStatus +## Properties + +| Name | Type | Description | Notes | +|------------ | ------------- | ------------- | -------------| +| **status** | **Integer** | | [optional] [default to null] | +| **message** | **String** | | [optional] [default to null] | +| **details** | **String** | | [optional] [default to null] | + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + diff --git a/confluence/README.md b/confluence/README.md index 0f7ab34a..734314ae 100644 --- a/confluence/README.md +++ b/confluence/README.md @@ -7,6 +7,7 @@ All URIs are relative to *https://CONFLUENCE_URL/rest/bootstrapi/1* | Class | Method | HTTP request | Description | |------------ | ------------- | ------------- | -------------| +| *AllApi* | [**setAll**](Apis/AllApi.md#setAll) | **PUT** / | Apply a complete configuration | | *ApplicationLinkApi* | [**createApplicationLink**](Apis/ApplicationLinkApi.md#createApplicationLink) | **POST** /application-link | Create an application link | *ApplicationLinkApi* | [**deleteApplicationLink**](Apis/ApplicationLinkApi.md#deleteApplicationLink) | **DELETE** /application-link/{uuid} | Delete an application link | *ApplicationLinkApi* | [**getApplicationLink**](Apis/ApplicationLinkApi.md#getApplicationLink) | **GET** /application-link/{uuid} | Get an application link | @@ -66,6 +67,7 @@ All URIs are relative to *https://CONFLUENCE_URL/rest/bootstrapi/1* - [ApplicationLinkModel](./Models/ApplicationLinkModel.md) - [AuthenticationIdpOidcModel](./Models/AuthenticationIdpOidcModel.md) - [AuthenticationIdpSamlModel](./Models/AuthenticationIdpSamlModel.md) + - [AuthenticationModel](./Models/AuthenticationModel.md) - [AuthenticationSsoModel](./Models/AuthenticationSsoModel.md) - [CacheModel](./Models/CacheModel.md) - [DirectoryCrowdAdvanced](./Models/DirectoryCrowdAdvanced.md) @@ -88,14 +90,18 @@ All URIs are relative to *https://CONFLUENCE_URL/rest/bootstrapi/1* - [ErrorCollection](./Models/ErrorCollection.md) - [GroupModel](./Models/GroupModel.md) - [LicenseModel](./Models/LicenseModel.md) + - [MailServerModel](./Models/MailServerModel.md) - [MailServerPopModel](./Models/MailServerPopModel.md) - [MailServerSmtpModel](./Models/MailServerSmtpModel.md) - [PermissionsGlobalModel](./Models/PermissionsGlobalModel.md) - [SettingsBrandingColorSchemeModel](./Models/SettingsBrandingColorSchemeModel.md) - [SettingsCustomHtmlModel](./Models/SettingsCustomHtmlModel.md) + - [SettingsGeneralModel](./Models/SettingsGeneralModel.md) - [SettingsModel](./Models/SettingsModel.md) - [SettingsSecurityModel](./Models/SettingsSecurityModel.md) - [UserModel](./Models/UserModel.md) + - [_AllModel](./Models/_AllModel.md) + - [_AllModelStatus](./Models/_AllModelStatus.md) diff --git a/confluence/src/main/java/com/deftdevs/bootstrapi/confluence/config/ServiceConfig.java b/confluence/src/main/java/com/deftdevs/bootstrapi/confluence/config/ServiceConfig.java index d0f39f51..aa43ae92 100644 --- a/confluence/src/main/java/com/deftdevs/bootstrapi/confluence/config/ServiceConfig.java +++ b/confluence/src/main/java/com/deftdevs/bootstrapi/confluence/config/ServiceConfig.java @@ -1,6 +1,7 @@ package com.deftdevs.bootstrapi.confluence.config; import com.deftdevs.bootstrapi.commons.service.api.*; +import com.deftdevs.bootstrapi.confluence.model._AllModel; import com.deftdevs.bootstrapi.confluence.service.*; import com.deftdevs.bootstrapi.confluence.service.api.CachesService; import com.deftdevs.bootstrapi.confluence.service.api.ConfluenceAuthenticationService; @@ -18,6 +19,18 @@ public class ServiceConfig { @Autowired private HelperConfig helperConfig; + @Bean + public _AllService<_AllModel> _allService() { + return new _AllServiceImpl( + confluenceSettingsService(), + directoriesService(), + applicationLinksService(), + confluenceAuthenticationService(), + licensesService(), + mailServerService(), + permissionsService()); + } + @Bean public ApplicationLinksService applicationLinksService() { return new ApplicationLinksServiceImpl( @@ -42,8 +55,19 @@ public ConfluenceAuthenticationService confluenceAuthenticationService() { @Bean public ConfluenceSettingsService confluenceSettingsService() { - return new SettingsServiceImpl( - atlassianConfig.globalSettingsManager()); + // The branding service is deliberately NOT its own bean: ConfluenceSettingsService + // extends SettingsBrandingService, so a standalone branding bean would make by-type + // injection of SettingsBrandingService ambiguous in the REST layer. + return new ConfluenceSettingsServiceImpl( + settingsService(), + new SettingsBrandingServiceImpl( + atlassianConfig.colourSchemeManager(), + atlassianConfig.siteLogoManager())); + } + + @Bean + public SettingsServiceImpl settingsService() { + return new SettingsServiceImpl(atlassianConfig.globalSettingsManager()); } @Bean @@ -70,13 +94,6 @@ public PermissionsService permissionsService() { atlassianConfig.spacePermissionManager()); } - @Bean - public SettingsBrandingService settingsBrandingService() { - return new SettingsBrandingServiceImpl( - atlassianConfig.colourSchemeManager(), - atlassianConfig.siteLogoManager()); - } - @Bean public UsersService usersService() { return new UsersServiceImpl( diff --git a/confluence/src/main/java/com/deftdevs/bootstrapi/confluence/model/SettingsModel.java b/confluence/src/main/java/com/deftdevs/bootstrapi/confluence/model/SettingsModel.java new file mode 100644 index 00000000..9da3af29 --- /dev/null +++ b/confluence/src/main/java/com/deftdevs/bootstrapi/confluence/model/SettingsModel.java @@ -0,0 +1,35 @@ +package com.deftdevs.bootstrapi.confluence.model; + +import com.deftdevs.bootstrapi.commons.model.SettingsBrandingColorSchemeModel; +import com.deftdevs.bootstrapi.commons.model.SettingsGeneralModel; +import com.deftdevs.bootstrapi.commons.model.SettingsSecurityModel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; + +import static com.deftdevs.bootstrapi.commons.constants.BootstrAPI.SETTINGS; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@XmlRootElement(name = SETTINGS) +public class SettingsModel { + + @XmlElement + private SettingsGeneralModel general; + + @XmlElement + private SettingsSecurityModel security; + + @XmlElement + private SettingsBrandingColorSchemeModel branding; + + @XmlElement + private SettingsCustomHtmlModel customHtml; + +} diff --git a/confluence/src/main/java/com/deftdevs/bootstrapi/confluence/model/_AllModel.java b/confluence/src/main/java/com/deftdevs/bootstrapi/confluence/model/_AllModel.java new file mode 100644 index 00000000..a9131cce --- /dev/null +++ b/confluence/src/main/java/com/deftdevs/bootstrapi/confluence/model/_AllModel.java @@ -0,0 +1,52 @@ +package com.deftdevs.bootstrapi.confluence.model; + +import com.deftdevs.bootstrapi.commons.constants.BootstrAPI; +import com.deftdevs.bootstrapi.commons.model.AbstractDirectoryModel; +import com.deftdevs.bootstrapi.commons.model.ApplicationLinkModel; +import com.deftdevs.bootstrapi.commons.model.AuthenticationModel; +import com.deftdevs.bootstrapi.commons.model.LicenseModel; +import com.deftdevs.bootstrapi.commons.model.MailServerModel; +import com.deftdevs.bootstrapi.commons.model.PermissionsGlobalModel; +import com.deftdevs.bootstrapi.commons.model.type._AllModelAccessor; +import com.deftdevs.bootstrapi.commons.model.type._AllModelStatus; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; +import java.util.Map; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@XmlRootElement(name = BootstrAPI._ALL) +public class _AllModel implements _AllModelAccessor { + + @XmlElement + private SettingsModel settings; + + @XmlElement + private Map directories; + + @XmlElement + private Map applicationLinks; + + @XmlElement + private AuthenticationModel authentication; + + @XmlElement + private Map licenses; + + @XmlElement + private MailServerModel mailServer; + + @XmlElement + private PermissionsGlobalModel permissionsGlobal; + + @XmlElement + private Map status; + +} diff --git a/confluence/src/main/java/com/deftdevs/bootstrapi/confluence/rest/SettingsResourceImpl.java b/confluence/src/main/java/com/deftdevs/bootstrapi/confluence/rest/SettingsGeneralResourceImpl.java similarity index 69% rename from confluence/src/main/java/com/deftdevs/bootstrapi/confluence/rest/SettingsResourceImpl.java rename to confluence/src/main/java/com/deftdevs/bootstrapi/confluence/rest/SettingsGeneralResourceImpl.java index 8809c6c3..948a1436 100644 --- a/confluence/src/main/java/com/deftdevs/bootstrapi/confluence/rest/SettingsResourceImpl.java +++ b/confluence/src/main/java/com/deftdevs/bootstrapi/confluence/rest/SettingsGeneralResourceImpl.java @@ -2,8 +2,8 @@ import com.atlassian.plugins.rest.api.security.annotation.SystemAdminOnly; import com.deftdevs.bootstrapi.commons.constants.BootstrAPI; -import com.deftdevs.bootstrapi.commons.model.SettingsModel; -import com.deftdevs.bootstrapi.commons.rest.AbstractSettingsResourceImpl; +import com.deftdevs.bootstrapi.commons.model.SettingsGeneralModel; +import com.deftdevs.bootstrapi.commons.rest.AbstractSettingsGeneralResourceImpl; import com.deftdevs.bootstrapi.confluence.service.api.ConfluenceSettingsService; import io.swagger.v3.oas.annotations.tags.Tag; @@ -18,10 +18,10 @@ @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) @SystemAdminOnly -public class SettingsResourceImpl extends AbstractSettingsResourceImpl { +public class SettingsGeneralResourceImpl extends AbstractSettingsGeneralResourceImpl { @Inject - public SettingsResourceImpl( + public SettingsGeneralResourceImpl( final ConfluenceSettingsService settingsService) { super(settingsService); diff --git a/confluence/src/main/java/com/deftdevs/bootstrapi/confluence/rest/_AllResourceImpl.java b/confluence/src/main/java/com/deftdevs/bootstrapi/confluence/rest/_AllResourceImpl.java new file mode 100644 index 00000000..93dbf4a5 --- /dev/null +++ b/confluence/src/main/java/com/deftdevs/bootstrapi/confluence/rest/_AllResourceImpl.java @@ -0,0 +1,64 @@ +package com.deftdevs.bootstrapi.confluence.rest; + +import com.atlassian.plugins.rest.api.security.annotation.SystemAdminOnly; +import com.deftdevs.bootstrapi.commons.constants.BootstrAPI; +import com.deftdevs.bootstrapi.commons.model.ErrorCollection; +import com.deftdevs.bootstrapi.commons.rest._AbstractAllResourceImpl; +import com.deftdevs.bootstrapi.commons.service.api._AllService; +import com.deftdevs.bootstrapi.confluence.model._AllModel; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.tags.Tag; + +import javax.inject.Inject; +import javax.ws.rs.Consumes; +import javax.ws.rs.PUT; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; + +@Path(BootstrAPI._ROOT) +@Tag(name = BootstrAPI._ALL) +@Consumes(MediaType.APPLICATION_JSON) +@Produces(MediaType.APPLICATION_JSON) +@SystemAdminOnly +public class _AllResourceImpl extends _AbstractAllResourceImpl<_AllModel> { + + @Inject + public _AllResourceImpl( + final _AllService<_AllModel> allService) { + + super(allService); + } + + // overridden to document the concrete response model in the generated OpenAPI spec + @PUT + @Operation( + summary = BootstrAPI._ALL_PUT_SUMMARY, + description = BootstrAPI._ALL_PUT_RESPONSE_DESCRIPTION, + responses = { + @ApiResponse( + responseCode = "200", content = @Content(schema = @Schema(implementation = _AllModel.class)), + description = BootstrAPI._ALL_PUT_RESPONSE_DESCRIPTION + ), + @ApiResponse( + responseCode = "207", content = @Content(schema = @Schema(implementation = _AllModel.class)), + description = BootstrAPI._ALL_PUT_PARTIAL_RESPONSE_DESCRIPTION + ), + @ApiResponse( + responseCode = "default", content = @Content(schema = @Schema(implementation = ErrorCollection.class)), + description = BootstrAPI.ERROR_COLLECTION_RESPONSE_DESCRIPTION + ), + } + ) + @Override + public Response setAll( + final _AllModel allModel) { + + return super.setAll(allModel); + } + +} diff --git a/confluence/src/main/java/com/deftdevs/bootstrapi/confluence/service/ConfluenceSettingsServiceImpl.java b/confluence/src/main/java/com/deftdevs/bootstrapi/confluence/service/ConfluenceSettingsServiceImpl.java new file mode 100644 index 00000000..b3e7e586 --- /dev/null +++ b/confluence/src/main/java/com/deftdevs/bootstrapi/confluence/service/ConfluenceSettingsServiceImpl.java @@ -0,0 +1,90 @@ +package com.deftdevs.bootstrapi.confluence.service; + +import com.deftdevs.bootstrapi.commons.model.SettingsBrandingColorSchemeModel; +import com.deftdevs.bootstrapi.commons.model.SettingsGeneralModel; +import com.deftdevs.bootstrapi.commons.model.SettingsSecurityModel; +import com.deftdevs.bootstrapi.commons.service.api.SettingsBrandingService; +import com.deftdevs.bootstrapi.confluence.model.SettingsCustomHtmlModel; +import com.deftdevs.bootstrapi.confluence.service.api.ConfluenceSettingsService; + +import java.io.InputStream; + +public class ConfluenceSettingsServiceImpl implements ConfluenceSettingsService { + + private final SettingsServiceImpl settingsService; + private final SettingsBrandingService settingsBrandingService; + + /** + * The general/security/custom-html operations are provided by {@link SettingsServiceImpl}, + * which intentionally exposes {@code getCustomHtml}/{@code setCustomHtml} as + * non-interface methods (there is no Confluence-shared interface for them). + * That forces the concrete type here. + */ + public ConfluenceSettingsServiceImpl( + final SettingsServiceImpl settingsService, + final SettingsBrandingService settingsBrandingService) { + + this.settingsService = settingsService; + this.settingsBrandingService = settingsBrandingService; + } + + @Override + public SettingsGeneralModel getSettingsGeneral() { + return settingsService.getSettingsGeneral(); + } + + @Override + public SettingsGeneralModel setSettingsGeneral(final SettingsGeneralModel settingsGeneralModel) { + return settingsService.setSettingsGeneral(settingsGeneralModel); + } + + @Override + public SettingsSecurityModel getSettingsSecurity() { + return settingsService.getSettingsSecurity(); + } + + @Override + public SettingsSecurityModel setSettingsSecurity(final SettingsSecurityModel settingsSecurityModel) { + return settingsService.setSettingsSecurity(settingsSecurityModel); + } + + @Override + public SettingsCustomHtmlModel getCustomHtml() { + return settingsService.getCustomHtml(); + } + + @Override + public SettingsCustomHtmlModel setCustomHtml(final SettingsCustomHtmlModel settingsCustomHtmlModel) { + return settingsService.setCustomHtml(settingsCustomHtmlModel); + } + + @Override + public SettingsBrandingColorSchemeModel getColourScheme() { + return settingsBrandingService.getColourScheme(); + } + + @Override + public SettingsBrandingColorSchemeModel setColourScheme(final SettingsBrandingColorSchemeModel colourSchemeModel) { + return settingsBrandingService.setColourScheme(colourSchemeModel); + } + + @Override + public InputStream getLogo() { + return settingsBrandingService.getLogo(); + } + + @Override + public void setLogo(final InputStream inputStream) { + settingsBrandingService.setLogo(inputStream); + } + + @Override + public InputStream getFavicon() { + return settingsBrandingService.getFavicon(); + } + + @Override + public void setFavicon(final InputStream inputStream) { + settingsBrandingService.setFavicon(inputStream); + } +} diff --git a/confluence/src/main/java/com/deftdevs/bootstrapi/confluence/service/LicensesServiceImpl.java b/confluence/src/main/java/com/deftdevs/bootstrapi/confluence/service/LicensesServiceImpl.java index f78cefe2..df4f5d16 100644 --- a/confluence/src/main/java/com/deftdevs/bootstrapi/confluence/service/LicensesServiceImpl.java +++ b/confluence/src/main/java/com/deftdevs/bootstrapi/confluence/service/LicensesServiceImpl.java @@ -7,10 +7,13 @@ import com.deftdevs.bootstrapi.commons.exception.web.InternalServerErrorException; import com.deftdevs.bootstrapi.commons.model.LicenseModel; import com.deftdevs.bootstrapi.commons.service.api.LicensesService; +import com.deftdevs.bootstrapi.commons.util.LicenseKeyRedactor; import com.deftdevs.bootstrapi.confluence.model.util.LicenseModelUtil; import java.util.Collections; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; import static com.atlassian.confluence.setup.ConfluenceBootstrapConstants.DEFAULT_LICENSE_REGISTRY_KEY; @@ -43,6 +46,17 @@ public List setLicenses( return getLicenses(); } + @Override + public Map setLicenses( + final Map licenseInputs) { + + final Map result = new LinkedHashMap<>(); + for (final String key : licenseInputs.keySet()) { + result.put(LicenseKeyRedactor.redact(key), addLicense(key)); + } + return result; + } + @Override public LicenseModel addLicense( final String licenseKey) { diff --git a/confluence/src/main/java/com/deftdevs/bootstrapi/confluence/service/SettingsServiceImpl.java b/confluence/src/main/java/com/deftdevs/bootstrapi/confluence/service/SettingsServiceImpl.java index 645bc895..cedaaaab 100644 --- a/confluence/src/main/java/com/deftdevs/bootstrapi/confluence/service/SettingsServiceImpl.java +++ b/confluence/src/main/java/com/deftdevs/bootstrapi/confluence/service/SettingsServiceImpl.java @@ -3,14 +3,15 @@ import com.atlassian.confluence.setup.settings.CustomHtmlSettings; import com.atlassian.confluence.setup.settings.GlobalSettingsManager; import com.atlassian.confluence.setup.settings.Settings; -import com.deftdevs.bootstrapi.commons.model.SettingsModel; +import com.deftdevs.bootstrapi.commons.model.SettingsGeneralModel; import com.deftdevs.bootstrapi.commons.model.SettingsSecurityModel; +import com.deftdevs.bootstrapi.commons.service.api.SettingsGeneralService; +import com.deftdevs.bootstrapi.commons.service.api.SettingsSecurityService; import com.deftdevs.bootstrapi.confluence.model.SettingsCustomHtmlModel; -import com.deftdevs.bootstrapi.confluence.service.api.ConfluenceSettingsService; - import java.net.URI; -public class SettingsServiceImpl implements ConfluenceSettingsService { +public class SettingsServiceImpl + implements SettingsGeneralService, SettingsSecurityService { private final GlobalSettingsManager globalSettingsManager; @@ -20,11 +21,12 @@ public SettingsServiceImpl( this.globalSettingsManager = globalSettingsManager; } + @Override - public SettingsModel getSettingsGeneral() { + public SettingsGeneralModel getSettingsGeneral() { final Settings settings = globalSettingsManager.getGlobalSettings(); - return SettingsModel.builder() + return SettingsGeneralModel.builder() .baseUrl(URI.create(settings.getBaseUrl())) .title(settings.getSiteTitle()) .contactMessage(settings.getCustomContactMessage()) @@ -32,8 +34,9 @@ public SettingsModel getSettingsGeneral() { .build(); } + @Override - public SettingsModel setSettingsGeneral(SettingsModel settingsModel) { + public SettingsGeneralModel setSettingsGeneral(SettingsGeneralModel settingsModel) { final Settings settings = globalSettingsManager.getGlobalSettings(); if (settingsModel.getBaseUrl() != null) { @@ -57,7 +60,7 @@ public SettingsModel setSettingsGeneral(SettingsModel settingsModel) { return getSettingsGeneral(); } - @Override + public SettingsCustomHtmlModel getCustomHtml() { final CustomHtmlSettings customHtmlSettings = globalSettingsManager.getGlobalSettings().getCustomHtmlSettings(); @@ -68,7 +71,7 @@ public SettingsCustomHtmlModel getCustomHtml() { .build(); } - @Override + public SettingsCustomHtmlModel setCustomHtml( final SettingsCustomHtmlModel settingsCustomHtmlModel) { @@ -92,6 +95,7 @@ public SettingsCustomHtmlModel setCustomHtml( return getCustomHtml(); } + @Override public SettingsSecurityModel getSettingsSecurity() { final Settings settings = globalSettingsManager.getGlobalSettings(); @@ -102,6 +106,7 @@ public SettingsSecurityModel getSettingsSecurity() { .build(); } + @Override public SettingsSecurityModel setSettingsSecurity( final SettingsSecurityModel settingsSecurityModel) { diff --git a/confluence/src/main/java/com/deftdevs/bootstrapi/confluence/service/_AllServiceImpl.java b/confluence/src/main/java/com/deftdevs/bootstrapi/confluence/service/_AllServiceImpl.java new file mode 100644 index 00000000..91d3ea1d --- /dev/null +++ b/confluence/src/main/java/com/deftdevs/bootstrapi/confluence/service/_AllServiceImpl.java @@ -0,0 +1,82 @@ +package com.deftdevs.bootstrapi.confluence.service; + +import com.deftdevs.bootstrapi.commons.model.PermissionsGlobalModel; +import com.deftdevs.bootstrapi.commons.model.type._AllModelStatus; +import com.deftdevs.bootstrapi.commons.service._AbstractAllServiceImpl; +import com.deftdevs.bootstrapi.commons.service.api.ApplicationLinksService; +import com.deftdevs.bootstrapi.commons.service.api.DirectoriesService; +import com.deftdevs.bootstrapi.commons.service.api.LicensesService; +import com.deftdevs.bootstrapi.commons.service.api.MailServerService; +import com.deftdevs.bootstrapi.commons.service.api.PermissionsService; +import com.deftdevs.bootstrapi.commons.util.ServiceResultUtil; +import com.deftdevs.bootstrapi.confluence.model._AllModel; +import com.deftdevs.bootstrapi.confluence.service.api.ConfluenceAuthenticationService; +import com.deftdevs.bootstrapi.confluence.service.api.ConfluenceSettingsService; + +import java.util.HashMap; +import java.util.Map; + +import static com.deftdevs.bootstrapi.commons.constants.BootstrAPI.APPLICATION_LINKS; +import static com.deftdevs.bootstrapi.commons.constants.BootstrAPI.DIRECTORIES; +import static com.deftdevs.bootstrapi.commons.constants.BootstrAPI.LICENSES; + +public class _AllServiceImpl extends _AbstractAllServiceImpl<_AllModel> { + + private final ConfluenceSettingsService settingsService; + private final DirectoriesService directoriesService; + private final ApplicationLinksService applicationLinksService; + private final ConfluenceAuthenticationService authenticationService; + private final LicensesService licensesService; + private final MailServerService mailServerService; + private final PermissionsService permissionsService; + + public _AllServiceImpl( + final ConfluenceSettingsService settingsService, + final DirectoriesService directoriesService, + final ApplicationLinksService applicationLinksService, + final ConfluenceAuthenticationService authenticationService, + final LicensesService licensesService, + final MailServerService mailServerService, + final PermissionsService permissionsService) { + + this.settingsService = settingsService; + this.directoriesService = directoriesService; + this.applicationLinksService = applicationLinksService; + this.authenticationService = authenticationService; + this.licensesService = licensesService; + this.mailServerService = mailServerService; + this.permissionsService = permissionsService; + } + + @Override + public _AllModel setAll( + final _AllModel allModel) { + + final _AllModel result = new _AllModel(); + final Map statusMap = new HashMap<>(); + + setEntityWithStatus(allModel.getSettings(), + settingsService::setSettings, result::setSettings, statusMap); + + setEntities(DIRECTORIES, allModel.getDirectories(), + directoriesService::setDirectories, result::setDirectories, statusMap); + + setEntities(APPLICATION_LINKS, allModel.getApplicationLinks(), + applicationLinksService::setApplicationLinks, result::setApplicationLinks, statusMap); + + setEntityWithStatus(allModel.getAuthentication(), + authenticationService::setAuthentication, result::setAuthentication, statusMap); + + setEntities(LICENSES, allModel.getLicenses(), + licensesService::setLicenses, result::setLicenses, statusMap); + + setEntityWithStatus(allModel.getMailServer(), + mailServerService::setMailServer, result::setMailServer, statusMap); + + setEntity(ServiceResultUtil.subEntityKey(PermissionsGlobalModel.class), allModel.getPermissionsGlobal(), + permissionsService::setPermissionsGlobal, result::setPermissionsGlobal, statusMap); + + result.setStatus(statusMap); + return result; + } +} diff --git a/confluence/src/main/java/com/deftdevs/bootstrapi/confluence/service/api/ConfluenceSettingsService.java b/confluence/src/main/java/com/deftdevs/bootstrapi/confluence/service/api/ConfluenceSettingsService.java index cb60ebc7..3a0d764f 100644 --- a/confluence/src/main/java/com/deftdevs/bootstrapi/confluence/service/api/ConfluenceSettingsService.java +++ b/confluence/src/main/java/com/deftdevs/bootstrapi/confluence/service/api/ConfluenceSettingsService.java @@ -1,18 +1,77 @@ package com.deftdevs.bootstrapi.confluence.service.api; -import com.deftdevs.bootstrapi.commons.model.SettingsModel; +import com.deftdevs.bootstrapi.commons.model.SettingsBrandingColorSchemeModel; +import com.deftdevs.bootstrapi.commons.model.SettingsGeneralModel; import com.deftdevs.bootstrapi.commons.model.SettingsSecurityModel; +import com.deftdevs.bootstrapi.commons.model.type._AllModelStatus; +import com.deftdevs.bootstrapi.commons.model.type.ServiceResult; +import com.deftdevs.bootstrapi.commons.service.api.SettingsBrandingService; +import com.deftdevs.bootstrapi.commons.service.api.SettingsGeneralService; import com.deftdevs.bootstrapi.commons.service.api.SettingsSecurityService; -import com.deftdevs.bootstrapi.commons.service.api.SettingsService; +import com.deftdevs.bootstrapi.commons.util.ServiceResultUtil; import com.deftdevs.bootstrapi.confluence.model.SettingsCustomHtmlModel; +import com.deftdevs.bootstrapi.confluence.model.SettingsModel; + +import java.util.LinkedHashMap; +import java.util.Map; public interface ConfluenceSettingsService extends - SettingsService, - SettingsSecurityService { + SettingsGeneralService, + SettingsSecurityService, + SettingsBrandingService { SettingsCustomHtmlModel getCustomHtml(); SettingsCustomHtmlModel setCustomHtml( SettingsCustomHtmlModel settingsCustomHtmlModel); + default SettingsModel getSettings() { + return new SettingsModel(getSettingsGeneral(), getSettingsSecurity(), + getColourScheme(), getCustomHtml()); + } + + default ServiceResult setSettings(final SettingsModel settingsModel) { + final SettingsModel result = new SettingsModel(); + final Map status = new LinkedHashMap<>(); + + if (settingsModel.getGeneral() != null) { + final String key = ServiceResultUtil.subEntityKey(SettingsGeneralModel.class); + try { + result.setGeneral(setSettingsGeneral(settingsModel.getGeneral())); + status.put(key, _AllModelStatus.success()); + } catch (Exception e) { + status.put(key, ServiceResultUtil.toErrorStatus(key, e)); + } + } + if (settingsModel.getSecurity() != null) { + final String key = ServiceResultUtil.subEntityKey(SettingsSecurityModel.class); + try { + result.setSecurity(setSettingsSecurity(settingsModel.getSecurity())); + status.put(key, _AllModelStatus.success()); + } catch (Exception e) { + status.put(key, ServiceResultUtil.toErrorStatus(key, e)); + } + } + if (settingsModel.getBranding() != null) { + final String key = ServiceResultUtil.subEntityKey(SettingsBrandingColorSchemeModel.class); + try { + result.setBranding(setColourScheme(settingsModel.getBranding())); + status.put(key, _AllModelStatus.success()); + } catch (Exception e) { + status.put(key, ServiceResultUtil.toErrorStatus(key, e)); + } + } + if (settingsModel.getCustomHtml() != null) { + // sub-name "custom-html" contains a dash, so derive the key from the group model + final String key = ServiceResultUtil.subEntityKey(SettingsModel.class, SettingsCustomHtmlModel.class); + try { + result.setCustomHtml(setCustomHtml(settingsModel.getCustomHtml())); + status.put(key, _AllModelStatus.success()); + } catch (Exception e) { + status.put(key, ServiceResultUtil.toErrorStatus(key, e)); + } + } + return new ServiceResult<>(result, status); + } + } diff --git a/confluence/src/test/java/com/deftdevs/bootstrapi/confluence/rest/_AllResourceTest.java b/confluence/src/test/java/com/deftdevs/bootstrapi/confluence/rest/_AllResourceTest.java new file mode 100644 index 00000000..021f9c48 --- /dev/null +++ b/confluence/src/test/java/com/deftdevs/bootstrapi/confluence/rest/_AllResourceTest.java @@ -0,0 +1,88 @@ +package com.deftdevs.bootstrapi.confluence.rest; + +import com.deftdevs.bootstrapi.commons.model.SettingsGeneralModel; +import com.deftdevs.bootstrapi.commons.model.type._AllModelStatus; +import com.deftdevs.bootstrapi.commons.service.api._AllService; +import com.deftdevs.bootstrapi.confluence.model.SettingsModel; +import com.deftdevs.bootstrapi.confluence.model._AllModel; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import javax.ws.rs.core.Response; +import java.util.HashMap; +import java.util.Map; + +import static com.deftdevs.bootstrapi.commons.constants.BootstrAPI.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +public class _AllResourceTest { + + @Mock + private _AllService<_AllModel> allService; + + private _AllResourceImpl allResource; + + private _AllModel allModel; + + @BeforeEach + public void setup() { + allResource = new _AllResourceImpl(allService); + + allModel = new _AllModel(); + final SettingsModel settings = new SettingsModel(); + settings.setGeneral(SettingsGeneralModel.EXAMPLE_1); + allModel.setSettings(settings); + + final Map status = new HashMap<>(); + status.put(SETTINGS + "/" + SETTINGS_GENERAL, _AllModelStatus.success()); + status.put(DIRECTORIES, _AllModelStatus.success()); + status.put(APPLICATION_LINKS, _AllModelStatus.success()); + allModel.setStatus(status); + } + + @Test + public void testSetAll() { + doReturn(allModel).when(allService).setAll(any()); + + final Response response = allResource.setAll(allModel); + assertEquals(200, response.getStatus()); + + final _AllModel responseModel = (_AllModel) response.getEntity(); + assertEquals(allModel, responseModel); + + verify(allService).setAll(allModel); + } + + @Test + public void testSetAllReturns500WhenAnySubFieldFailedWithServerError() { + final _AllModel result = new _AllModel(); + final Map status = new HashMap<>(); + status.put(SETTINGS + "/" + SETTINGS_GENERAL, _AllModelStatus.success()); + status.put(DIRECTORIES, + _AllModelStatus.error(Response.Status.INTERNAL_SERVER_ERROR, "boom", null)); + result.setStatus(status); + doReturn(result).when(allService).setAll(any()); + + final Response response = allResource.setAll(allModel); + assertEquals(500, response.getStatus()); + } + + @Test + public void testSetAllReturns207OnMixedSuccessAndClientError() { + final _AllModel result = new _AllModel(); + final Map status = new HashMap<>(); + status.put(SETTINGS + "/" + SETTINGS_GENERAL, _AllModelStatus.success()); + status.put(DIRECTORIES, + _AllModelStatus.error(Response.Status.BAD_REQUEST, "bad", null)); + result.setStatus(status); + doReturn(result).when(allService).setAll(any()); + + final Response response = allResource.setAll(allModel); + assertEquals(207, response.getStatus()); + } +} diff --git a/confluence/src/test/java/com/deftdevs/bootstrapi/confluence/service/ConfluenceSettingsServiceTest.java b/confluence/src/test/java/com/deftdevs/bootstrapi/confluence/service/ConfluenceSettingsServiceTest.java new file mode 100644 index 00000000..c215f08a --- /dev/null +++ b/confluence/src/test/java/com/deftdevs/bootstrapi/confluence/service/ConfluenceSettingsServiceTest.java @@ -0,0 +1,107 @@ +package com.deftdevs.bootstrapi.confluence.service; + +import com.deftdevs.bootstrapi.commons.exception.web.BadRequestException; +import com.deftdevs.bootstrapi.commons.model.SettingsBrandingColorSchemeModel; +import com.deftdevs.bootstrapi.commons.model.SettingsGeneralModel; +import com.deftdevs.bootstrapi.commons.model.SettingsSecurityModel; +import com.deftdevs.bootstrapi.commons.model.type.ServiceResult; +import com.deftdevs.bootstrapi.commons.service.api.SettingsBrandingService; +import com.deftdevs.bootstrapi.confluence.model.SettingsCustomHtmlModel; +import com.deftdevs.bootstrapi.confluence.model.SettingsModel; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import javax.ws.rs.core.Response; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; + +@ExtendWith(MockitoExtension.class) +class ConfluenceSettingsServiceTest { + + @Mock + private SettingsServiceImpl settingsService; + + @Mock + private SettingsBrandingService settingsBrandingService; + + private ConfluenceSettingsServiceImpl confluenceSettingsService; + + @BeforeEach + void setup() { + confluenceSettingsService = new ConfluenceSettingsServiceImpl(settingsService, settingsBrandingService); + } + + @Test + void testGetSettings() { + doReturn(SettingsGeneralModel.EXAMPLE_1).when(settingsService).getSettingsGeneral(); + doReturn(SettingsSecurityModel.EXAMPLE_1).when(settingsService).getSettingsSecurity(); + doReturn(SettingsBrandingColorSchemeModel.EXAMPLE_1).when(settingsBrandingService).getColourScheme(); + doReturn(SettingsCustomHtmlModel.EXAMPLE_1).when(settingsService).getCustomHtml(); + + final SettingsModel settingsModel = confluenceSettingsService.getSettings(); + + assertEquals(SettingsGeneralModel.EXAMPLE_1, settingsModel.getGeneral()); + assertEquals(SettingsSecurityModel.EXAMPLE_1, settingsModel.getSecurity()); + assertEquals(SettingsBrandingColorSchemeModel.EXAMPLE_1, settingsModel.getBranding()); + assertEquals(SettingsCustomHtmlModel.EXAMPLE_1, settingsModel.getCustomHtml()); + } + + @Test + void testSetSettingsAppliesAllSubFields() { + doReturn(SettingsGeneralModel.EXAMPLE_1).when(settingsService).setSettingsGeneral(SettingsGeneralModel.EXAMPLE_1); + doReturn(SettingsSecurityModel.EXAMPLE_1).when(settingsService).setSettingsSecurity(SettingsSecurityModel.EXAMPLE_1); + doReturn(SettingsBrandingColorSchemeModel.EXAMPLE_1).when(settingsBrandingService).setColourScheme(SettingsBrandingColorSchemeModel.EXAMPLE_1); + doReturn(SettingsCustomHtmlModel.EXAMPLE_1).when(settingsService).setCustomHtml(SettingsCustomHtmlModel.EXAMPLE_1); + + final ServiceResult result = confluenceSettingsService.setSettings(new SettingsModel( + SettingsGeneralModel.EXAMPLE_1, + SettingsSecurityModel.EXAMPLE_1, + SettingsBrandingColorSchemeModel.EXAMPLE_1, + SettingsCustomHtmlModel.EXAMPLE_1)); + + assertEquals(SettingsGeneralModel.EXAMPLE_1, result.getModel().getGeneral()); + assertEquals(SettingsSecurityModel.EXAMPLE_1, result.getModel().getSecurity()); + assertEquals(SettingsBrandingColorSchemeModel.EXAMPLE_1, result.getModel().getBranding()); + assertEquals(SettingsCustomHtmlModel.EXAMPLE_1, result.getModel().getCustomHtml()); + + assertEquals(Response.Status.OK.getStatusCode(), result.getStatus().get("settings/general").getStatus()); + assertEquals(Response.Status.OK.getStatusCode(), result.getStatus().get("settings/security").getStatus()); + assertEquals(Response.Status.OK.getStatusCode(), result.getStatus().get("settings/branding").getStatus()); + assertEquals(Response.Status.OK.getStatusCode(), result.getStatus().get("settings/custom-html").getStatus()); + } + + @Test + void testSetSettingsSkipsNullSubFields() { + final ServiceResult result = confluenceSettingsService.setSettings(new SettingsModel()); + + assertTrue(result.getStatus().isEmpty()); + verify(settingsService, never()).setSettingsGeneral(SettingsGeneralModel.EXAMPLE_1); + } + + @Test + void testSetSettingsRecordsPerSubFieldFailure() { + doReturn(SettingsGeneralModel.EXAMPLE_1).when(settingsService).setSettingsGeneral(SettingsGeneralModel.EXAMPLE_1); + doThrow(new BadRequestException("invalid colour scheme")) + .when(settingsBrandingService).setColourScheme(SettingsBrandingColorSchemeModel.EXAMPLE_1); + + final ServiceResult result = confluenceSettingsService.setSettings(new SettingsModel( + SettingsGeneralModel.EXAMPLE_1, + null, + SettingsBrandingColorSchemeModel.EXAMPLE_1, + null)); + + assertEquals(SettingsGeneralModel.EXAMPLE_1, result.getModel().getGeneral()); + assertNull(result.getModel().getBranding()); + assertEquals(Response.Status.OK.getStatusCode(), result.getStatus().get("settings/general").getStatus()); + assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), result.getStatus().get("settings/branding").getStatus()); + } +} diff --git a/confluence/src/test/java/com/deftdevs/bootstrapi/confluence/service/SettingsServiceTest.java b/confluence/src/test/java/com/deftdevs/bootstrapi/confluence/service/SettingsServiceTest.java index 06f07919..c0315750 100644 --- a/confluence/src/test/java/com/deftdevs/bootstrapi/confluence/service/SettingsServiceTest.java +++ b/confluence/src/test/java/com/deftdevs/bootstrapi/confluence/service/SettingsServiceTest.java @@ -5,7 +5,7 @@ import com.atlassian.confluence.setup.settings.CustomHtmlSettings; import com.atlassian.confluence.setup.settings.GlobalSettingsManager; import com.atlassian.confluence.setup.settings.Settings; -import com.deftdevs.bootstrapi.commons.model.SettingsModel; +import com.deftdevs.bootstrapi.commons.model.SettingsGeneralModel; import com.deftdevs.bootstrapi.commons.model.SettingsSecurityModel; import com.deftdevs.bootstrapi.confluence.model.SettingsCustomHtmlModel; import org.junit.jupiter.api.BeforeEach; @@ -39,9 +39,9 @@ void testGetSettingsGeneral() { doReturn(settings).when(globalSettingsManager).getGlobalSettings(); - final SettingsModel settingsModel = settingsService.getSettingsGeneral(); + final SettingsGeneralModel settingsModel = settingsService.getSettingsGeneral(); - final SettingsModel settingsModelRef = SettingsModel.builder() + final SettingsGeneralModel settingsModelRef = SettingsGeneralModel.builder() .baseUrl(URI.create(settings.getBaseUrl())) .title(settings.getSiteTitle()) .contactMessage(settings.getCustomContactMessage()) @@ -58,20 +58,20 @@ void testSetSettingsGeneral() { final Settings updateSettings = new OtherTestSettings(); - final SettingsModel requestModel = SettingsModel.builder() + final SettingsGeneralModel requestModel = SettingsGeneralModel.builder() .baseUrl(URI.create(updateSettings.getBaseUrl())) .title(updateSettings.getSiteTitle()) .contactMessage(updateSettings.getCustomContactMessage()) .externalUserManagement(updateSettings.isExternalUserManagement()) .build(); - final SettingsModel responseModel = settingsService.setSettingsGeneral(requestModel); + final SettingsGeneralModel responseModel = settingsService.setSettingsGeneral(requestModel); final ArgumentCaptor settingsCaptor = ArgumentCaptor.forClass(Settings.class); verify(globalSettingsManager).updateGlobalSettings(settingsCaptor.capture()); final Settings settings = settingsCaptor.getValue(); - final SettingsModel settingsModel = SettingsModel.builder() + final SettingsGeneralModel settingsModel = SettingsGeneralModel.builder() .baseUrl(URI.create(settings.getBaseUrl())) .title(settings.getSiteTitle()) .contactMessage(settings.getCustomContactMessage()) @@ -84,7 +84,7 @@ void testSetSettingsGeneral() { @Test void testSetSettingsDefaultConfig(){ - final SettingsModel settingsModel = SettingsModel.builder().build(); + final SettingsGeneralModel settingsModel = SettingsGeneralModel.builder().build(); final Settings defaultSettings = new DefaultTestSettings(); doReturn(defaultSettings).when(globalSettingsManager).getGlobalSettings(); diff --git a/confluence/src/test/java/com/deftdevs/bootstrapi/confluence/service/_AllServiceImplTest.java b/confluence/src/test/java/com/deftdevs/bootstrapi/confluence/service/_AllServiceImplTest.java new file mode 100644 index 00000000..d99768ea --- /dev/null +++ b/confluence/src/test/java/com/deftdevs/bootstrapi/confluence/service/_AllServiceImplTest.java @@ -0,0 +1,172 @@ +package com.deftdevs.bootstrapi.confluence.service; + +import com.deftdevs.bootstrapi.commons.model.AbstractDirectoryModel; +import com.deftdevs.bootstrapi.commons.model.ApplicationLinkModel; +import com.deftdevs.bootstrapi.commons.model.AuthenticationModel; +import com.deftdevs.bootstrapi.commons.model.LicenseModel; +import com.deftdevs.bootstrapi.commons.model.MailServerModel; +import com.deftdevs.bootstrapi.commons.model.MailServerSmtpModel; +import com.deftdevs.bootstrapi.commons.model.PermissionsGlobalModel; +import com.deftdevs.bootstrapi.commons.model.SettingsGeneralModel; +import com.deftdevs.bootstrapi.commons.model.type.ServiceResult; +import com.deftdevs.bootstrapi.commons.model.type._AllModelStatus; +import com.deftdevs.bootstrapi.commons.service.api.ApplicationLinksService; +import com.deftdevs.bootstrapi.commons.service.api.DirectoriesService; +import com.deftdevs.bootstrapi.commons.service.api.LicensesService; +import com.deftdevs.bootstrapi.commons.service.api.MailServerService; +import com.deftdevs.bootstrapi.commons.service.api.PermissionsService; +import com.deftdevs.bootstrapi.confluence.model.SettingsModel; +import com.deftdevs.bootstrapi.confluence.model._AllModel; +import com.deftdevs.bootstrapi.confluence.service.api.ConfluenceAuthenticationService; +import com.deftdevs.bootstrapi.confluence.service.api.ConfluenceSettingsService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.Response; +import java.util.Collections; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verifyNoInteractions; + +@ExtendWith(MockitoExtension.class) +class _AllServiceImplTest { + + @Mock + private ConfluenceSettingsService settingsService; + + @Mock + private DirectoriesService directoriesService; + + @Mock + private ApplicationLinksService applicationLinksService; + + @Mock + private ConfluenceAuthenticationService authenticationService; + + @Mock + private LicensesService licensesService; + + @Mock + private MailServerService mailServerService; + + @Mock + private PermissionsService permissionsService; + + private _AllServiceImpl allService; + + @BeforeEach + void setup() { + allService = new _AllServiceImpl( + settingsService, + directoriesService, + applicationLinksService, + authenticationService, + licensesService, + mailServerService, + permissionsService); + } + + @Test + void testSetAllEmptyModelYieldsEmptyStatus() { + final _AllModel result = allService.setAll(new _AllModel()); + + assertTrue(result.getStatus().isEmpty()); + verifyNoInteractions(settingsService, directoriesService, applicationLinksService, + authenticationService, licensesService, mailServerService, permissionsService); + } + + @Test + void testSetAllAppliesAllFields() { + final SettingsModel settings = new SettingsModel(); + settings.setGeneral(SettingsGeneralModel.EXAMPLE_1); + final Map directories = + Collections.singletonMap("directory", mock(AbstractDirectoryModel.class)); + final Map applicationLinks = + Collections.singletonMap("link", ApplicationLinkModel.EXAMPLE_1); + final AuthenticationModel authentication = new AuthenticationModel(); + final Map licenses = + Collections.singletonMap("licenseKey", LicenseModel.EXAMPLE_1); + final Map redactedLicenses = + Collections.singletonMap("lice...nse1#abcd", LicenseModel.EXAMPLE_1); + final MailServerModel mailServer = new MailServerModel(MailServerSmtpModel.EXAMPLE_1, null); + final PermissionsGlobalModel permissionsGlobal = new PermissionsGlobalModel(); + + doReturn(new ServiceResult<>(settings, + Collections.singletonMap("settings/general", _AllModelStatus.success()))) + .when(settingsService).setSettings(settings); + doReturn(directories).when(directoriesService).setDirectories(directories); + doReturn(applicationLinks).when(applicationLinksService).setApplicationLinks(applicationLinks); + doReturn(new ServiceResult<>(authentication, + Collections.singletonMap("authentication/sso", _AllModelStatus.success()))) + .when(authenticationService).setAuthentication(authentication); + doReturn(redactedLicenses).when(licensesService).setLicenses(licenses); + doReturn(new ServiceResult<>(mailServer, + Collections.singletonMap("mail-server/smtp", _AllModelStatus.success()))) + .when(mailServerService).setMailServer(mailServer); + doReturn(permissionsGlobal).when(permissionsService).setPermissionsGlobal(permissionsGlobal); + + final _AllModel allModel = new _AllModel(); + allModel.setSettings(settings); + allModel.setDirectories(directories); + allModel.setApplicationLinks(applicationLinks); + allModel.setAuthentication(authentication); + allModel.setLicenses(licenses); + allModel.setMailServer(mailServer); + allModel.setPermissionsGlobal(permissionsGlobal); + + final _AllModel result = allService.setAll(allModel); + + assertEquals(settings, result.getSettings()); + assertEquals(directories, result.getDirectories()); + assertEquals(applicationLinks, result.getApplicationLinks()); + assertEquals(authentication, result.getAuthentication()); + assertEquals(redactedLicenses, result.getLicenses()); + assertEquals(mailServer, result.getMailServer()); + assertEquals(permissionsGlobal, result.getPermissionsGlobal()); + + final Map status = result.getStatus(); + assertEquals(7, status.size()); + assertEquals(Response.Status.OK.getStatusCode(), status.get("settings/general").getStatus()); + assertEquals(Response.Status.OK.getStatusCode(), status.get("directories").getStatus()); + assertEquals(Response.Status.OK.getStatusCode(), status.get("application-links").getStatus()); + assertEquals(Response.Status.OK.getStatusCode(), status.get("authentication/sso").getStatus()); + assertEquals(Response.Status.OK.getStatusCode(), status.get("licenses").getStatus()); + assertEquals(Response.Status.OK.getStatusCode(), status.get("mail-server/smtp").getStatus()); + assertEquals(Response.Status.OK.getStatusCode(), status.get("permissions/global").getStatus()); + } + + @Test + void testSetAllRecordsFailureAndContinuesWithOtherFields() { + final SettingsModel settings = new SettingsModel(); + settings.setGeneral(SettingsGeneralModel.EXAMPLE_1); + final Map licenses = + Collections.singletonMap("licenseKey", LicenseModel.EXAMPLE_1); + + doReturn(new ServiceResult<>(settings, + Collections.singletonMap("settings/general", _AllModelStatus.success()))) + .when(settingsService).setSettings(settings); + doThrow(new WebApplicationException(Response.Status.CONFLICT)) + .when(licensesService).setLicenses(licenses); + + final _AllModel allModel = new _AllModel(); + allModel.setSettings(settings); + allModel.setLicenses(licenses); + + final _AllModel result = allService.setAll(allModel); + + assertEquals(settings, result.getSettings()); + assertNull(result.getLicenses()); + assertEquals(Response.Status.OK.getStatusCode(), result.getStatus().get("settings/general").getStatus()); + assertEquals(Response.Status.CONFLICT.getStatusCode(), result.getStatus().get("licenses").getStatus()); + } +} diff --git a/confluence/src/test/java/it/com/deftdevs/bootstrapi/confluence/rest/SettingsBrandingResourceFuncTest.java b/confluence/src/test/java/it/com/deftdevs/bootstrapi/confluence/rest/SettingsBrandingResourceFuncTest.java new file mode 100644 index 00000000..c401daf4 --- /dev/null +++ b/confluence/src/test/java/it/com/deftdevs/bootstrapi/confluence/rest/SettingsBrandingResourceFuncTest.java @@ -0,0 +1,46 @@ +package it.com.deftdevs.bootstrapi.confluence.rest; + +import com.deftdevs.bootstrapi.commons.constants.BootstrAPI; +import com.deftdevs.bootstrapi.commons.model.SettingsBrandingColorSchemeModel; +import com.fasterxml.jackson.databind.ObjectMapper; +import it.com.deftdevs.bootstrapi.commons.rest.HttpRequestHelper; +import org.junit.jupiter.api.Test; + +import javax.ws.rs.core.Response; +import java.net.http.HttpResponse; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +/** + * Guards the branding resource's DI wiring: {@code SettingsBrandingService} must resolve + * to exactly one bean even though the composite {@code ConfluenceSettingsService} also + * implements it (see the note in {@code ServiceConfig#confluenceSettingsService()}). + */ +public class SettingsBrandingResourceFuncTest { + + private static final String COLOR_SCHEME_PATH = + BootstrAPI.SETTINGS + "/" + BootstrAPI.SETTINGS_BRANDING + "/" + BootstrAPI.COLOR_SCHEME; + + private final ObjectMapper objectMapper = new ObjectMapper(); + + @Test + void testGetColourScheme() throws Exception { + final HttpResponse colourSchemeResponse = HttpRequestHelper.builder(COLOR_SCHEME_PATH) + .request(); + assertEquals(Response.Status.OK.getStatusCode(), colourSchemeResponse.statusCode()); + + final SettingsBrandingColorSchemeModel colorSchemeModel = + objectMapper.readValue(colourSchemeResponse.body(), SettingsBrandingColorSchemeModel.class); + assertNotNull(colorSchemeModel); + } + + @Test + void testGetColourSchemeUnauthenticated() throws Exception { + final HttpResponse colourSchemeResponse = HttpRequestHelper.builder(COLOR_SCHEME_PATH) + .username("wrong") + .password("password") + .request(); + assertEquals(Response.Status.UNAUTHORIZED.getStatusCode(), colourSchemeResponse.statusCode()); + } +} diff --git a/confluence/src/test/java/it/com/deftdevs/bootstrapi/confluence/rest/SettingsGeneralResourceFuncTest.java b/confluence/src/test/java/it/com/deftdevs/bootstrapi/confluence/rest/SettingsGeneralResourceFuncTest.java new file mode 100644 index 00000000..0ce82df3 --- /dev/null +++ b/confluence/src/test/java/it/com/deftdevs/bootstrapi/confluence/rest/SettingsGeneralResourceFuncTest.java @@ -0,0 +1,12 @@ +package it.com.deftdevs.bootstrapi.confluence.rest; + +import com.deftdevs.bootstrapi.commons.model.SettingsGeneralModel; +import it.com.deftdevs.bootstrapi.commons.rest.AbstractSettingsGeneralResourceFuncTest; + +public class SettingsGeneralResourceFuncTest extends AbstractSettingsGeneralResourceFuncTest { + + @Override + protected SettingsGeneralModel getExampleModel() { + return SettingsGeneralModel.EXAMPLE_1_NO_MODE; + } +} diff --git a/confluence/src/test/java/it/com/deftdevs/bootstrapi/confluence/rest/SettingsResourceFuncTest.java b/confluence/src/test/java/it/com/deftdevs/bootstrapi/confluence/rest/SettingsResourceFuncTest.java deleted file mode 100644 index fbfb0ebb..00000000 --- a/confluence/src/test/java/it/com/deftdevs/bootstrapi/confluence/rest/SettingsResourceFuncTest.java +++ /dev/null @@ -1,12 +0,0 @@ -package it.com.deftdevs.bootstrapi.confluence.rest; - -import com.deftdevs.bootstrapi.commons.model.SettingsModel; -import it.com.deftdevs.bootstrapi.commons.rest.AbstractSettingsResourceFuncTest; - -public class SettingsResourceFuncTest extends AbstractSettingsResourceFuncTest { - - @Override - protected SettingsModel getExampleModel() { - return SettingsModel.EXAMPLE_1_NO_MODE; - } -} diff --git a/confluence/src/test/java/it/com/deftdevs/bootstrapi/confluence/rest/_AllResourceFuncTest.java b/confluence/src/test/java/it/com/deftdevs/bootstrapi/confluence/rest/_AllResourceFuncTest.java new file mode 100644 index 00000000..e689b0bf --- /dev/null +++ b/confluence/src/test/java/it/com/deftdevs/bootstrapi/confluence/rest/_AllResourceFuncTest.java @@ -0,0 +1,51 @@ +package it.com.deftdevs.bootstrapi.confluence.rest; + +import com.deftdevs.bootstrapi.commons.model.MailServerModel; +import com.deftdevs.bootstrapi.commons.model.MailServerPopModel; +import com.deftdevs.bootstrapi.commons.model.MailServerSmtpModel; +import com.deftdevs.bootstrapi.commons.model.SettingsGeneralModel; +import com.deftdevs.bootstrapi.commons.util.ServiceResultUtil; +import com.deftdevs.bootstrapi.confluence.model.SettingsCustomHtmlModel; +import com.deftdevs.bootstrapi.confluence.model.SettingsModel; +import com.deftdevs.bootstrapi.confluence.model._AllModel; +import it.com.deftdevs.bootstrapi.commons.rest.Abstract_AllResourceFuncTest; +import org.junit.jupiter.api.Test; + +import java.util.Arrays; + +public class _AllResourceFuncTest extends Abstract_AllResourceFuncTest { + + @Override + protected Object getExampleAllModel() { + return _AllModel.builder() + .settings(SettingsModel.builder() + .general(getExampleSettingsGeneralModel()) + .build()) + .build(); + } + + @Override + protected SettingsGeneralModel getExampleSettingsGeneralModel() { + return SettingsGeneralModel.EXAMPLE_1_NO_MODE; + } + + @Test + void testSetAllAppliesMultipleEntities() throws Exception { + final _AllModel allModel = _AllModel.builder() + .settings(SettingsModel.builder() + .general(getExampleSettingsGeneralModel()) + .customHtml(SettingsCustomHtmlModel.EXAMPLE_1) + .build()) + .mailServer(MailServerModel.builder() + .smtp(MailServerSmtpModel.EXAMPLE_2) + .pop(MailServerPopModel.EXAMPLE_2) + .build()) + .build(); + + assertSetAllApplied(allModel, Arrays.asList( + ServiceResultUtil.subEntityKey(SettingsGeneralModel.class), + ServiceResultUtil.subEntityKey(SettingsModel.class, SettingsCustomHtmlModel.class), + ServiceResultUtil.subEntityKey(MailServerSmtpModel.class), + ServiceResultUtil.subEntityKey(MailServerPopModel.class))); + } +} diff --git a/crowd/Apis/AllApi.md b/crowd/Apis/AllApi.md new file mode 100644 index 00000000..c56ea6de --- /dev/null +++ b/crowd/Apis/AllApi.md @@ -0,0 +1,36 @@ +# AllApi + +All URIs are relative to *https://CROWD_URL/rest/bootstrapi/1* + +| Method | HTTP request | Description | +|------------- | ------------- | -------------| +| [**setAll**](AllApi.md#setAll) | **PUT** / | Apply a complete configuration | + + + +# **setAll** +> _AllModel setAll(\_AllModel) + +Apply a complete configuration + + Returns the updated configuration. The per-sub-field outcome is reported in the 'status' map (2xx for success, 4xx/5xx for failure with a human-readable 'message' and optional 'details'). License keys in the response are redacted (e.g. 'AAAB...wxyz#a1b2'). + +### Parameters + +|Name | Type | Description | Notes | +|------------- | ------------- | ------------- | -------------| +| **\_AllModel** | [**_AllModel**](../Models/_AllModel.md)| | | + +### Return type + +[**_AllModel**](../Models/_AllModel.md) + +### Authorization + +[basicAuth](../README.md#basicAuth) + +### HTTP request headers + +- **Content-Type**: application/json +- **Accept**: application/json + diff --git a/crowd/Apis/SettingsApi.md b/crowd/Apis/SettingsApi.md index 7b783d13..a9b82cdd 100644 --- a/crowd/Apis/SettingsApi.md +++ b/crowd/Apis/SettingsApi.md @@ -35,7 +35,7 @@ This endpoint does not need any parameter. # **getSettings** -> SettingsModel getSettings() +> SettingsGeneralModel getSettings() Get the general settings @@ -44,7 +44,7 @@ This endpoint does not need any parameter. ### Return type -[**SettingsModel**](../Models/SettingsModel.md) +[**SettingsGeneralModel**](../Models/SettingsGeneralModel.md) ### Authorization @@ -107,7 +107,7 @@ Set the logo # **setSettings** -> SettingsModel setSettings(SettingsModel) +> SettingsGeneralModel setSettings(SettingsGeneralModel) Set the general settings @@ -115,11 +115,11 @@ Set the general settings |Name | Type | Description | Notes | |------------- | ------------- | ------------- | -------------| -| **SettingsModel** | [**SettingsModel**](../Models/SettingsModel.md)| | [optional] | +| **SettingsGeneralModel** | [**SettingsGeneralModel**](../Models/SettingsGeneralModel.md)| | [optional] | ### Return type -[**SettingsModel**](../Models/SettingsModel.md) +[**SettingsGeneralModel**](../Models/SettingsGeneralModel.md) ### Authorization diff --git a/crowd/Models/MailServerModel.md b/crowd/Models/MailServerModel.md new file mode 100644 index 00000000..34c39bf7 --- /dev/null +++ b/crowd/Models/MailServerModel.md @@ -0,0 +1,10 @@ +# MailServerModel +## Properties + +| Name | Type | Description | Notes | +|------------ | ------------- | ------------- | -------------| +| **smtp** | [**MailServerSmtpModel**](MailServerSmtpModel.md) | | [optional] [default to null] | +| **pop** | [**MailServerPopModel**](MailServerPopModel.md) | | [optional] [default to null] | + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + diff --git a/crowd/Models/MailServerPopModel.md b/crowd/Models/MailServerPopModel.md new file mode 100644 index 00000000..72025fb3 --- /dev/null +++ b/crowd/Models/MailServerPopModel.md @@ -0,0 +1,16 @@ +# MailServerPopModel +## Properties + +| Name | Type | Description | Notes | +|------------ | ------------- | ------------- | -------------| +| **name** | **String** | | [optional] [default to null] | +| **description** | **String** | | [optional] [default to null] | +| **host** | **String** | | [optional] [default to null] | +| **port** | **Integer** | | [optional] [default to null] | +| **protocol** | **String** | | [optional] [default to null] | +| **timeout** | **Long** | | [optional] [default to null] | +| **username** | **String** | | [optional] [default to null] | +| **password** | **String** | | [optional] [default to null] | + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + diff --git a/crowd/Models/SettingsGeneralModel.md b/crowd/Models/SettingsGeneralModel.md new file mode 100644 index 00000000..8ebe677b --- /dev/null +++ b/crowd/Models/SettingsGeneralModel.md @@ -0,0 +1,13 @@ +# SettingsGeneralModel +## Properties + +| Name | Type | Description | Notes | +|------------ | ------------- | ------------- | -------------| +| **baseUrl** | **URI** | | [optional] [default to null] | +| **mode** | **String** | | [optional] [default to null] | +| **title** | **String** | | [optional] [default to null] | +| **contactMessage** | **String** | | [optional] [default to null] | +| **externalUserManagement** | **Boolean** | | [optional] [default to null] | + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + diff --git a/crowd/Models/SettingsModel.md b/crowd/Models/SettingsModel.md index f4eb0e7f..6a6a5063 100644 --- a/crowd/Models/SettingsModel.md +++ b/crowd/Models/SettingsModel.md @@ -3,11 +3,8 @@ | Name | Type | Description | Notes | |------------ | ------------- | ------------- | -------------| -| **baseUrl** | **URI** | | [optional] [default to null] | -| **mode** | **String** | | [optional] [default to null] | -| **title** | **String** | | [optional] [default to null] | -| **contactMessage** | **String** | | [optional] [default to null] | -| **externalUserManagement** | **Boolean** | | [optional] [default to null] | +| **general** | [**SettingsGeneralModel**](SettingsGeneralModel.md) | | [optional] [default to null] | +| **branding** | [**SettingsBrandingLoginPageModel**](SettingsBrandingLoginPageModel.md) | | [optional] [default to null] | [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) diff --git a/crowd/Models/_AllModel.md b/crowd/Models/_AllModel.md new file mode 100644 index 00000000..5f85397e --- /dev/null +++ b/crowd/Models/_AllModel.md @@ -0,0 +1,18 @@ +# _AllModel +## Properties + +| Name | Type | Description | Notes | +|------------ | ------------- | ------------- | -------------| +| **settings** | [**SettingsModel**](SettingsModel.md) | | [optional] [default to null] | +| **directories** | [**Map**](AbstractDirectoryModel.md) | | [optional] [default to null] | +| **applications** | [**Map**](ApplicationModel.md) | | [optional] [default to null] | +| **applicationLinks** | [**Map**](ApplicationLinkModel.md) | | [optional] [default to null] | +| **licenses** | [**Map**](LicenseModel.md) | | [optional] [default to null] | +| **mailServer** | [**MailServerModel**](MailServerModel.md) | | [optional] [default to null] | +| **mailTemplates** | [**MailTemplatesModel**](MailTemplatesModel.md) | | [optional] [default to null] | +| **sessionConfig** | [**SessionConfigModel**](SessionConfigModel.md) | | [optional] [default to null] | +| **trustedProxies** | **List** | | [optional] [default to null] | +| **status** | [**Map**](_AllModelStatus.md) | | [optional] [default to null] | + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + diff --git a/crowd/Models/_AllModelStatus.md b/crowd/Models/_AllModelStatus.md new file mode 100644 index 00000000..ed699765 --- /dev/null +++ b/crowd/Models/_AllModelStatus.md @@ -0,0 +1,11 @@ +# _AllModelStatus +## Properties + +| Name | Type | Description | Notes | +|------------ | ------------- | ------------- | -------------| +| **status** | **Integer** | | [optional] [default to null] | +| **message** | **String** | | [optional] [default to null] | +| **details** | **String** | | [optional] [default to null] | + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + diff --git a/crowd/README.md b/crowd/README.md index 7993ee1b..3ae0bcf4 100644 --- a/crowd/README.md +++ b/crowd/README.md @@ -7,6 +7,7 @@ All URIs are relative to *https://CROWD_URL/rest/bootstrapi/1* | Class | Method | HTTP request | Description | |------------ | ------------- | ------------- | -------------| +| *AllApi* | [**setAll**](Apis/AllApi.md#setAll) | **PUT** / | Apply a complete configuration | | *ApplicationApi* | [**createApplication**](Apis/ApplicationApi.md#createApplication) | **POST** /application | Create an application | *ApplicationApi* | [**deleteApplication**](Apis/ApplicationApi.md#deleteApplication) | **DELETE** /application/{id} | Delete an application | *ApplicationApi* | [**getApplication**](Apis/ApplicationApi.md#getApplication) | **GET** /application/{id} | Get an application | @@ -84,12 +85,17 @@ All URIs are relative to *https://CROWD_URL/rest/bootstrapi/1* - [ErrorCollection](./Models/ErrorCollection.md) - [GroupModel](./Models/GroupModel.md) - [LicenseModel](./Models/LicenseModel.md) + - [MailServerModel](./Models/MailServerModel.md) + - [MailServerPopModel](./Models/MailServerPopModel.md) - [MailServerSmtpModel](./Models/MailServerSmtpModel.md) - [MailTemplatesModel](./Models/MailTemplatesModel.md) - [SessionConfigModel](./Models/SessionConfigModel.md) - [SettingsBrandingLoginPageModel](./Models/SettingsBrandingLoginPageModel.md) + - [SettingsGeneralModel](./Models/SettingsGeneralModel.md) - [SettingsModel](./Models/SettingsModel.md) - [UserModel](./Models/UserModel.md) + - [_AllModel](./Models/_AllModel.md) + - [_AllModelStatus](./Models/_AllModelStatus.md) diff --git a/crowd/src/main/java/com/deftdevs/bootstrapi/crowd/config/ServiceConfig.java b/crowd/src/main/java/com/deftdevs/bootstrapi/crowd/config/ServiceConfig.java index 8528fb6b..296440e0 100644 --- a/crowd/src/main/java/com/deftdevs/bootstrapi/crowd/config/ServiceConfig.java +++ b/crowd/src/main/java/com/deftdevs/bootstrapi/crowd/config/ServiceConfig.java @@ -1,6 +1,7 @@ package com.deftdevs.bootstrapi.crowd.config; import com.deftdevs.bootstrapi.commons.service.api.*; +import com.deftdevs.bootstrapi.crowd.model._AllModel; import com.deftdevs.bootstrapi.crowd.service.*; import com.deftdevs.bootstrapi.crowd.service.api.*; import org.springframework.beans.factory.annotation.Autowired; @@ -16,6 +17,25 @@ public class ServiceConfig { @Autowired private HelperConfig helperConfig; + @Bean + public _AllService<_AllModel> _allService() { + // The composite settings service is deliberately NOT a bean: its type extends + // CrowdSettingsGeneralService and CrowdSettingsBrandingService, so exposing it + // would make by-type injection of those interfaces ambiguous in the REST layer. + return new _AllServiceImpl( + new CrowdSettingsServiceImpl( + crowdSettingsGeneralService(), + settingsBrandingService()), + directoriesService(), + applicationsService(), + applicationLinksService(), + licensesService(), + mailServerService(), + mailTemplatesService(), + sessionConfigService(), + trustedProxiesService()); + } + @Bean public ApplicationLinksService applicationLinksService() { return new ApplicationLinksServiceImpl( diff --git a/crowd/src/main/java/com/deftdevs/bootstrapi/crowd/model/SettingsBrandingLoginPageModel.java b/crowd/src/main/java/com/deftdevs/bootstrapi/crowd/model/SettingsBrandingLoginPageModel.java index 3107978b..d1a327a2 100644 --- a/crowd/src/main/java/com/deftdevs/bootstrapi/crowd/model/SettingsBrandingLoginPageModel.java +++ b/crowd/src/main/java/com/deftdevs/bootstrapi/crowd/model/SettingsBrandingLoginPageModel.java @@ -1,6 +1,5 @@ package com.deftdevs.bootstrapi.crowd.model; -import com.deftdevs.bootstrapi.commons.constants.BootstrAPI; import lombok.AllArgsConstructor; import lombok.Data; import lombok.Builder; @@ -9,11 +8,14 @@ import javax.xml.bind.annotation.XmlElement; import javax.xml.bind.annotation.XmlRootElement; +import static com.deftdevs.bootstrapi.commons.constants.BootstrAPI.SETTINGS; +import static com.deftdevs.bootstrapi.commons.constants.BootstrAPI.SETTINGS_BRANDING; + @Data @Builder @NoArgsConstructor @AllArgsConstructor -@XmlRootElement(name = BootstrAPI.SETTINGS_BRANDING_LOGIN_PAGE) +@XmlRootElement(name = SETTINGS + "-" + SETTINGS_BRANDING) public class SettingsBrandingLoginPageModel { @XmlElement diff --git a/crowd/src/main/java/com/deftdevs/bootstrapi/crowd/model/AllModel.java b/crowd/src/main/java/com/deftdevs/bootstrapi/crowd/model/SettingsModel.java similarity index 53% rename from crowd/src/main/java/com/deftdevs/bootstrapi/crowd/model/AllModel.java rename to crowd/src/main/java/com/deftdevs/bootstrapi/crowd/model/SettingsModel.java index 94eb5694..89173705 100644 --- a/crowd/src/main/java/com/deftdevs/bootstrapi/crowd/model/AllModel.java +++ b/crowd/src/main/java/com/deftdevs/bootstrapi/crowd/model/SettingsModel.java @@ -1,26 +1,27 @@ package com.deftdevs.bootstrapi.crowd.model; -import com.deftdevs.bootstrapi.commons.model.SettingsModel; +import com.deftdevs.bootstrapi.commons.model.SettingsGeneralModel; import lombok.AllArgsConstructor; -import lombok.Data; import lombok.Builder; +import lombok.Data; import lombok.NoArgsConstructor; import javax.xml.bind.annotation.XmlElement; import javax.xml.bind.annotation.XmlRootElement; -import java.util.List; + +import static com.deftdevs.bootstrapi.commons.constants.BootstrAPI.SETTINGS; @Data @Builder @NoArgsConstructor @AllArgsConstructor -@XmlRootElement(name = "all") -public class AllModel { +@XmlRootElement(name = SETTINGS) +public class SettingsModel { @XmlElement - private SettingsModel settings; + private SettingsGeneralModel general; @XmlElement - private List applications; + private SettingsBrandingLoginPageModel branding; } diff --git a/crowd/src/main/java/com/deftdevs/bootstrapi/crowd/model/_AllModel.java b/crowd/src/main/java/com/deftdevs/bootstrapi/crowd/model/_AllModel.java new file mode 100644 index 00000000..ab258631 --- /dev/null +++ b/crowd/src/main/java/com/deftdevs/bootstrapi/crowd/model/_AllModel.java @@ -0,0 +1,57 @@ +package com.deftdevs.bootstrapi.crowd.model; + +import com.deftdevs.bootstrapi.commons.constants.BootstrAPI; +import com.deftdevs.bootstrapi.commons.model.AbstractDirectoryModel; +import com.deftdevs.bootstrapi.commons.model.ApplicationLinkModel; +import com.deftdevs.bootstrapi.commons.model.LicenseModel; +import com.deftdevs.bootstrapi.commons.model.MailServerModel; +import com.deftdevs.bootstrapi.commons.model.type._AllModelAccessor; +import com.deftdevs.bootstrapi.commons.model.type._AllModelStatus; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; +import java.util.List; +import java.util.Map; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@XmlRootElement(name = BootstrAPI._ALL) +public class _AllModel implements _AllModelAccessor { + + @XmlElement + private SettingsModel settings; + + @XmlElement + private Map directories; + + @XmlElement + private Map applications; + + @XmlElement + private Map applicationLinks; + + @XmlElement + private Map licenses; + + @XmlElement + private MailServerModel mailServer; + + @XmlElement + private MailTemplatesModel mailTemplates; + + @XmlElement + private SessionConfigModel sessionConfig; + + @XmlElement + private List trustedProxies; + + @XmlElement + private Map status; + +} diff --git a/crowd/src/main/java/com/deftdevs/bootstrapi/crowd/rest/SettingsResourceImpl.java b/crowd/src/main/java/com/deftdevs/bootstrapi/crowd/rest/SettingsGeneralResourceImpl.java similarity index 69% rename from crowd/src/main/java/com/deftdevs/bootstrapi/crowd/rest/SettingsResourceImpl.java rename to crowd/src/main/java/com/deftdevs/bootstrapi/crowd/rest/SettingsGeneralResourceImpl.java index 22299041..fd13dea8 100644 --- a/crowd/src/main/java/com/deftdevs/bootstrapi/crowd/rest/SettingsResourceImpl.java +++ b/crowd/src/main/java/com/deftdevs/bootstrapi/crowd/rest/SettingsGeneralResourceImpl.java @@ -2,8 +2,8 @@ import com.atlassian.plugins.rest.api.security.annotation.SystemAdminOnly; import com.deftdevs.bootstrapi.commons.constants.BootstrAPI; -import com.deftdevs.bootstrapi.commons.model.SettingsModel; -import com.deftdevs.bootstrapi.commons.rest.AbstractSettingsResourceImpl; +import com.deftdevs.bootstrapi.commons.model.SettingsGeneralModel; +import com.deftdevs.bootstrapi.commons.rest.AbstractSettingsGeneralResourceImpl; import com.deftdevs.bootstrapi.crowd.service.api.CrowdSettingsGeneralService; import io.swagger.v3.oas.annotations.tags.Tag; @@ -19,10 +19,10 @@ @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) @SystemAdminOnly -public class SettingsResourceImpl extends AbstractSettingsResourceImpl { +public class SettingsGeneralResourceImpl extends AbstractSettingsGeneralResourceImpl { @Inject - public SettingsResourceImpl( + public SettingsGeneralResourceImpl( final CrowdSettingsGeneralService settingsService) { super(settingsService); diff --git a/crowd/src/main/java/com/deftdevs/bootstrapi/crowd/rest/_AllResourceImpl.java b/crowd/src/main/java/com/deftdevs/bootstrapi/crowd/rest/_AllResourceImpl.java new file mode 100644 index 00000000..b44f3620 --- /dev/null +++ b/crowd/src/main/java/com/deftdevs/bootstrapi/crowd/rest/_AllResourceImpl.java @@ -0,0 +1,64 @@ +package com.deftdevs.bootstrapi.crowd.rest; + +import com.atlassian.plugins.rest.api.security.annotation.SystemAdminOnly; +import com.deftdevs.bootstrapi.commons.constants.BootstrAPI; +import com.deftdevs.bootstrapi.commons.model.ErrorCollection; +import com.deftdevs.bootstrapi.commons.rest._AbstractAllResourceImpl; +import com.deftdevs.bootstrapi.commons.service.api._AllService; +import com.deftdevs.bootstrapi.crowd.model._AllModel; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.tags.Tag; + +import javax.inject.Inject; +import javax.ws.rs.Consumes; +import javax.ws.rs.PUT; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; + +@Path(BootstrAPI._ROOT) +@Tag(name = BootstrAPI._ALL) +@Consumes(MediaType.APPLICATION_JSON) +@Produces(MediaType.APPLICATION_JSON) +@SystemAdminOnly +public class _AllResourceImpl extends _AbstractAllResourceImpl<_AllModel> { + + @Inject + public _AllResourceImpl( + final _AllService<_AllModel> allService) { + + super(allService); + } + + // overridden to document the concrete response model in the generated OpenAPI spec + @PUT + @Operation( + summary = BootstrAPI._ALL_PUT_SUMMARY, + description = BootstrAPI._ALL_PUT_RESPONSE_DESCRIPTION, + responses = { + @ApiResponse( + responseCode = "200", content = @Content(schema = @Schema(implementation = _AllModel.class)), + description = BootstrAPI._ALL_PUT_RESPONSE_DESCRIPTION + ), + @ApiResponse( + responseCode = "207", content = @Content(schema = @Schema(implementation = _AllModel.class)), + description = BootstrAPI._ALL_PUT_PARTIAL_RESPONSE_DESCRIPTION + ), + @ApiResponse( + responseCode = "default", content = @Content(schema = @Schema(implementation = ErrorCollection.class)), + description = BootstrAPI.ERROR_COLLECTION_RESPONSE_DESCRIPTION + ), + } + ) + @Override + public Response setAll( + final _AllModel allModel) { + + return super.setAll(allModel); + } + +} diff --git a/crowd/src/main/java/com/deftdevs/bootstrapi/crowd/service/CrowdSettingsServiceImpl.java b/crowd/src/main/java/com/deftdevs/bootstrapi/crowd/service/CrowdSettingsServiceImpl.java new file mode 100644 index 00000000..477e9f9b --- /dev/null +++ b/crowd/src/main/java/com/deftdevs/bootstrapi/crowd/service/CrowdSettingsServiceImpl.java @@ -0,0 +1,49 @@ +package com.deftdevs.bootstrapi.crowd.service; + +import com.deftdevs.bootstrapi.commons.model.SettingsGeneralModel; +import com.deftdevs.bootstrapi.crowd.model.SettingsBrandingLoginPageModel; +import com.deftdevs.bootstrapi.crowd.service.api.CrowdSettingsBrandingService; +import com.deftdevs.bootstrapi.crowd.service.api.CrowdSettingsGeneralService; +import com.deftdevs.bootstrapi.crowd.service.api.CrowdSettingsService; + +import java.io.InputStream; + +public class CrowdSettingsServiceImpl implements CrowdSettingsService { + + private final CrowdSettingsGeneralService settingsService; + private final CrowdSettingsBrandingService settingsBrandingService; + + public CrowdSettingsServiceImpl( + final CrowdSettingsGeneralService settingsService, + final CrowdSettingsBrandingService settingsBrandingService) { + + this.settingsService = settingsService; + this.settingsBrandingService = settingsBrandingService; + } + + @Override + public SettingsGeneralModel getSettingsGeneral() { + return settingsService.getSettingsGeneral(); + } + + @Override + public SettingsGeneralModel setSettingsGeneral(final SettingsGeneralModel settingsGeneralModel) { + return settingsService.setSettingsGeneral(settingsGeneralModel); + } + + @Override + public SettingsBrandingLoginPageModel getLoginPage() { + return settingsBrandingService.getLoginPage(); + } + + @Override + public SettingsBrandingLoginPageModel setLoginPage(final SettingsBrandingLoginPageModel settingsBrandingLoginPageModel) { + return settingsBrandingService.setLoginPage(settingsBrandingLoginPageModel); + } + + @Override + public void setLogo(final InputStream inputStream) { + settingsBrandingService.setLogo(inputStream); + } + +} diff --git a/crowd/src/main/java/com/deftdevs/bootstrapi/crowd/service/LicensesServiceImpl.java b/crowd/src/main/java/com/deftdevs/bootstrapi/crowd/service/LicensesServiceImpl.java index 31a0d9cc..3998434e 100644 --- a/crowd/src/main/java/com/deftdevs/bootstrapi/crowd/service/LicensesServiceImpl.java +++ b/crowd/src/main/java/com/deftdevs/bootstrapi/crowd/service/LicensesServiceImpl.java @@ -6,9 +6,12 @@ import com.deftdevs.bootstrapi.commons.exception.web.InternalServerErrorException; import com.deftdevs.bootstrapi.commons.model.LicenseModel; import com.deftdevs.bootstrapi.commons.service.api.LicensesService; +import com.deftdevs.bootstrapi.commons.util.LicenseKeyRedactor; import java.util.Collections; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; public class LicensesServiceImpl implements LicensesService { @@ -40,6 +43,17 @@ public List setLicenses( ); } + @Override + public Map setLicenses( + final Map licenseInputs) { + + final Map result = new LinkedHashMap<>(); + for (final String key : licenseInputs.keySet()) { + result.put(LicenseKeyRedactor.redact(key), addLicense(key)); + } + return result; + } + public LicenseModel addLicense( final String licenseKey) { diff --git a/crowd/src/main/java/com/deftdevs/bootstrapi/crowd/service/SettingsServiceImpl.java b/crowd/src/main/java/com/deftdevs/bootstrapi/crowd/service/SettingsServiceImpl.java index ec4f6d64..a8cee89d 100644 --- a/crowd/src/main/java/com/deftdevs/bootstrapi/crowd/service/SettingsServiceImpl.java +++ b/crowd/src/main/java/com/deftdevs/bootstrapi/crowd/service/SettingsServiceImpl.java @@ -3,7 +3,7 @@ import com.atlassian.crowd.manager.property.PropertyManager; import com.atlassian.crowd.manager.property.PropertyManagerException; import com.deftdevs.bootstrapi.commons.exception.web.InternalServerErrorException; -import com.deftdevs.bootstrapi.commons.model.SettingsModel; +import com.deftdevs.bootstrapi.commons.model.SettingsGeneralModel; import com.deftdevs.bootstrapi.crowd.service.api.CrowdSettingsGeneralService; public class SettingsServiceImpl @@ -18,9 +18,9 @@ public SettingsServiceImpl( } @Override - public SettingsModel getSettingsGeneral() { + public SettingsGeneralModel getSettingsGeneral() { try { - return SettingsModel.builder() + return SettingsGeneralModel.builder() .baseUrl(propertyManager.getBaseUrl()) .title(propertyManager.getDeploymentTitle()) .build(); @@ -30,7 +30,7 @@ public SettingsModel getSettingsGeneral() { } @Override - public SettingsModel setSettingsGeneral(SettingsModel settingsModel) { + public SettingsGeneralModel setSettingsGeneral(SettingsGeneralModel settingsModel) { if (settingsModel.getBaseUrl() != null) { propertyManager.setBaseUrl(settingsModel.getBaseUrl()); } diff --git a/crowd/src/main/java/com/deftdevs/bootstrapi/crowd/service/_AllServiceImpl.java b/crowd/src/main/java/com/deftdevs/bootstrapi/crowd/service/_AllServiceImpl.java new file mode 100644 index 00000000..f2aec961 --- /dev/null +++ b/crowd/src/main/java/com/deftdevs/bootstrapi/crowd/service/_AllServiceImpl.java @@ -0,0 +1,99 @@ +package com.deftdevs.bootstrapi.crowd.service; + +import com.deftdevs.bootstrapi.commons.model.type._AllModelStatus; +import com.deftdevs.bootstrapi.commons.service._AbstractAllServiceImpl; +import com.deftdevs.bootstrapi.commons.service.api.ApplicationLinksService; +import com.deftdevs.bootstrapi.commons.service.api.DirectoriesService; +import com.deftdevs.bootstrapi.commons.service.api.LicensesService; +import com.deftdevs.bootstrapi.commons.service.api.MailServerService; +import com.deftdevs.bootstrapi.commons.util.ServiceResultUtil; +import com.deftdevs.bootstrapi.crowd.model.MailTemplatesModel; +import com.deftdevs.bootstrapi.crowd.model.SessionConfigModel; +import com.deftdevs.bootstrapi.crowd.model._AllModel; +import com.deftdevs.bootstrapi.crowd.service.api.ApplicationsService; +import com.deftdevs.bootstrapi.crowd.service.api.CrowdSettingsService; +import com.deftdevs.bootstrapi.crowd.service.api.MailTemplatesService; +import com.deftdevs.bootstrapi.crowd.service.api.SessionConfigService; +import com.deftdevs.bootstrapi.crowd.service.api.TrustedProxiesService; + +import java.util.HashMap; +import java.util.Map; + +import static com.deftdevs.bootstrapi.commons.constants.BootstrAPI.APPLICATIONS; +import static com.deftdevs.bootstrapi.commons.constants.BootstrAPI.APPLICATION_LINKS; +import static com.deftdevs.bootstrapi.commons.constants.BootstrAPI.DIRECTORIES; +import static com.deftdevs.bootstrapi.commons.constants.BootstrAPI.LICENSES; +import static com.deftdevs.bootstrapi.commons.constants.BootstrAPI.TRUSTED_PROXIES; + +public class _AllServiceImpl extends _AbstractAllServiceImpl<_AllModel> { + + private final CrowdSettingsService settingsService; + private final DirectoriesService directoriesService; + private final ApplicationsService applicationsService; + private final ApplicationLinksService applicationLinksService; + private final LicensesService licensesService; + private final MailServerService mailServerService; + private final MailTemplatesService mailTemplatesService; + private final SessionConfigService sessionConfigService; + private final TrustedProxiesService trustedProxiesService; + + public _AllServiceImpl( + final CrowdSettingsService settingsService, + final DirectoriesService directoriesService, + final ApplicationsService applicationsService, + final ApplicationLinksService applicationLinksService, + final LicensesService licensesService, + final MailServerService mailServerService, + final MailTemplatesService mailTemplatesService, + final SessionConfigService sessionConfigService, + final TrustedProxiesService trustedProxiesService) { + + this.settingsService = settingsService; + this.directoriesService = directoriesService; + this.applicationsService = applicationsService; + this.applicationLinksService = applicationLinksService; + this.licensesService = licensesService; + this.mailServerService = mailServerService; + this.mailTemplatesService = mailTemplatesService; + this.sessionConfigService = sessionConfigService; + this.trustedProxiesService = trustedProxiesService; + } + + @Override + public _AllModel setAll( + final _AllModel allModel) { + + final _AllModel result = new _AllModel(); + final Map statusMap = new HashMap<>(); + + setEntityWithStatus(allModel.getSettings(), + settingsService::setSettings, result::setSettings, statusMap); + + setEntities(DIRECTORIES, allModel.getDirectories(), + directoriesService::setDirectories, result::setDirectories, statusMap); + + setEntities(APPLICATIONS, allModel.getApplications(), + applicationsService::setApplications, result::setApplications, statusMap); + + setEntities(APPLICATION_LINKS, allModel.getApplicationLinks(), + applicationLinksService::setApplicationLinks, result::setApplicationLinks, statusMap); + + setEntities(LICENSES, allModel.getLicenses(), + licensesService::setLicenses, result::setLicenses, statusMap); + + setEntityWithStatus(allModel.getMailServer(), + mailServerService::setMailServer, result::setMailServer, statusMap); + + setEntity(ServiceResultUtil.entityType(MailTemplatesModel.class), allModel.getMailTemplates(), + mailTemplatesService::setMailTemplates, result::setMailTemplates, statusMap); + + setEntity(ServiceResultUtil.entityType(SessionConfigModel.class), allModel.getSessionConfig(), + sessionConfigService::setSessionConfig, result::setSessionConfig, statusMap); + + setEntity(TRUSTED_PROXIES, allModel.getTrustedProxies(), + trustedProxiesService::setTrustedProxies, result::setTrustedProxies, statusMap); + + result.setStatus(statusMap); + return result; + } +} diff --git a/crowd/src/main/java/com/deftdevs/bootstrapi/crowd/service/api/AllService.java b/crowd/src/main/java/com/deftdevs/bootstrapi/crowd/service/api/AllService.java deleted file mode 100644 index 71e5a090..00000000 --- a/crowd/src/main/java/com/deftdevs/bootstrapi/crowd/service/api/AllService.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.deftdevs.bootstrapi.crowd.service.api; - -import com.deftdevs.bootstrapi.crowd.model.AllModel; - -public interface AllService { - - void setAll( - AllModel allModel); - -} diff --git a/crowd/src/main/java/com/deftdevs/bootstrapi/crowd/service/api/CrowdSettingsGeneralService.java b/crowd/src/main/java/com/deftdevs/bootstrapi/crowd/service/api/CrowdSettingsGeneralService.java index a7098e91..610100cb 100644 --- a/crowd/src/main/java/com/deftdevs/bootstrapi/crowd/service/api/CrowdSettingsGeneralService.java +++ b/crowd/src/main/java/com/deftdevs/bootstrapi/crowd/service/api/CrowdSettingsGeneralService.java @@ -1,8 +1,8 @@ package com.deftdevs.bootstrapi.crowd.service.api; -import com.deftdevs.bootstrapi.commons.model.SettingsModel; -import com.deftdevs.bootstrapi.commons.service.api.SettingsService; +import com.deftdevs.bootstrapi.commons.model.SettingsGeneralModel; +import com.deftdevs.bootstrapi.commons.service.api.SettingsGeneralService; public interface CrowdSettingsGeneralService extends - SettingsService { + SettingsGeneralService { } diff --git a/crowd/src/main/java/com/deftdevs/bootstrapi/crowd/service/api/CrowdSettingsService.java b/crowd/src/main/java/com/deftdevs/bootstrapi/crowd/service/api/CrowdSettingsService.java new file mode 100644 index 00000000..1bea779f --- /dev/null +++ b/crowd/src/main/java/com/deftdevs/bootstrapi/crowd/service/api/CrowdSettingsService.java @@ -0,0 +1,46 @@ +package com.deftdevs.bootstrapi.crowd.service.api; + +import com.deftdevs.bootstrapi.commons.model.SettingsGeneralModel; +import com.deftdevs.bootstrapi.commons.model.type._AllModelStatus; +import com.deftdevs.bootstrapi.commons.model.type.ServiceResult; +import com.deftdevs.bootstrapi.commons.util.ServiceResultUtil; +import com.deftdevs.bootstrapi.crowd.model.SettingsBrandingLoginPageModel; +import com.deftdevs.bootstrapi.crowd.model.SettingsModel; + +import java.util.LinkedHashMap; +import java.util.Map; + +public interface CrowdSettingsService extends + CrowdSettingsGeneralService, + CrowdSettingsBrandingService { + + default SettingsModel getSettings() { + return new SettingsModel(getSettingsGeneral(), getLoginPage()); + } + + default ServiceResult setSettings(final SettingsModel settingsModel) { + final SettingsModel result = new SettingsModel(); + final Map status = new LinkedHashMap<>(); + + if (settingsModel.getGeneral() != null) { + final String key = ServiceResultUtil.subEntityKey(SettingsGeneralModel.class); + try { + result.setGeneral(setSettingsGeneral(settingsModel.getGeneral())); + status.put(key, _AllModelStatus.success()); + } catch (Exception e) { + status.put(key, ServiceResultUtil.toErrorStatus(key, e)); + } + } + if (settingsModel.getBranding() != null) { + final String key = ServiceResultUtil.subEntityKey(SettingsBrandingLoginPageModel.class); + try { + result.setBranding(setLoginPage(settingsModel.getBranding())); + status.put(key, _AllModelStatus.success()); + } catch (Exception e) { + status.put(key, ServiceResultUtil.toErrorStatus(key, e)); + } + } + return new ServiceResult<>(result, status); + } + +} diff --git a/crowd/src/test/java/com/deftdevs/bootstrapi/crowd/rest/_AllResourceTest.java b/crowd/src/test/java/com/deftdevs/bootstrapi/crowd/rest/_AllResourceTest.java new file mode 100644 index 00000000..826e0d3a --- /dev/null +++ b/crowd/src/test/java/com/deftdevs/bootstrapi/crowd/rest/_AllResourceTest.java @@ -0,0 +1,93 @@ +package com.deftdevs.bootstrapi.crowd.rest; + +import com.deftdevs.bootstrapi.commons.model.SettingsGeneralModel; +import com.deftdevs.bootstrapi.crowd.model.ApplicationModel; +import com.deftdevs.bootstrapi.crowd.model.SettingsModel; +import com.deftdevs.bootstrapi.crowd.model._AllModel; +import com.deftdevs.bootstrapi.commons.model.type._AllModelStatus; +import com.deftdevs.bootstrapi.commons.service.api._AllService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import javax.ws.rs.core.Response; +import java.util.HashMap; +import java.util.Map; + +import static com.deftdevs.bootstrapi.commons.constants.BootstrAPI.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +public class _AllResourceTest { + + @Mock + private _AllService<_AllModel> allService; + + private _AllResourceImpl allResource; + + private _AllModel allModel; + + @BeforeEach + public void setup() { + allResource = new _AllResourceImpl(allService); + + allModel = new _AllModel(); + final SettingsModel settings = new SettingsModel(); + settings.setGeneral(SettingsGeneralModel.EXAMPLE_1); + allModel.setSettings(settings); + + final Map applications = new HashMap<>(); + applications.put(ApplicationModel.EXAMPLE_1.getName(), ApplicationModel.EXAMPLE_1); + allModel.setApplications(applications); + + final Map status = new HashMap<>(); + status.put(SETTINGS + "/" + SETTINGS_GENERAL, _AllModelStatus.success()); + status.put(DIRECTORIES, _AllModelStatus.success()); + status.put(APPLICATIONS, _AllModelStatus.success()); + allModel.setStatus(status); + } + + @Test + public void testSetAll() { + doReturn(allModel).when(allService).setAll(any()); + + final Response response = allResource.setAll(allModel); + assertEquals(200, response.getStatus()); + + final _AllModel responseModel = (_AllModel) response.getEntity(); + assertEquals(allModel, responseModel); + + verify(allService).setAll(allModel); + } + + @Test + public void testSetAllReturns500WhenAnySubFieldFailedWithServerError() { + final _AllModel result = new _AllModel(); + final Map status = new HashMap<>(); + status.put(SETTINGS + "/" + SETTINGS_GENERAL, _AllModelStatus.success()); + status.put(DIRECTORIES, + _AllModelStatus.error(Response.Status.INTERNAL_SERVER_ERROR, "boom", null)); + result.setStatus(status); + doReturn(result).when(allService).setAll(any()); + + final Response response = allResource.setAll(allModel); + assertEquals(500, response.getStatus()); + } + + @Test + public void testSetAllReturns207OnMixedSuccessAndClientError() { + final _AllModel result = new _AllModel(); + final Map status = new HashMap<>(); + status.put(SETTINGS + "/" + SETTINGS_GENERAL, _AllModelStatus.success()); + status.put(DIRECTORIES, + _AllModelStatus.error(Response.Status.BAD_REQUEST, "bad", null)); + result.setStatus(status); + doReturn(result).when(allService).setAll(any()); + + final Response response = allResource.setAll(allModel); + assertEquals(207, response.getStatus()); + } +} diff --git a/crowd/src/test/java/com/deftdevs/bootstrapi/crowd/service/CrowdSettingsServiceTest.java b/crowd/src/test/java/com/deftdevs/bootstrapi/crowd/service/CrowdSettingsServiceTest.java new file mode 100644 index 00000000..85e932e4 --- /dev/null +++ b/crowd/src/test/java/com/deftdevs/bootstrapi/crowd/service/CrowdSettingsServiceTest.java @@ -0,0 +1,92 @@ +package com.deftdevs.bootstrapi.crowd.service; + +import com.deftdevs.bootstrapi.commons.exception.web.BadRequestException; +import com.deftdevs.bootstrapi.commons.model.SettingsGeneralModel; +import com.deftdevs.bootstrapi.commons.model.type.ServiceResult; +import com.deftdevs.bootstrapi.crowd.model.SettingsBrandingLoginPageModel; +import com.deftdevs.bootstrapi.crowd.model.SettingsModel; +import com.deftdevs.bootstrapi.crowd.service.api.CrowdSettingsBrandingService; +import com.deftdevs.bootstrapi.crowd.service.api.CrowdSettingsGeneralService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import javax.ws.rs.core.Response; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; + +@ExtendWith(MockitoExtension.class) +class CrowdSettingsServiceTest { + + @Mock + private CrowdSettingsGeneralService settingsGeneralService; + + @Mock + private CrowdSettingsBrandingService settingsBrandingService; + + private CrowdSettingsServiceImpl crowdSettingsService; + + @BeforeEach + void setup() { + crowdSettingsService = new CrowdSettingsServiceImpl(settingsGeneralService, settingsBrandingService); + } + + @Test + void testGetSettings() { + doReturn(SettingsGeneralModel.EXAMPLE_1).when(settingsGeneralService).getSettingsGeneral(); + doReturn(SettingsBrandingLoginPageModel.EXAMPLE_1).when(settingsBrandingService).getLoginPage(); + + final SettingsModel settingsModel = crowdSettingsService.getSettings(); + + assertEquals(SettingsGeneralModel.EXAMPLE_1, settingsModel.getGeneral()); + assertEquals(SettingsBrandingLoginPageModel.EXAMPLE_1, settingsModel.getBranding()); + } + + @Test + void testSetSettingsAppliesAllSubFields() { + doReturn(SettingsGeneralModel.EXAMPLE_1).when(settingsGeneralService).setSettingsGeneral(SettingsGeneralModel.EXAMPLE_1); + doReturn(SettingsBrandingLoginPageModel.EXAMPLE_1).when(settingsBrandingService).setLoginPage(SettingsBrandingLoginPageModel.EXAMPLE_1); + + final ServiceResult result = crowdSettingsService.setSettings(new SettingsModel( + SettingsGeneralModel.EXAMPLE_1, + SettingsBrandingLoginPageModel.EXAMPLE_1)); + + assertEquals(SettingsGeneralModel.EXAMPLE_1, result.getModel().getGeneral()); + assertEquals(SettingsBrandingLoginPageModel.EXAMPLE_1, result.getModel().getBranding()); + assertEquals(Response.Status.OK.getStatusCode(), result.getStatus().get("settings/general").getStatus()); + assertEquals(Response.Status.OK.getStatusCode(), result.getStatus().get("settings/branding").getStatus()); + } + + @Test + void testSetSettingsSkipsNullSubFields() { + final ServiceResult result = crowdSettingsService.setSettings(new SettingsModel()); + + assertTrue(result.getStatus().isEmpty()); + verify(settingsGeneralService, never()).setSettingsGeneral(SettingsGeneralModel.EXAMPLE_1); + verify(settingsBrandingService, never()).setLoginPage(SettingsBrandingLoginPageModel.EXAMPLE_1); + } + + @Test + void testSetSettingsRecordsPerSubFieldFailure() { + doReturn(SettingsGeneralModel.EXAMPLE_1).when(settingsGeneralService).setSettingsGeneral(SettingsGeneralModel.EXAMPLE_1); + doThrow(new BadRequestException("invalid login page")) + .when(settingsBrandingService).setLoginPage(SettingsBrandingLoginPageModel.EXAMPLE_1); + + final ServiceResult result = crowdSettingsService.setSettings(new SettingsModel( + SettingsGeneralModel.EXAMPLE_1, + SettingsBrandingLoginPageModel.EXAMPLE_1)); + + assertEquals(SettingsGeneralModel.EXAMPLE_1, result.getModel().getGeneral()); + assertNull(result.getModel().getBranding()); + assertEquals(Response.Status.OK.getStatusCode(), result.getStatus().get("settings/general").getStatus()); + assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), result.getStatus().get("settings/branding").getStatus()); + } +} diff --git a/crowd/src/test/java/com/deftdevs/bootstrapi/crowd/service/DirectoriesServiceTest.java b/crowd/src/test/java/com/deftdevs/bootstrapi/crowd/service/DirectoriesServiceTest.java index 4fcf66ca..9f89f027 100644 --- a/crowd/src/test/java/com/deftdevs/bootstrapi/crowd/service/DirectoriesServiceTest.java +++ b/crowd/src/test/java/com/deftdevs/bootstrapi/crowd/service/DirectoriesServiceTest.java @@ -139,7 +139,6 @@ public void testSetDirectoriesAddNewUnsupportedType() { final AbstractDirectoryModel directoryModel = DirectoryModelUtil.toDirectoryModel(directoryAzureAd); final Map directoryModels = Collections.singletonMap(directoryModel.getName(), directoryModel); - final boolean testConnection = false; assertThrows(BadRequestException.class, () -> { spy.setDirectories(directoryModels); diff --git a/crowd/src/test/java/com/deftdevs/bootstrapi/crowd/service/SettingsServiceTest.java b/crowd/src/test/java/com/deftdevs/bootstrapi/crowd/service/SettingsServiceTest.java index c146d736..c1af5d19 100644 --- a/crowd/src/test/java/com/deftdevs/bootstrapi/crowd/service/SettingsServiceTest.java +++ b/crowd/src/test/java/com/deftdevs/bootstrapi/crowd/service/SettingsServiceTest.java @@ -3,7 +3,7 @@ import com.atlassian.crowd.manager.property.PropertyManager; import com.atlassian.crowd.manager.property.PropertyManagerException; import com.deftdevs.bootstrapi.commons.exception.web.InternalServerErrorException; -import com.deftdevs.bootstrapi.commons.model.SettingsModel; +import com.deftdevs.bootstrapi.commons.model.SettingsGeneralModel; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -12,7 +12,7 @@ import java.net.URISyntaxException; -import static com.deftdevs.bootstrapi.commons.model.SettingsModel.EXAMPLE_1_NO_MODE; +import static com.deftdevs.bootstrapi.commons.model.SettingsGeneralModel.EXAMPLE_1_NO_MODE; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.Mockito.*; @@ -23,7 +23,7 @@ public class SettingsServiceTest { @Mock private PropertyManager propertyManager; - public static final SettingsModel SETTINGS_MODEL = EXAMPLE_1_NO_MODE; + public static final SettingsGeneralModel SETTINGS_MODEL = EXAMPLE_1_NO_MODE; private SettingsServiceImpl settingsService; diff --git a/crowd/src/test/java/com/deftdevs/bootstrapi/crowd/service/_AllServiceImplTest.java b/crowd/src/test/java/com/deftdevs/bootstrapi/crowd/service/_AllServiceImplTest.java new file mode 100644 index 00000000..2e09f792 --- /dev/null +++ b/crowd/src/test/java/com/deftdevs/bootstrapi/crowd/service/_AllServiceImplTest.java @@ -0,0 +1,194 @@ +package com.deftdevs.bootstrapi.crowd.service; + +import com.deftdevs.bootstrapi.commons.model.AbstractDirectoryModel; +import com.deftdevs.bootstrapi.commons.model.ApplicationLinkModel; +import com.deftdevs.bootstrapi.commons.model.LicenseModel; +import com.deftdevs.bootstrapi.commons.model.MailServerModel; +import com.deftdevs.bootstrapi.commons.model.MailServerSmtpModel; +import com.deftdevs.bootstrapi.commons.model.SettingsGeneralModel; +import com.deftdevs.bootstrapi.commons.model.type.ServiceResult; +import com.deftdevs.bootstrapi.commons.model.type._AllModelStatus; +import com.deftdevs.bootstrapi.commons.service.api.ApplicationLinksService; +import com.deftdevs.bootstrapi.commons.service.api.DirectoriesService; +import com.deftdevs.bootstrapi.commons.service.api.LicensesService; +import com.deftdevs.bootstrapi.commons.service.api.MailServerService; +import com.deftdevs.bootstrapi.crowd.model.ApplicationModel; +import com.deftdevs.bootstrapi.crowd.model.MailTemplatesModel; +import com.deftdevs.bootstrapi.crowd.model.SessionConfigModel; +import com.deftdevs.bootstrapi.crowd.model.SettingsModel; +import com.deftdevs.bootstrapi.crowd.model._AllModel; +import com.deftdevs.bootstrapi.crowd.service.api.ApplicationsService; +import com.deftdevs.bootstrapi.crowd.service.api.CrowdSettingsService; +import com.deftdevs.bootstrapi.crowd.service.api.MailTemplatesService; +import com.deftdevs.bootstrapi.crowd.service.api.SessionConfigService; +import com.deftdevs.bootstrapi.crowd.service.api.TrustedProxiesService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.Response; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verifyNoInteractions; + +@ExtendWith(MockitoExtension.class) +class _AllServiceImplTest { + + @Mock + private CrowdSettingsService settingsService; + + @Mock + private DirectoriesService directoriesService; + + @Mock + private ApplicationsService applicationsService; + + @Mock + private ApplicationLinksService applicationLinksService; + + @Mock + private LicensesService licensesService; + + @Mock + private MailServerService mailServerService; + + @Mock + private MailTemplatesService mailTemplatesService; + + @Mock + private SessionConfigService sessionConfigService; + + @Mock + private TrustedProxiesService trustedProxiesService; + + private _AllServiceImpl allService; + + @BeforeEach + void setup() { + allService = new _AllServiceImpl( + settingsService, + directoriesService, + applicationsService, + applicationLinksService, + licensesService, + mailServerService, + mailTemplatesService, + sessionConfigService, + trustedProxiesService); + } + + @Test + void testSetAllEmptyModelYieldsEmptyStatus() { + final _AllModel result = allService.setAll(new _AllModel()); + + assertTrue(result.getStatus().isEmpty()); + verifyNoInteractions(settingsService, directoriesService, applicationsService, + applicationLinksService, licensesService, mailServerService, + mailTemplatesService, sessionConfigService, trustedProxiesService); + } + + @Test + void testSetAllAppliesAllFields() { + final SettingsModel settings = new SettingsModel(); + settings.setGeneral(SettingsGeneralModel.EXAMPLE_1); + final Map directories = + Collections.singletonMap("directory", mock(AbstractDirectoryModel.class)); + final Map applications = + Collections.singletonMap("application", new ApplicationModel()); + final Map applicationLinks = + Collections.singletonMap("link", ApplicationLinkModel.EXAMPLE_1); + final Map licenses = + Collections.singletonMap("licenseKey", LicenseModel.EXAMPLE_1); + final Map redactedLicenses = + Collections.singletonMap("lice...nse1#abcd", LicenseModel.EXAMPLE_1); + final MailServerModel mailServer = new MailServerModel(MailServerSmtpModel.EXAMPLE_1, null); + final MailTemplatesModel mailTemplates = MailTemplatesModel.EXAMPLE_1; + final SessionConfigModel sessionConfig = new SessionConfigModel(); + final List trustedProxies = Collections.singletonList("192.168.0.1"); + + doReturn(new ServiceResult<>(settings, + Collections.singletonMap("settings/general", _AllModelStatus.success()))) + .when(settingsService).setSettings(settings); + doReturn(directories).when(directoriesService).setDirectories(directories); + doReturn(applications).when(applicationsService).setApplications(applications); + doReturn(applicationLinks).when(applicationLinksService).setApplicationLinks(applicationLinks); + doReturn(redactedLicenses).when(licensesService).setLicenses(licenses); + doReturn(new ServiceResult<>(mailServer, + Collections.singletonMap("mail-server/smtp", _AllModelStatus.success()))) + .when(mailServerService).setMailServer(mailServer); + doReturn(mailTemplates).when(mailTemplatesService).setMailTemplates(mailTemplates); + doReturn(sessionConfig).when(sessionConfigService).setSessionConfig(sessionConfig); + doReturn(trustedProxies).when(trustedProxiesService).setTrustedProxies(trustedProxies); + + final _AllModel allModel = new _AllModel(); + allModel.setSettings(settings); + allModel.setDirectories(directories); + allModel.setApplications(applications); + allModel.setApplicationLinks(applicationLinks); + allModel.setLicenses(licenses); + allModel.setMailServer(mailServer); + allModel.setMailTemplates(mailTemplates); + allModel.setSessionConfig(sessionConfig); + allModel.setTrustedProxies(trustedProxies); + + final _AllModel result = allService.setAll(allModel); + + assertEquals(settings, result.getSettings()); + assertEquals(directories, result.getDirectories()); + assertEquals(applications, result.getApplications()); + assertEquals(applicationLinks, result.getApplicationLinks()); + assertEquals(redactedLicenses, result.getLicenses()); + assertEquals(mailServer, result.getMailServer()); + assertEquals(mailTemplates, result.getMailTemplates()); + assertEquals(sessionConfig, result.getSessionConfig()); + assertEquals(trustedProxies, result.getTrustedProxies()); + + final Map status = result.getStatus(); + assertEquals(9, status.size()); + assertEquals(Response.Status.OK.getStatusCode(), status.get("settings/general").getStatus()); + assertEquals(Response.Status.OK.getStatusCode(), status.get("directories").getStatus()); + assertEquals(Response.Status.OK.getStatusCode(), status.get("applications").getStatus()); + assertEquals(Response.Status.OK.getStatusCode(), status.get("application-links").getStatus()); + assertEquals(Response.Status.OK.getStatusCode(), status.get("licenses").getStatus()); + assertEquals(Response.Status.OK.getStatusCode(), status.get("mail-server/smtp").getStatus()); + assertEquals(Response.Status.OK.getStatusCode(), status.get("mail-templates").getStatus()); + assertEquals(Response.Status.OK.getStatusCode(), status.get("session-config").getStatus()); + assertEquals(Response.Status.OK.getStatusCode(), status.get("trusted-proxies").getStatus()); + } + + @Test + void testSetAllRecordsFailureAndContinuesWithOtherFields() { + final SettingsModel settings = new SettingsModel(); + settings.setGeneral(SettingsGeneralModel.EXAMPLE_1); + final Map licenses = + Collections.singletonMap("licenseKey", LicenseModel.EXAMPLE_1); + + doReturn(new ServiceResult<>(settings, + Collections.singletonMap("settings/general", _AllModelStatus.success()))) + .when(settingsService).setSettings(settings); + doThrow(new WebApplicationException(Response.Status.CONFLICT)) + .when(licensesService).setLicenses(licenses); + + final _AllModel allModel = new _AllModel(); + allModel.setSettings(settings); + allModel.setLicenses(licenses); + + final _AllModel result = allService.setAll(allModel); + + assertEquals(settings, result.getSettings()); + assertNull(result.getLicenses()); + assertEquals(Response.Status.OK.getStatusCode(), result.getStatus().get("settings/general").getStatus()); + assertEquals(Response.Status.CONFLICT.getStatusCode(), result.getStatus().get("licenses").getStatus()); + } +} diff --git a/crowd/src/test/java/it/com/deftdevs/bootstrapi/crowd/rest/FuncTestExamples.java b/crowd/src/test/java/it/com/deftdevs/bootstrapi/crowd/rest/FuncTestExamples.java new file mode 100644 index 00000000..78543077 --- /dev/null +++ b/crowd/src/test/java/it/com/deftdevs/bootstrapi/crowd/rest/FuncTestExamples.java @@ -0,0 +1,31 @@ +package it.com.deftdevs.bootstrapi.crowd.rest; + +import com.deftdevs.bootstrapi.commons.model.MailServerSmtpModel; +import com.deftdevs.bootstrapi.commons.model.SettingsGeneralModel; + +/** + * Example entities for functional tests. Unlike the {@code EXAMPLE_*} constants on the + * models (which document the API), these values describe the integration test + * environment — the mail server points at the greenmail service started for + * integration test runs (see {@code ci.yaml}), and the general settings are limited + * to the fields supported by Crowd. + */ +public final class FuncTestExamples { + + public static final SettingsGeneralModel SETTINGS_GENERAL = SettingsGeneralModel.builder() + .baseUrl(SettingsGeneralModel.EXAMPLE_1.getBaseUrl()) + .title(SettingsGeneralModel.EXAMPLE_1.getTitle()) + .build(); + + public static final MailServerSmtpModel MAIL_SERVER_SMTP_GREENMAIL = MailServerSmtpModel.builder() + .from("test@example.com") + .prefix("[Test]") + .host("localhost") + .port(3025) + .useTls(false) + .timeout(5000L) + .build(); + + private FuncTestExamples() { + } +} diff --git a/crowd/src/test/java/it/com/deftdevs/bootstrapi/crowd/rest/SettingsGeneralResourceFuncTest.java b/crowd/src/test/java/it/com/deftdevs/bootstrapi/crowd/rest/SettingsGeneralResourceFuncTest.java new file mode 100644 index 00000000..622bb6d1 --- /dev/null +++ b/crowd/src/test/java/it/com/deftdevs/bootstrapi/crowd/rest/SettingsGeneralResourceFuncTest.java @@ -0,0 +1,13 @@ +package it.com.deftdevs.bootstrapi.crowd.rest; + +import com.deftdevs.bootstrapi.commons.model.SettingsGeneralModel; +import it.com.deftdevs.bootstrapi.commons.rest.AbstractSettingsGeneralResourceFuncTest; + +public class SettingsGeneralResourceFuncTest extends AbstractSettingsGeneralResourceFuncTest { + + @Override + protected SettingsGeneralModel getExampleModel() { + return FuncTestExamples.SETTINGS_GENERAL; + } + +} diff --git a/crowd/src/test/java/it/com/deftdevs/bootstrapi/crowd/rest/SettingsResourceFuncTest.java b/crowd/src/test/java/it/com/deftdevs/bootstrapi/crowd/rest/SettingsResourceFuncTest.java deleted file mode 100644 index ca31a892..00000000 --- a/crowd/src/test/java/it/com/deftdevs/bootstrapi/crowd/rest/SettingsResourceFuncTest.java +++ /dev/null @@ -1,16 +0,0 @@ -package it.com.deftdevs.bootstrapi.crowd.rest; - -import com.deftdevs.bootstrapi.commons.model.SettingsModel; -import it.com.deftdevs.bootstrapi.commons.rest.AbstractSettingsResourceFuncTest; - -public class SettingsResourceFuncTest extends AbstractSettingsResourceFuncTest { - - @Override - protected SettingsModel getExampleModel() { - return SettingsModel.builder() - .baseUrl(SettingsModel.EXAMPLE_1.getBaseUrl()) - .title(SettingsModel.EXAMPLE_1.getTitle()) - .build(); - } - -} diff --git a/crowd/src/test/java/it/com/deftdevs/bootstrapi/crowd/rest/_AllResourceFuncTest.java b/crowd/src/test/java/it/com/deftdevs/bootstrapi/crowd/rest/_AllResourceFuncTest.java new file mode 100644 index 00000000..ffee15c4 --- /dev/null +++ b/crowd/src/test/java/it/com/deftdevs/bootstrapi/crowd/rest/_AllResourceFuncTest.java @@ -0,0 +1,55 @@ +package it.com.deftdevs.bootstrapi.crowd.rest; + +import com.deftdevs.bootstrapi.commons.constants.BootstrAPI; +import com.deftdevs.bootstrapi.commons.model.MailServerModel; +import com.deftdevs.bootstrapi.commons.model.MailServerSmtpModel; +import com.deftdevs.bootstrapi.commons.model.SettingsGeneralModel; +import com.deftdevs.bootstrapi.commons.util.ServiceResultUtil; +import com.deftdevs.bootstrapi.crowd.model.MailTemplatesModel; +import com.deftdevs.bootstrapi.crowd.model.SessionConfigModel; +import com.deftdevs.bootstrapi.crowd.model.SettingsModel; +import com.deftdevs.bootstrapi.crowd.model._AllModel; +import it.com.deftdevs.bootstrapi.commons.rest.Abstract_AllResourceFuncTest; +import org.junit.jupiter.api.Test; + +import java.util.Arrays; + +public class _AllResourceFuncTest extends Abstract_AllResourceFuncTest { + + @Override + protected Object getExampleAllModel() { + return _AllModel.builder() + .settings(SettingsModel.builder() + .general(getExampleSettingsGeneralModel()) + .build()) + .build(); + } + + @Override + protected SettingsGeneralModel getExampleSettingsGeneralModel() { + return FuncTestExamples.SETTINGS_GENERAL; + } + + @Test + void testSetAllAppliesMultipleEntities() throws Exception { + // no POP mail server here: setting POP is unsupported by Crowd + final _AllModel allModel = _AllModel.builder() + .settings(SettingsModel.builder() + .general(getExampleSettingsGeneralModel()) + .build()) + .mailServer(MailServerModel.builder() + .smtp(FuncTestExamples.MAIL_SERVER_SMTP_GREENMAIL) + .build()) + .mailTemplates(MailTemplatesModel.EXAMPLE_1) + .sessionConfig(SessionConfigModel.EXAMPLE_2) + .trustedProxies(Arrays.asList("10.0.0.1", "10.0.0.2")) + .build(); + + assertSetAllApplied(allModel, Arrays.asList( + ServiceResultUtil.subEntityKey(SettingsGeneralModel.class), + ServiceResultUtil.subEntityKey(MailServerSmtpModel.class), + ServiceResultUtil.entityType(MailTemplatesModel.class), + ServiceResultUtil.entityType(SessionConfigModel.class), + BootstrAPI.TRUSTED_PROXIES)); + } +} diff --git a/jira/Apis/AllApi.md b/jira/Apis/AllApi.md new file mode 100644 index 00000000..1c17cae5 --- /dev/null +++ b/jira/Apis/AllApi.md @@ -0,0 +1,36 @@ +# AllApi + +All URIs are relative to *https://JIRA_URL/rest/bootstrapi/1* + +| Method | HTTP request | Description | +|------------- | ------------- | -------------| +| [**setAll**](AllApi.md#setAll) | **PUT** / | Apply a complete configuration | + + + +# **setAll** +> _AllModel setAll(\_AllModel) + +Apply a complete configuration + + Returns the updated configuration. The per-sub-field outcome is reported in the 'status' map (2xx for success, 4xx/5xx for failure with a human-readable 'message' and optional 'details'). License keys in the response are redacted (e.g. 'AAAB...wxyz#a1b2'). + +### Parameters + +|Name | Type | Description | Notes | +|------------- | ------------- | ------------- | -------------| +| **\_AllModel** | [**_AllModel**](../Models/_AllModel.md)| | | + +### Return type + +[**_AllModel**](../Models/_AllModel.md) + +### Authorization + +[basicAuth](../README.md#basicAuth) + +### HTTP request headers + +- **Content-Type**: application/json +- **Accept**: application/json + diff --git a/jira/Apis/SettingsApi.md b/jira/Apis/SettingsApi.md index 5f33d6c1..adc60745 100644 --- a/jira/Apis/SettingsApi.md +++ b/jira/Apis/SettingsApi.md @@ -36,7 +36,7 @@ This endpoint does not need any parameter. # **getSettings** -> SettingsModel getSettings() +> SettingsGeneralModel getSettings() Get the general settings @@ -45,7 +45,7 @@ This endpoint does not need any parameter. ### Return type -[**SettingsModel**](../Models/SettingsModel.md) +[**SettingsGeneralModel**](../Models/SettingsGeneralModel.md) ### Authorization @@ -105,7 +105,7 @@ Set the banner # **setSettings** -> SettingsModel setSettings(SettingsModel) +> SettingsGeneralModel setSettings(SettingsGeneralModel) Set the general settings @@ -113,11 +113,11 @@ Set the general settings |Name | Type | Description | Notes | |------------- | ------------- | ------------- | -------------| -| **SettingsModel** | [**SettingsModel**](../Models/SettingsModel.md)| | [optional] | +| **SettingsGeneralModel** | [**SettingsGeneralModel**](../Models/SettingsGeneralModel.md)| | [optional] | ### Return type -[**SettingsModel**](../Models/SettingsModel.md) +[**SettingsGeneralModel**](../Models/SettingsGeneralModel.md) ### Authorization diff --git a/jira/Models/AuthenticationModel.md b/jira/Models/AuthenticationModel.md new file mode 100644 index 00000000..b873bdc6 --- /dev/null +++ b/jira/Models/AuthenticationModel.md @@ -0,0 +1,10 @@ +# AuthenticationModel +## Properties + +| Name | Type | Description | Notes | +|------------ | ------------- | ------------- | -------------| +| **idps** | [**Map**](AbstractAuthenticationIdpModel.md) | | [optional] [default to null] | +| **sso** | [**AuthenticationSsoModel**](AuthenticationSsoModel.md) | | [optional] [default to null] | + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + diff --git a/jira/Models/MailServerModel.md b/jira/Models/MailServerModel.md new file mode 100644 index 00000000..34c39bf7 --- /dev/null +++ b/jira/Models/MailServerModel.md @@ -0,0 +1,10 @@ +# MailServerModel +## Properties + +| Name | Type | Description | Notes | +|------------ | ------------- | ------------- | -------------| +| **smtp** | [**MailServerSmtpModel**](MailServerSmtpModel.md) | | [optional] [default to null] | +| **pop** | [**MailServerPopModel**](MailServerPopModel.md) | | [optional] [default to null] | + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + diff --git a/jira/Models/SettingsGeneralModel.md b/jira/Models/SettingsGeneralModel.md new file mode 100644 index 00000000..8ebe677b --- /dev/null +++ b/jira/Models/SettingsGeneralModel.md @@ -0,0 +1,13 @@ +# SettingsGeneralModel +## Properties + +| Name | Type | Description | Notes | +|------------ | ------------- | ------------- | -------------| +| **baseUrl** | **URI** | | [optional] [default to null] | +| **mode** | **String** | | [optional] [default to null] | +| **title** | **String** | | [optional] [default to null] | +| **contactMessage** | **String** | | [optional] [default to null] | +| **externalUserManagement** | **Boolean** | | [optional] [default to null] | + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + diff --git a/jira/Models/SettingsModel.md b/jira/Models/SettingsModel.md index f4eb0e7f..23d5cc40 100644 --- a/jira/Models/SettingsModel.md +++ b/jira/Models/SettingsModel.md @@ -3,11 +3,9 @@ | Name | Type | Description | Notes | |------------ | ------------- | ------------- | -------------| -| **baseUrl** | **URI** | | [optional] [default to null] | -| **mode** | **String** | | [optional] [default to null] | -| **title** | **String** | | [optional] [default to null] | -| **contactMessage** | **String** | | [optional] [default to null] | -| **externalUserManagement** | **Boolean** | | [optional] [default to null] | +| **general** | [**SettingsGeneralModel**](SettingsGeneralModel.md) | | [optional] [default to null] | +| **security** | [**SettingsSecurityModel**](SettingsSecurityModel.md) | | [optional] [default to null] | +| **banner** | [**SettingsBannerModel**](SettingsBannerModel.md) | | [optional] [default to null] | [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) diff --git a/jira/Models/_AllModel.md b/jira/Models/_AllModel.md new file mode 100644 index 00000000..3a2e4dee --- /dev/null +++ b/jira/Models/_AllModel.md @@ -0,0 +1,16 @@ +# _AllModel +## Properties + +| Name | Type | Description | Notes | +|------------ | ------------- | ------------- | -------------| +| **settings** | [**SettingsModel**](SettingsModel.md) | | [optional] [default to null] | +| **directories** | [**Map**](AbstractDirectoryModel.md) | | [optional] [default to null] | +| **applicationLinks** | [**Map**](ApplicationLinkModel.md) | | [optional] [default to null] | +| **authentication** | [**AuthenticationModel**](AuthenticationModel.md) | | [optional] [default to null] | +| **licenses** | [**Map**](LicenseModel.md) | | [optional] [default to null] | +| **mailServer** | [**MailServerModel**](MailServerModel.md) | | [optional] [default to null] | +| **permissionsGlobal** | [**PermissionsGlobalModel**](PermissionsGlobalModel.md) | | [optional] [default to null] | +| **status** | [**Map**](_AllModelStatus.md) | | [optional] [default to null] | + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + diff --git a/jira/Models/_AllModelStatus.md b/jira/Models/_AllModelStatus.md new file mode 100644 index 00000000..ed699765 --- /dev/null +++ b/jira/Models/_AllModelStatus.md @@ -0,0 +1,11 @@ +# _AllModelStatus +## Properties + +| Name | Type | Description | Notes | +|------------ | ------------- | ------------- | -------------| +| **status** | **Integer** | | [optional] [default to null] | +| **message** | **String** | | [optional] [default to null] | +| **details** | **String** | | [optional] [default to null] | + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + diff --git a/jira/README.md b/jira/README.md index 464f61c3..1680c658 100644 --- a/jira/README.md +++ b/jira/README.md @@ -7,6 +7,7 @@ All URIs are relative to *https://JIRA_URL/rest/bootstrapi/1* | Class | Method | HTTP request | Description | |------------ | ------------- | ------------- | -------------| +| *AllApi* | [**setAll**](Apis/AllApi.md#setAll) | **PUT** / | Apply a complete configuration | | *ApplicationLinkApi* | [**createApplicationLink**](Apis/ApplicationLinkApi.md#createApplicationLink) | **POST** /application-link | Create an application link | *ApplicationLinkApi* | [**deleteApplicationLink**](Apis/ApplicationLinkApi.md#deleteApplicationLink) | **DELETE** /application-link/{uuid} | Delete an application link | *ApplicationLinkApi* | [**getApplicationLink**](Apis/ApplicationLinkApi.md#getApplicationLink) | **GET** /application-link/{uuid} | Get an application link | @@ -53,6 +54,7 @@ All URIs are relative to *https://JIRA_URL/rest/bootstrapi/1* - [ApplicationLinkModel](./Models/ApplicationLinkModel.md) - [AuthenticationIdpOidcModel](./Models/AuthenticationIdpOidcModel.md) - [AuthenticationIdpSamlModel](./Models/AuthenticationIdpSamlModel.md) + - [AuthenticationModel](./Models/AuthenticationModel.md) - [AuthenticationSsoModel](./Models/AuthenticationSsoModel.md) - [DirectoryCrowdAdvanced](./Models/DirectoryCrowdAdvanced.md) - [DirectoryCrowdModel](./Models/DirectoryCrowdModel.md) @@ -74,13 +76,17 @@ All URIs are relative to *https://JIRA_URL/rest/bootstrapi/1* - [ErrorCollection](./Models/ErrorCollection.md) - [GroupModel](./Models/GroupModel.md) - [LicenseModel](./Models/LicenseModel.md) + - [MailServerModel](./Models/MailServerModel.md) - [MailServerPopModel](./Models/MailServerPopModel.md) - [MailServerSmtpModel](./Models/MailServerSmtpModel.md) - [PermissionsGlobalModel](./Models/PermissionsGlobalModel.md) - [SettingsBannerModel](./Models/SettingsBannerModel.md) + - [SettingsGeneralModel](./Models/SettingsGeneralModel.md) - [SettingsModel](./Models/SettingsModel.md) - [SettingsSecurityModel](./Models/SettingsSecurityModel.md) - [UserModel](./Models/UserModel.md) + - [_AllModel](./Models/_AllModel.md) + - [_AllModelStatus](./Models/_AllModelStatus.md) diff --git a/jira/src/main/java/com/deftdevs/bootstrapi/jira/config/ServiceConfig.java b/jira/src/main/java/com/deftdevs/bootstrapi/jira/config/ServiceConfig.java index bc0b1f91..0671d8c7 100644 --- a/jira/src/main/java/com/deftdevs/bootstrapi/jira/config/ServiceConfig.java +++ b/jira/src/main/java/com/deftdevs/bootstrapi/jira/config/ServiceConfig.java @@ -1,6 +1,7 @@ package com.deftdevs.bootstrapi.jira.config; import com.deftdevs.bootstrapi.commons.service.api.*; +import com.deftdevs.bootstrapi.jira.model._AllModel; import com.deftdevs.bootstrapi.jira.service.*; import com.deftdevs.bootstrapi.jira.service.api.JiraAuthenticationService; import com.deftdevs.bootstrapi.jira.service.api.JiraSettingsService; @@ -17,6 +18,18 @@ public class ServiceConfig { @Autowired private HelperConfig helperConfig; + @Bean + public _AllService<_AllModel> _allService() { + return new _AllServiceImpl( + jiraSettingsService(), + directoriesService(), + applicationLinksService(), + jiraAuthenticationService(), + licensesService(), + mailServerService(), + permissionsService()); + } + @Bean public ApplicationLinksService applicationLinksService() { return new ApplicationLinksServiceImpl( diff --git a/jira/src/main/java/com/deftdevs/bootstrapi/jira/model/SettingsModel.java b/jira/src/main/java/com/deftdevs/bootstrapi/jira/model/SettingsModel.java new file mode 100644 index 00000000..18cb985a --- /dev/null +++ b/jira/src/main/java/com/deftdevs/bootstrapi/jira/model/SettingsModel.java @@ -0,0 +1,31 @@ +package com.deftdevs.bootstrapi.jira.model; + +import com.deftdevs.bootstrapi.commons.model.SettingsGeneralModel; +import com.deftdevs.bootstrapi.commons.model.SettingsSecurityModel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; + +import static com.deftdevs.bootstrapi.commons.constants.BootstrAPI.SETTINGS; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@XmlRootElement(name = SETTINGS) +public class SettingsModel { + + @XmlElement + private SettingsGeneralModel general; + + @XmlElement + private SettingsSecurityModel security; + + @XmlElement + private SettingsBannerModel banner; + +} diff --git a/jira/src/main/java/com/deftdevs/bootstrapi/jira/model/_AllModel.java b/jira/src/main/java/com/deftdevs/bootstrapi/jira/model/_AllModel.java new file mode 100644 index 00000000..eb49dfba --- /dev/null +++ b/jira/src/main/java/com/deftdevs/bootstrapi/jira/model/_AllModel.java @@ -0,0 +1,52 @@ +package com.deftdevs.bootstrapi.jira.model; + +import com.deftdevs.bootstrapi.commons.constants.BootstrAPI; +import com.deftdevs.bootstrapi.commons.model.AbstractDirectoryModel; +import com.deftdevs.bootstrapi.commons.model.ApplicationLinkModel; +import com.deftdevs.bootstrapi.commons.model.AuthenticationModel; +import com.deftdevs.bootstrapi.commons.model.LicenseModel; +import com.deftdevs.bootstrapi.commons.model.MailServerModel; +import com.deftdevs.bootstrapi.commons.model.PermissionsGlobalModel; +import com.deftdevs.bootstrapi.commons.model.type._AllModelAccessor; +import com.deftdevs.bootstrapi.commons.model.type._AllModelStatus; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; +import java.util.Map; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@XmlRootElement(name = BootstrAPI._ALL) +public class _AllModel implements _AllModelAccessor { + + @XmlElement + private SettingsModel settings; + + @XmlElement + private Map directories; + + @XmlElement + private Map applicationLinks; + + @XmlElement + private AuthenticationModel authentication; + + @XmlElement + private Map licenses; + + @XmlElement + private MailServerModel mailServer; + + @XmlElement + private PermissionsGlobalModel permissionsGlobal; + + @XmlElement + private Map status; + +} diff --git a/jira/src/main/java/com/deftdevs/bootstrapi/jira/rest/SettingsResourceImpl.java b/jira/src/main/java/com/deftdevs/bootstrapi/jira/rest/SettingsGeneralResourceImpl.java similarity index 69% rename from jira/src/main/java/com/deftdevs/bootstrapi/jira/rest/SettingsResourceImpl.java rename to jira/src/main/java/com/deftdevs/bootstrapi/jira/rest/SettingsGeneralResourceImpl.java index f8b5773b..0730eea6 100644 --- a/jira/src/main/java/com/deftdevs/bootstrapi/jira/rest/SettingsResourceImpl.java +++ b/jira/src/main/java/com/deftdevs/bootstrapi/jira/rest/SettingsGeneralResourceImpl.java @@ -2,8 +2,8 @@ import com.atlassian.plugins.rest.api.security.annotation.SystemAdminOnly; import com.deftdevs.bootstrapi.commons.constants.BootstrAPI; -import com.deftdevs.bootstrapi.commons.model.SettingsModel; -import com.deftdevs.bootstrapi.commons.rest.AbstractSettingsResourceImpl; +import com.deftdevs.bootstrapi.commons.model.SettingsGeneralModel; +import com.deftdevs.bootstrapi.commons.rest.AbstractSettingsGeneralResourceImpl; import com.deftdevs.bootstrapi.jira.service.api.JiraSettingsService; import io.swagger.v3.oas.annotations.tags.Tag; @@ -18,10 +18,10 @@ @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) @SystemAdminOnly -public class SettingsResourceImpl extends AbstractSettingsResourceImpl { +public class SettingsGeneralResourceImpl extends AbstractSettingsGeneralResourceImpl { @Inject - public SettingsResourceImpl( + public SettingsGeneralResourceImpl( final JiraSettingsService settingsService) { super(settingsService); diff --git a/jira/src/main/java/com/deftdevs/bootstrapi/jira/rest/_AllResourceImpl.java b/jira/src/main/java/com/deftdevs/bootstrapi/jira/rest/_AllResourceImpl.java new file mode 100644 index 00000000..b5ffe089 --- /dev/null +++ b/jira/src/main/java/com/deftdevs/bootstrapi/jira/rest/_AllResourceImpl.java @@ -0,0 +1,64 @@ +package com.deftdevs.bootstrapi.jira.rest; + +import com.atlassian.plugins.rest.api.security.annotation.SystemAdminOnly; +import com.deftdevs.bootstrapi.commons.constants.BootstrAPI; +import com.deftdevs.bootstrapi.commons.model.ErrorCollection; +import com.deftdevs.bootstrapi.commons.rest._AbstractAllResourceImpl; +import com.deftdevs.bootstrapi.commons.service.api._AllService; +import com.deftdevs.bootstrapi.jira.model._AllModel; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.tags.Tag; + +import javax.inject.Inject; +import javax.ws.rs.Consumes; +import javax.ws.rs.PUT; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; + +@Path(BootstrAPI._ROOT) +@Tag(name = BootstrAPI._ALL) +@Consumes(MediaType.APPLICATION_JSON) +@Produces(MediaType.APPLICATION_JSON) +@SystemAdminOnly +public class _AllResourceImpl extends _AbstractAllResourceImpl<_AllModel> { + + @Inject + public _AllResourceImpl( + final _AllService<_AllModel> allService) { + + super(allService); + } + + // overridden to document the concrete response model in the generated OpenAPI spec + @PUT + @Operation( + summary = BootstrAPI._ALL_PUT_SUMMARY, + description = BootstrAPI._ALL_PUT_RESPONSE_DESCRIPTION, + responses = { + @ApiResponse( + responseCode = "200", content = @Content(schema = @Schema(implementation = _AllModel.class)), + description = BootstrAPI._ALL_PUT_RESPONSE_DESCRIPTION + ), + @ApiResponse( + responseCode = "207", content = @Content(schema = @Schema(implementation = _AllModel.class)), + description = BootstrAPI._ALL_PUT_PARTIAL_RESPONSE_DESCRIPTION + ), + @ApiResponse( + responseCode = "default", content = @Content(schema = @Schema(implementation = ErrorCollection.class)), + description = BootstrAPI.ERROR_COLLECTION_RESPONSE_DESCRIPTION + ), + } + ) + @Override + public Response setAll( + final _AllModel allModel) { + + return super.setAll(allModel); + } + +} diff --git a/jira/src/main/java/com/deftdevs/bootstrapi/jira/service/LicensesServiceImpl.java b/jira/src/main/java/com/deftdevs/bootstrapi/jira/service/LicensesServiceImpl.java index bdf8badc..6f50abfb 100644 --- a/jira/src/main/java/com/deftdevs/bootstrapi/jira/service/LicensesServiceImpl.java +++ b/jira/src/main/java/com/deftdevs/bootstrapi/jira/service/LicensesServiceImpl.java @@ -3,9 +3,12 @@ import com.atlassian.jira.license.JiraLicenseManager; import com.deftdevs.bootstrapi.commons.model.LicenseModel; import com.deftdevs.bootstrapi.commons.service.api.LicensesService; +import com.deftdevs.bootstrapi.commons.util.LicenseKeyRedactor; import com.deftdevs.bootstrapi.jira.model.util.LicenseModelUtil; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; import java.util.stream.Collectors; import java.util.stream.StreamSupport; @@ -36,6 +39,17 @@ public List setLicenses( return getLicenses(); } + @Override + public Map setLicenses( + final Map licenseInputs) { + + final Map result = new LinkedHashMap<>(); + for (final String key : licenseInputs.keySet()) { + result.put(LicenseKeyRedactor.redact(key), addLicense(key)); + } + return result; + } + @Override public LicenseModel addLicense( final String license) { diff --git a/jira/src/main/java/com/deftdevs/bootstrapi/jira/service/SettingsServiceImpl.java b/jira/src/main/java/com/deftdevs/bootstrapi/jira/service/SettingsServiceImpl.java index 961a3437..dad34f63 100644 --- a/jira/src/main/java/com/deftdevs/bootstrapi/jira/service/SettingsServiceImpl.java +++ b/jira/src/main/java/com/deftdevs/bootstrapi/jira/service/SettingsServiceImpl.java @@ -3,7 +3,7 @@ import com.atlassian.jira.config.properties.APKeys; import com.atlassian.jira.config.properties.ApplicationProperties; import com.deftdevs.bootstrapi.commons.exception.web.BadRequestException; -import com.deftdevs.bootstrapi.commons.model.SettingsModel; +import com.deftdevs.bootstrapi.commons.model.SettingsGeneralModel; import com.deftdevs.bootstrapi.commons.model.SettingsSecurityModel; import com.deftdevs.bootstrapi.jira.model.SettingsBannerModel; import com.deftdevs.bootstrapi.jira.service.api.JiraSettingsService; @@ -28,14 +28,14 @@ public SettingsServiceImpl( } @Override - public SettingsModel getSettingsGeneral() { + public SettingsGeneralModel getSettingsGeneral() { final String baseUrl = applicationProperties.getString(JIRA_BASEURL); final String mode = applicationProperties.getString(JIRA_MODE); final String title = applicationProperties.getString(JIRA_TITLE); final String contactMessage = applicationProperties.getString(JIRA_CONTACT_ADMINISTRATORS_MESSSAGE); final Boolean externalUserManagement = Boolean.parseBoolean(applicationProperties.getString(JIRA_OPTION_USER_EXTERNALMGT)); - final SettingsModel settingsModel = SettingsModel.builder() + final SettingsGeneralModel settingsModel = SettingsGeneralModel.builder() .baseUrl(baseUrl != null ? URI.create(baseUrl) : null) .mode(mode) .title(title) @@ -47,8 +47,8 @@ public SettingsModel getSettingsGeneral() { } @Override - public SettingsModel setSettingsGeneral( - final SettingsModel settingsModel) { + public SettingsGeneralModel setSettingsGeneral( + final SettingsGeneralModel settingsModel) { if (settingsModel.getBaseUrl() != null) { applicationProperties.setString(JIRA_BASEURL, settingsModel.getBaseUrl().toString()); diff --git a/jira/src/main/java/com/deftdevs/bootstrapi/jira/service/_AllServiceImpl.java b/jira/src/main/java/com/deftdevs/bootstrapi/jira/service/_AllServiceImpl.java new file mode 100644 index 00000000..3b154105 --- /dev/null +++ b/jira/src/main/java/com/deftdevs/bootstrapi/jira/service/_AllServiceImpl.java @@ -0,0 +1,82 @@ +package com.deftdevs.bootstrapi.jira.service; + +import com.deftdevs.bootstrapi.commons.model.PermissionsGlobalModel; +import com.deftdevs.bootstrapi.commons.model.type._AllModelStatus; +import com.deftdevs.bootstrapi.commons.service._AbstractAllServiceImpl; +import com.deftdevs.bootstrapi.commons.service.api.ApplicationLinksService; +import com.deftdevs.bootstrapi.commons.service.api.DirectoriesService; +import com.deftdevs.bootstrapi.commons.service.api.LicensesService; +import com.deftdevs.bootstrapi.commons.service.api.MailServerService; +import com.deftdevs.bootstrapi.commons.service.api.PermissionsService; +import com.deftdevs.bootstrapi.commons.util.ServiceResultUtil; +import com.deftdevs.bootstrapi.jira.model._AllModel; +import com.deftdevs.bootstrapi.jira.service.api.JiraAuthenticationService; +import com.deftdevs.bootstrapi.jira.service.api.JiraSettingsService; + +import java.util.HashMap; +import java.util.Map; + +import static com.deftdevs.bootstrapi.commons.constants.BootstrAPI.APPLICATION_LINKS; +import static com.deftdevs.bootstrapi.commons.constants.BootstrAPI.DIRECTORIES; +import static com.deftdevs.bootstrapi.commons.constants.BootstrAPI.LICENSES; + +public class _AllServiceImpl extends _AbstractAllServiceImpl<_AllModel> { + + private final JiraSettingsService settingsService; + private final DirectoriesService directoriesService; + private final ApplicationLinksService applicationLinksService; + private final JiraAuthenticationService authenticationService; + private final LicensesService licensesService; + private final MailServerService mailServerService; + private final PermissionsService permissionsService; + + public _AllServiceImpl( + final JiraSettingsService settingsService, + final DirectoriesService directoriesService, + final ApplicationLinksService applicationLinksService, + final JiraAuthenticationService authenticationService, + final LicensesService licensesService, + final MailServerService mailServerService, + final PermissionsService permissionsService) { + + this.settingsService = settingsService; + this.directoriesService = directoriesService; + this.applicationLinksService = applicationLinksService; + this.authenticationService = authenticationService; + this.licensesService = licensesService; + this.mailServerService = mailServerService; + this.permissionsService = permissionsService; + } + + @Override + public _AllModel setAll( + final _AllModel allModel) { + + final _AllModel result = new _AllModel(); + final Map statusMap = new HashMap<>(); + + setEntityWithStatus(allModel.getSettings(), + settingsService::setSettings, result::setSettings, statusMap); + + setEntities(DIRECTORIES, allModel.getDirectories(), + directoriesService::setDirectories, result::setDirectories, statusMap); + + setEntities(APPLICATION_LINKS, allModel.getApplicationLinks(), + applicationLinksService::setApplicationLinks, result::setApplicationLinks, statusMap); + + setEntityWithStatus(allModel.getAuthentication(), + authenticationService::setAuthentication, result::setAuthentication, statusMap); + + setEntities(LICENSES, allModel.getLicenses(), + licensesService::setLicenses, result::setLicenses, statusMap); + + setEntityWithStatus(allModel.getMailServer(), + mailServerService::setMailServer, result::setMailServer, statusMap); + + setEntity(ServiceResultUtil.subEntityKey(PermissionsGlobalModel.class), allModel.getPermissionsGlobal(), + permissionsService::setPermissionsGlobal, result::setPermissionsGlobal, statusMap); + + result.setStatus(statusMap); + return result; + } +} diff --git a/jira/src/main/java/com/deftdevs/bootstrapi/jira/service/api/JiraSettingsService.java b/jira/src/main/java/com/deftdevs/bootstrapi/jira/service/api/JiraSettingsService.java index 7c6b54d8..ed439921 100644 --- a/jira/src/main/java/com/deftdevs/bootstrapi/jira/service/api/JiraSettingsService.java +++ b/jira/src/main/java/com/deftdevs/bootstrapi/jira/service/api/JiraSettingsService.java @@ -1,13 +1,20 @@ package com.deftdevs.bootstrapi.jira.service.api; -import com.deftdevs.bootstrapi.commons.model.SettingsModel; +import com.deftdevs.bootstrapi.commons.model.SettingsGeneralModel; import com.deftdevs.bootstrapi.commons.model.SettingsSecurityModel; +import com.deftdevs.bootstrapi.commons.model.type._AllModelStatus; +import com.deftdevs.bootstrapi.commons.model.type.ServiceResult; +import com.deftdevs.bootstrapi.commons.service.api.SettingsGeneralService; import com.deftdevs.bootstrapi.commons.service.api.SettingsSecurityService; -import com.deftdevs.bootstrapi.commons.service.api.SettingsService; +import com.deftdevs.bootstrapi.commons.util.ServiceResultUtil; import com.deftdevs.bootstrapi.jira.model.SettingsBannerModel; +import com.deftdevs.bootstrapi.jira.model.SettingsModel; + +import java.util.LinkedHashMap; +import java.util.Map; public interface JiraSettingsService extends - SettingsService, + SettingsGeneralService, SettingsSecurityService { SettingsBannerModel getSettingsBanner(); @@ -15,4 +22,42 @@ public interface JiraSettingsService extends SettingsBannerModel setSettingsBanner( SettingsBannerModel settingsBannerModel); + default SettingsModel getSettings() { + return new SettingsModel(getSettingsGeneral(), getSettingsSecurity(), getSettingsBanner()); + } + + default ServiceResult setSettings(final SettingsModel settingsModel) { + final SettingsModel result = new SettingsModel(); + final Map status = new LinkedHashMap<>(); + + if (settingsModel.getGeneral() != null) { + final String key = ServiceResultUtil.subEntityKey(SettingsGeneralModel.class); + try { + result.setGeneral(setSettingsGeneral(settingsModel.getGeneral())); + status.put(key, _AllModelStatus.success()); + } catch (Exception e) { + status.put(key, ServiceResultUtil.toErrorStatus(key, e)); + } + } + if (settingsModel.getSecurity() != null) { + final String key = ServiceResultUtil.subEntityKey(SettingsSecurityModel.class); + try { + result.setSecurity(setSettingsSecurity(settingsModel.getSecurity())); + status.put(key, _AllModelStatus.success()); + } catch (Exception e) { + status.put(key, ServiceResultUtil.toErrorStatus(key, e)); + } + } + if (settingsModel.getBanner() != null) { + final String key = ServiceResultUtil.subEntityKey(SettingsBannerModel.class); + try { + result.setBanner(setSettingsBanner(settingsModel.getBanner())); + status.put(key, _AllModelStatus.success()); + } catch (Exception e) { + status.put(key, ServiceResultUtil.toErrorStatus(key, e)); + } + } + return new ServiceResult<>(result, status); + } + } diff --git a/jira/src/test/java/com/deftdevs/bootstrapi/jira/rest/_AllResourceTest.java b/jira/src/test/java/com/deftdevs/bootstrapi/jira/rest/_AllResourceTest.java new file mode 100644 index 00000000..c5fe65f0 --- /dev/null +++ b/jira/src/test/java/com/deftdevs/bootstrapi/jira/rest/_AllResourceTest.java @@ -0,0 +1,88 @@ +package com.deftdevs.bootstrapi.jira.rest; + +import com.deftdevs.bootstrapi.commons.model.SettingsGeneralModel; +import com.deftdevs.bootstrapi.commons.model.type._AllModelStatus; +import com.deftdevs.bootstrapi.commons.service.api._AllService; +import com.deftdevs.bootstrapi.jira.model.SettingsModel; +import com.deftdevs.bootstrapi.jira.model._AllModel; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import javax.ws.rs.core.Response; +import java.util.HashMap; +import java.util.Map; + +import static com.deftdevs.bootstrapi.commons.constants.BootstrAPI.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +public class _AllResourceTest { + + @Mock + private _AllService<_AllModel> allService; + + private _AllResourceImpl allResource; + + private _AllModel allModel; + + @BeforeEach + public void setup() { + allResource = new _AllResourceImpl(allService); + + allModel = new _AllModel(); + final SettingsModel settings = new SettingsModel(); + settings.setGeneral(SettingsGeneralModel.EXAMPLE_1); + allModel.setSettings(settings); + + final Map status = new HashMap<>(); + status.put(SETTINGS + "/" + SETTINGS_GENERAL, _AllModelStatus.success()); + status.put(DIRECTORIES, _AllModelStatus.success()); + status.put(APPLICATION_LINKS, _AllModelStatus.success()); + allModel.setStatus(status); + } + + @Test + public void testSetAll() { + doReturn(allModel).when(allService).setAll(any()); + + final Response response = allResource.setAll(allModel); + assertEquals(200, response.getStatus()); + + final _AllModel responseModel = (_AllModel) response.getEntity(); + assertEquals(allModel, responseModel); + + verify(allService).setAll(allModel); + } + + @Test + public void testSetAllReturns500WhenAnySubFieldFailedWithServerError() { + final _AllModel result = new _AllModel(); + final Map status = new HashMap<>(); + status.put(SETTINGS + "/" + SETTINGS_GENERAL, _AllModelStatus.success()); + status.put(DIRECTORIES, + _AllModelStatus.error(Response.Status.INTERNAL_SERVER_ERROR, "boom", null)); + result.setStatus(status); + doReturn(result).when(allService).setAll(any()); + + final Response response = allResource.setAll(allModel); + assertEquals(500, response.getStatus()); + } + + @Test + public void testSetAllReturns207OnMixedSuccessAndClientError() { + final _AllModel result = new _AllModel(); + final Map status = new HashMap<>(); + status.put(SETTINGS + "/" + SETTINGS_GENERAL, _AllModelStatus.success()); + status.put(DIRECTORIES, + _AllModelStatus.error(Response.Status.BAD_REQUEST, "bad", null)); + result.setStatus(status); + doReturn(result).when(allService).setAll(any()); + + final Response response = allResource.setAll(allModel); + assertEquals(207, response.getStatus()); + } +} diff --git a/jira/src/test/java/com/deftdevs/bootstrapi/jira/service/JiraSettingsServiceTest.java b/jira/src/test/java/com/deftdevs/bootstrapi/jira/service/JiraSettingsServiceTest.java index dbd56e8a..72e05366 100644 --- a/jira/src/test/java/com/deftdevs/bootstrapi/jira/service/JiraSettingsServiceTest.java +++ b/jira/src/test/java/com/deftdevs/bootstrapi/jira/service/JiraSettingsServiceTest.java @@ -2,17 +2,23 @@ import com.atlassian.jira.config.properties.ApplicationProperties; import com.deftdevs.bootstrapi.commons.exception.web.BadRequestException; -import com.deftdevs.bootstrapi.commons.model.SettingsModel; +import com.deftdevs.bootstrapi.commons.model.SettingsGeneralModel; +import com.deftdevs.bootstrapi.commons.model.SettingsSecurityModel; +import com.deftdevs.bootstrapi.commons.model.type.ServiceResult; +import com.deftdevs.bootstrapi.jira.model.SettingsBannerModel; +import com.deftdevs.bootstrapi.jira.model.SettingsModel; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import javax.ws.rs.core.Response; import java.net.URI; import static com.atlassian.jira.config.properties.APKeys.*; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.Mockito.*; @@ -43,7 +49,7 @@ void testGetSettingsGeneral() { doReturn(CONTACT_MESSAGE).when(applicationProperties).getString(JIRA_CONTACT_ADMINISTRATORS_MESSSAGE); doReturn(EXTERNAL_USER_MANAGEMENT).when(applicationProperties).getString(JIRA_OPTION_USER_EXTERNALMGT); - final SettingsModel settingsModel = settingsService.getSettingsGeneral(); + final SettingsGeneralModel settingsModel = settingsService.getSettingsGeneral(); assertEquals(BASE_URL, settingsModel.getBaseUrl()); assertEquals(MODE_PUBLIC, settingsModel.getMode()); @@ -54,7 +60,7 @@ void testGetSettingsGeneral() { @Test void testSetSettingsGeneral() { - final SettingsModel settingsModel = SettingsModel.builder() + final SettingsGeneralModel settingsModel = SettingsGeneralModel.builder() .baseUrl(BASE_URL) .mode(MODE_PUBLIC) .title(TITLE) @@ -73,7 +79,7 @@ void testSetSettingsGeneral() { @Test void testSetSettingsGeneralEmptyModel() { - final SettingsModel settingsModel = SettingsModel.builder().build(); + final SettingsGeneralModel settingsModel = SettingsGeneralModel.builder().build(); settingsService.setSettingsGeneral(settingsModel); @@ -85,7 +91,7 @@ void testSetSettingsGeneralEmptyModel() { @Test void testSetSettingsGeneralUnsupportedMode() { - final SettingsModel settingsModel = SettingsModel.builder().mode("unsupported").build(); + final SettingsGeneralModel settingsModel = SettingsGeneralModel.builder().mode("unsupported").build(); assertThrows(BadRequestException.class, () -> { settingsService.setSettingsGeneral(settingsModel); @@ -94,7 +100,7 @@ void testSetSettingsGeneralUnsupportedMode() { @Test void testSetSettingsGeneralInvalidCombination() { - final SettingsModel settingsModel = SettingsModel.builder().mode(MODE_PUBLIC).build(); + final SettingsGeneralModel settingsModel = SettingsGeneralModel.builder().mode(MODE_PUBLIC).build(); doReturn(true).when(applicationProperties).getOption(JIRA_OPTION_USER_EXTERNALMGT); assertThrows(BadRequestException.class, () -> { @@ -102,4 +108,67 @@ void testSetSettingsGeneralInvalidCombination() { }); } + // composite getSettings/setSettings default methods + + @Test + void testGetSettings() { + final SettingsServiceImpl serviceSpy = spy(settingsService); + doReturn(SettingsGeneralModel.EXAMPLE_1).when(serviceSpy).getSettingsGeneral(); + doReturn(SettingsSecurityModel.EXAMPLE_1).when(serviceSpy).getSettingsSecurity(); + doReturn(SettingsBannerModel.EXAMPLE_1).when(serviceSpy).getSettingsBanner(); + + final SettingsModel settingsModel = serviceSpy.getSettings(); + + assertEquals(SettingsGeneralModel.EXAMPLE_1, settingsModel.getGeneral()); + assertEquals(SettingsSecurityModel.EXAMPLE_1, settingsModel.getSecurity()); + assertEquals(SettingsBannerModel.EXAMPLE_1, settingsModel.getBanner()); + } + + @Test + void testSetSettingsAppliesAllSubFields() { + final SettingsServiceImpl serviceSpy = spy(settingsService); + doReturn(SettingsGeneralModel.EXAMPLE_1).when(serviceSpy).setSettingsGeneral(SettingsGeneralModel.EXAMPLE_1); + doReturn(SettingsSecurityModel.EXAMPLE_1).when(serviceSpy).setSettingsSecurity(SettingsSecurityModel.EXAMPLE_1); + doReturn(SettingsBannerModel.EXAMPLE_1).when(serviceSpy).setSettingsBanner(SettingsBannerModel.EXAMPLE_1); + + final ServiceResult result = serviceSpy.setSettings(new SettingsModel( + SettingsGeneralModel.EXAMPLE_1, + SettingsSecurityModel.EXAMPLE_1, + SettingsBannerModel.EXAMPLE_1)); + + assertEquals(SettingsGeneralModel.EXAMPLE_1, result.getModel().getGeneral()); + assertEquals(SettingsSecurityModel.EXAMPLE_1, result.getModel().getSecurity()); + assertEquals(SettingsBannerModel.EXAMPLE_1, result.getModel().getBanner()); + assertEquals(Response.Status.OK.getStatusCode(), result.getStatus().get("settings/general").getStatus()); + assertEquals(Response.Status.OK.getStatusCode(), result.getStatus().get("settings/security").getStatus()); + assertEquals(Response.Status.OK.getStatusCode(), result.getStatus().get("settings/banner").getStatus()); + } + + @Test + void testSetSettingsSkipsNullSubFields() { + final SettingsServiceImpl serviceSpy = spy(settingsService); + + final ServiceResult result = serviceSpy.setSettings(new SettingsModel()); + + assertEquals(0, result.getStatus().size()); + verify(serviceSpy, never()).setSettingsGeneral(SettingsGeneralModel.EXAMPLE_1); + } + + @Test + void testSetSettingsRecordsPerSubFieldFailure() { + final SettingsServiceImpl serviceSpy = spy(settingsService); + doReturn(SettingsGeneralModel.EXAMPLE_1).when(serviceSpy).setSettingsGeneral(SettingsGeneralModel.EXAMPLE_1); + doThrow(new BadRequestException("invalid banner")).when(serviceSpy).setSettingsBanner(SettingsBannerModel.EXAMPLE_1); + + final ServiceResult result = serviceSpy.setSettings(new SettingsModel( + SettingsGeneralModel.EXAMPLE_1, + null, + SettingsBannerModel.EXAMPLE_1)); + + assertEquals(SettingsGeneralModel.EXAMPLE_1, result.getModel().getGeneral()); + assertNull(result.getModel().getBanner()); + assertEquals(Response.Status.OK.getStatusCode(), result.getStatus().get("settings/general").getStatus()); + assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), result.getStatus().get("settings/banner").getStatus()); + } + } diff --git a/jira/src/test/java/com/deftdevs/bootstrapi/jira/service/LicensesServiceTest.java b/jira/src/test/java/com/deftdevs/bootstrapi/jira/service/LicensesServiceTest.java index 6fb05576..44327650 100644 --- a/jira/src/test/java/com/deftdevs/bootstrapi/jira/service/LicensesServiceTest.java +++ b/jira/src/test/java/com/deftdevs/bootstrapi/jira/service/LicensesServiceTest.java @@ -12,11 +12,14 @@ import org.mockito.junit.jupiter.MockitoExtension; import java.util.Collections; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; import static com.atlassian.extras.api.LicenseType.TESTING; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.Mockito.*; @ExtendWith(MockitoExtension.class) @@ -64,6 +67,38 @@ void testSetLicenses() { verify(spy).getLicenses(); } + @Test + void testSetLicensesMapRedactsKeysAndPairsPerKey() { + // Set up two distinct LicenseDetails so we can verify each map entry's + // value comes from that key's own addLicense call (not from index-into-getLicenses). + final LicensedApplications apps = mock(LicensedApplications.class); + doReturn(Collections.singleton(ApplicationKey.valueOf("jira"))).when(apps).getKeys(); + final LicenseDetails detailsA = mock(LicenseDetails.class); + doReturn(apps).when(detailsA).getLicensedApplications(); + doReturn(TESTING).when(detailsA).getLicenseType(); + final LicenseDetails detailsB = mock(LicenseDetails.class); + doReturn(apps).when(detailsB).getLicensedApplications(); + doReturn(TESTING).when(detailsB).getLicenseType(); + + doReturn(detailsA).when(licenseManager).setLicense("KEY_A_payload_aaaa"); + doReturn(detailsB).when(licenseManager).setLicense("KEY_B_payload_bbbb"); + + final Map input = new LinkedHashMap<>(); + input.put("KEY_A_payload_aaaa", null); + input.put("KEY_B_payload_bbbb", null); + + final Map result = licensesService.setLicenses(input); + + assertEquals(2, result.size()); + // Keys in the response must be redacted (not the original keys). + assertTrue(result.keySet().stream().allMatch(k -> k.contains("...") && k.contains("#"))); + assertTrue(result.keySet().stream().noneMatch(k -> k.contains("payload"))); + // Each addLicense was called per input key (verifies we aren't relying + // on JiraLicenseManager.getLicenses() ordering). + verify(licenseManager).setLicense("KEY_A_payload_aaaa"); + verify(licenseManager).setLicense("KEY_B_payload_bbbb"); + } + @Test void testAddLicense() { final LicensedApplications licensedApplications = mock(LicensedApplications.class); diff --git a/jira/src/test/java/com/deftdevs/bootstrapi/jira/service/SettingsServiceTest.java b/jira/src/test/java/com/deftdevs/bootstrapi/jira/service/SettingsServiceTest.java index c4992884..c176c44f 100644 --- a/jira/src/test/java/com/deftdevs/bootstrapi/jira/service/SettingsServiceTest.java +++ b/jira/src/test/java/com/deftdevs/bootstrapi/jira/service/SettingsServiceTest.java @@ -2,7 +2,7 @@ import com.atlassian.jira.config.properties.ApplicationProperties; import com.deftdevs.bootstrapi.commons.exception.web.BadRequestException; -import com.deftdevs.bootstrapi.commons.model.SettingsModel; +import com.deftdevs.bootstrapi.commons.model.SettingsGeneralModel; import com.deftdevs.bootstrapi.commons.model.SettingsSecurityModel; import com.deftdevs.bootstrapi.jira.model.SettingsBannerModel; import org.junit.jupiter.api.BeforeEach; @@ -45,7 +45,7 @@ void testGetSettingsGeneral() { doReturn(CONTACT_MESSAGE).when(applicationProperties).getString(JIRA_CONTACT_ADMINISTRATORS_MESSSAGE); doReturn(EXTERNAL_USER_MANAGEMENT).when(applicationProperties).getString(JIRA_OPTION_USER_EXTERNALMGT); - final SettingsModel settingsModel = settingsService.getSettingsGeneral(); + final SettingsGeneralModel settingsModel = settingsService.getSettingsGeneral(); assertEquals(BASE_URL, settingsModel.getBaseUrl()); assertEquals(MODE_PUBLIC, settingsModel.getMode()); @@ -56,7 +56,7 @@ void testGetSettingsGeneral() { @Test void testSetSettingsGeneral() { - final SettingsModel settingsModel = SettingsModel.builder() + final SettingsGeneralModel settingsModel = SettingsGeneralModel.builder() .baseUrl(BASE_URL) .mode(MODE_PUBLIC) .title(TITLE) @@ -75,7 +75,7 @@ void testSetSettingsGeneral() { @Test void testSetSettingsGeneralEmptyModel() { - final SettingsModel settingsModel = SettingsModel.builder().build(); + final SettingsGeneralModel settingsModel = SettingsGeneralModel.builder().build(); settingsService.setSettingsGeneral(settingsModel); @@ -109,7 +109,7 @@ void testSetSettingsSecurity() { @Test void testSetSettingsGeneralUnsupportedMode() { - final SettingsModel settingsModel = SettingsModel.builder().mode("unsupported").build(); + final SettingsGeneralModel settingsModel = SettingsGeneralModel.builder().mode("unsupported").build(); assertThrows(BadRequestException.class, () -> { settingsService.setSettingsGeneral(settingsModel); @@ -118,7 +118,7 @@ void testSetSettingsGeneralUnsupportedMode() { @Test void testSetSettingsGeneralInvalidCombination() { - final SettingsModel settingsModel = SettingsModel.builder().mode(MODE_PUBLIC).build(); + final SettingsGeneralModel settingsModel = SettingsGeneralModel.builder().mode(MODE_PUBLIC).build(); doReturn(true).when(applicationProperties).getOption(JIRA_OPTION_USER_EXTERNALMGT); assertThrows(BadRequestException.class, () -> { diff --git a/jira/src/test/java/com/deftdevs/bootstrapi/jira/service/_AllServiceImplTest.java b/jira/src/test/java/com/deftdevs/bootstrapi/jira/service/_AllServiceImplTest.java new file mode 100644 index 00000000..71496718 --- /dev/null +++ b/jira/src/test/java/com/deftdevs/bootstrapi/jira/service/_AllServiceImplTest.java @@ -0,0 +1,172 @@ +package com.deftdevs.bootstrapi.jira.service; + +import com.deftdevs.bootstrapi.commons.model.AbstractDirectoryModel; +import com.deftdevs.bootstrapi.commons.model.ApplicationLinkModel; +import com.deftdevs.bootstrapi.commons.model.AuthenticationModel; +import com.deftdevs.bootstrapi.commons.model.LicenseModel; +import com.deftdevs.bootstrapi.commons.model.MailServerModel; +import com.deftdevs.bootstrapi.commons.model.MailServerSmtpModel; +import com.deftdevs.bootstrapi.commons.model.PermissionsGlobalModel; +import com.deftdevs.bootstrapi.commons.model.SettingsGeneralModel; +import com.deftdevs.bootstrapi.commons.model.type.ServiceResult; +import com.deftdevs.bootstrapi.commons.model.type._AllModelStatus; +import com.deftdevs.bootstrapi.commons.service.api.ApplicationLinksService; +import com.deftdevs.bootstrapi.commons.service.api.DirectoriesService; +import com.deftdevs.bootstrapi.commons.service.api.LicensesService; +import com.deftdevs.bootstrapi.commons.service.api.MailServerService; +import com.deftdevs.bootstrapi.commons.service.api.PermissionsService; +import com.deftdevs.bootstrapi.jira.model.SettingsModel; +import com.deftdevs.bootstrapi.jira.model._AllModel; +import com.deftdevs.bootstrapi.jira.service.api.JiraAuthenticationService; +import com.deftdevs.bootstrapi.jira.service.api.JiraSettingsService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.Response; +import java.util.Collections; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verifyNoInteractions; + +@ExtendWith(MockitoExtension.class) +class _AllServiceImplTest { + + @Mock + private JiraSettingsService settingsService; + + @Mock + private DirectoriesService directoriesService; + + @Mock + private ApplicationLinksService applicationLinksService; + + @Mock + private JiraAuthenticationService authenticationService; + + @Mock + private LicensesService licensesService; + + @Mock + private MailServerService mailServerService; + + @Mock + private PermissionsService permissionsService; + + private _AllServiceImpl allService; + + @BeforeEach + void setup() { + allService = new _AllServiceImpl( + settingsService, + directoriesService, + applicationLinksService, + authenticationService, + licensesService, + mailServerService, + permissionsService); + } + + @Test + void testSetAllEmptyModelYieldsEmptyStatus() { + final _AllModel result = allService.setAll(new _AllModel()); + + assertTrue(result.getStatus().isEmpty()); + verifyNoInteractions(settingsService, directoriesService, applicationLinksService, + authenticationService, licensesService, mailServerService, permissionsService); + } + + @Test + void testSetAllAppliesAllFields() { + final SettingsModel settings = new SettingsModel(); + settings.setGeneral(SettingsGeneralModel.EXAMPLE_1); + final Map directories = + Collections.singletonMap("directory", mock(AbstractDirectoryModel.class)); + final Map applicationLinks = + Collections.singletonMap("link", ApplicationLinkModel.EXAMPLE_1); + final AuthenticationModel authentication = new AuthenticationModel(); + final Map licenses = + Collections.singletonMap("licenseKey", LicenseModel.EXAMPLE_1); + final Map redactedLicenses = + Collections.singletonMap("lice...nse1#abcd", LicenseModel.EXAMPLE_1); + final MailServerModel mailServer = new MailServerModel(MailServerSmtpModel.EXAMPLE_1, null); + final PermissionsGlobalModel permissionsGlobal = new PermissionsGlobalModel(); + + doReturn(new ServiceResult<>(settings, + Collections.singletonMap("settings/general", _AllModelStatus.success()))) + .when(settingsService).setSettings(settings); + doReturn(directories).when(directoriesService).setDirectories(directories); + doReturn(applicationLinks).when(applicationLinksService).setApplicationLinks(applicationLinks); + doReturn(new ServiceResult<>(authentication, + Collections.singletonMap("authentication/sso", _AllModelStatus.success()))) + .when(authenticationService).setAuthentication(authentication); + doReturn(redactedLicenses).when(licensesService).setLicenses(licenses); + doReturn(new ServiceResult<>(mailServer, + Collections.singletonMap("mail-server/smtp", _AllModelStatus.success()))) + .when(mailServerService).setMailServer(mailServer); + doReturn(permissionsGlobal).when(permissionsService).setPermissionsGlobal(permissionsGlobal); + + final _AllModel allModel = new _AllModel(); + allModel.setSettings(settings); + allModel.setDirectories(directories); + allModel.setApplicationLinks(applicationLinks); + allModel.setAuthentication(authentication); + allModel.setLicenses(licenses); + allModel.setMailServer(mailServer); + allModel.setPermissionsGlobal(permissionsGlobal); + + final _AllModel result = allService.setAll(allModel); + + assertEquals(settings, result.getSettings()); + assertEquals(directories, result.getDirectories()); + assertEquals(applicationLinks, result.getApplicationLinks()); + assertEquals(authentication, result.getAuthentication()); + assertEquals(redactedLicenses, result.getLicenses()); + assertEquals(mailServer, result.getMailServer()); + assertEquals(permissionsGlobal, result.getPermissionsGlobal()); + + final Map status = result.getStatus(); + assertEquals(7, status.size()); + assertEquals(Response.Status.OK.getStatusCode(), status.get("settings/general").getStatus()); + assertEquals(Response.Status.OK.getStatusCode(), status.get("directories").getStatus()); + assertEquals(Response.Status.OK.getStatusCode(), status.get("application-links").getStatus()); + assertEquals(Response.Status.OK.getStatusCode(), status.get("authentication/sso").getStatus()); + assertEquals(Response.Status.OK.getStatusCode(), status.get("licenses").getStatus()); + assertEquals(Response.Status.OK.getStatusCode(), status.get("mail-server/smtp").getStatus()); + assertEquals(Response.Status.OK.getStatusCode(), status.get("permissions/global").getStatus()); + } + + @Test + void testSetAllRecordsFailureAndContinuesWithOtherFields() { + final SettingsModel settings = new SettingsModel(); + settings.setGeneral(SettingsGeneralModel.EXAMPLE_1); + final Map licenses = + Collections.singletonMap("licenseKey", LicenseModel.EXAMPLE_1); + + doReturn(new ServiceResult<>(settings, + Collections.singletonMap("settings/general", _AllModelStatus.success()))) + .when(settingsService).setSettings(settings); + doThrow(new WebApplicationException(Response.Status.CONFLICT)) + .when(licensesService).setLicenses(licenses); + + final _AllModel allModel = new _AllModel(); + allModel.setSettings(settings); + allModel.setLicenses(licenses); + + final _AllModel result = allService.setAll(allModel); + + assertEquals(settings, result.getSettings()); + assertNull(result.getLicenses()); + assertEquals(Response.Status.OK.getStatusCode(), result.getStatus().get("settings/general").getStatus()); + assertEquals(Response.Status.CONFLICT.getStatusCode(), result.getStatus().get("licenses").getStatus()); + } +} diff --git a/jira/src/test/java/it/com/deftdevs/bootstrapi/jira/rest/SettingsGeneralResourceFuncTest.java b/jira/src/test/java/it/com/deftdevs/bootstrapi/jira/rest/SettingsGeneralResourceFuncTest.java new file mode 100644 index 00000000..d8d7c2d8 --- /dev/null +++ b/jira/src/test/java/it/com/deftdevs/bootstrapi/jira/rest/SettingsGeneralResourceFuncTest.java @@ -0,0 +1,5 @@ +package it.com.deftdevs.bootstrapi.jira.rest; + +import it.com.deftdevs.bootstrapi.commons.rest.AbstractSettingsGeneralResourceFuncTest; + +public class SettingsGeneralResourceFuncTest extends AbstractSettingsGeneralResourceFuncTest { } diff --git a/jira/src/test/java/it/com/deftdevs/bootstrapi/jira/rest/SettingsResourceFuncTest.java b/jira/src/test/java/it/com/deftdevs/bootstrapi/jira/rest/SettingsResourceFuncTest.java deleted file mode 100644 index cc6cc1a1..00000000 --- a/jira/src/test/java/it/com/deftdevs/bootstrapi/jira/rest/SettingsResourceFuncTest.java +++ /dev/null @@ -1,5 +0,0 @@ -package it.com.deftdevs.bootstrapi.jira.rest; - -import it.com.deftdevs.bootstrapi.commons.rest.AbstractSettingsResourceFuncTest; - -public class SettingsResourceFuncTest extends AbstractSettingsResourceFuncTest { } diff --git a/jira/src/test/java/it/com/deftdevs/bootstrapi/jira/rest/_AllResourceFuncTest.java b/jira/src/test/java/it/com/deftdevs/bootstrapi/jira/rest/_AllResourceFuncTest.java new file mode 100644 index 00000000..82212de9 --- /dev/null +++ b/jira/src/test/java/it/com/deftdevs/bootstrapi/jira/rest/_AllResourceFuncTest.java @@ -0,0 +1,46 @@ +package it.com.deftdevs.bootstrapi.jira.rest; + +import com.deftdevs.bootstrapi.commons.model.MailServerModel; +import com.deftdevs.bootstrapi.commons.model.MailServerPopModel; +import com.deftdevs.bootstrapi.commons.model.MailServerSmtpModel; +import com.deftdevs.bootstrapi.commons.model.SettingsGeneralModel; +import com.deftdevs.bootstrapi.commons.util.ServiceResultUtil; +import com.deftdevs.bootstrapi.jira.model.SettingsBannerModel; +import com.deftdevs.bootstrapi.jira.model.SettingsModel; +import com.deftdevs.bootstrapi.jira.model._AllModel; +import it.com.deftdevs.bootstrapi.commons.rest.Abstract_AllResourceFuncTest; +import org.junit.jupiter.api.Test; + +import java.util.Arrays; + +public class _AllResourceFuncTest extends Abstract_AllResourceFuncTest { + + @Override + protected Object getExampleAllModel() { + return _AllModel.builder() + .settings(SettingsModel.builder() + .general(getExampleSettingsGeneralModel()) + .build()) + .build(); + } + + @Test + void testSetAllAppliesMultipleEntities() throws Exception { + final _AllModel allModel = _AllModel.builder() + .settings(SettingsModel.builder() + .general(getExampleSettingsGeneralModel()) + .banner(SettingsBannerModel.EXAMPLE_1) + .build()) + .mailServer(MailServerModel.builder() + .smtp(MailServerSmtpModel.EXAMPLE_2) + .pop(MailServerPopModel.EXAMPLE_2) + .build()) + .build(); + + assertSetAllApplied(allModel, Arrays.asList( + ServiceResultUtil.subEntityKey(SettingsGeneralModel.class), + ServiceResultUtil.subEntityKey(SettingsBannerModel.class), + ServiceResultUtil.subEntityKey(MailServerSmtpModel.class), + ServiceResultUtil.subEntityKey(MailServerPopModel.class))); + } +} From 9f8fd49afe4fb8464395ae8d91af576a3737be6e Mon Sep 17 00:00:00 2001 From: Patrick Hobusch Date: Sun, 5 Jul 2026 21:50:40 +0200 Subject: [PATCH 02/18] Move map-based setLicenses into the LicensesService interface The implementation was identical in all three products: apply each license key and echo it back redacted. As a default method it lives next to its contract and the products only implement the product-specific addLicense. --- .../commons/service/api/LicensesService.java | 13 +++++++++++-- .../confluence/service/LicensesServiceImpl.java | 14 -------------- .../crowd/service/LicensesServiceImpl.java | 14 -------------- .../jira/service/LicensesServiceImpl.java | 14 -------------- 4 files changed, 11 insertions(+), 44 deletions(-) diff --git a/commons/src/main/java/com/deftdevs/bootstrapi/commons/service/api/LicensesService.java b/commons/src/main/java/com/deftdevs/bootstrapi/commons/service/api/LicensesService.java index 6c39a758..00c2d9a8 100644 --- a/commons/src/main/java/com/deftdevs/bootstrapi/commons/service/api/LicensesService.java +++ b/commons/src/main/java/com/deftdevs/bootstrapi/commons/service/api/LicensesService.java @@ -1,7 +1,9 @@ package com.deftdevs.bootstrapi.commons.service.api; import com.deftdevs.bootstrapi.commons.model.LicenseModel; +import com.deftdevs.bootstrapi.commons.util.LicenseKeyRedactor; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -32,8 +34,15 @@ List setLicenses( * @param licenseInputs map of license key to (optional) input model; values are ignored * @return map of redacted-key to applied {@link LicenseModel} */ - Map setLicenses( - Map licenseInputs); + default Map setLicenses( + final Map licenseInputs) { + + final Map result = new LinkedHashMap<>(); + for (final String licenseKey : licenseInputs.keySet()) { + result.put(LicenseKeyRedactor.redact(licenseKey), addLicense(licenseKey)); + } + return result; + } /** * Add a single license diff --git a/confluence/src/main/java/com/deftdevs/bootstrapi/confluence/service/LicensesServiceImpl.java b/confluence/src/main/java/com/deftdevs/bootstrapi/confluence/service/LicensesServiceImpl.java index df4f5d16..f78cefe2 100644 --- a/confluence/src/main/java/com/deftdevs/bootstrapi/confluence/service/LicensesServiceImpl.java +++ b/confluence/src/main/java/com/deftdevs/bootstrapi/confluence/service/LicensesServiceImpl.java @@ -7,13 +7,10 @@ import com.deftdevs.bootstrapi.commons.exception.web.InternalServerErrorException; import com.deftdevs.bootstrapi.commons.model.LicenseModel; import com.deftdevs.bootstrapi.commons.service.api.LicensesService; -import com.deftdevs.bootstrapi.commons.util.LicenseKeyRedactor; import com.deftdevs.bootstrapi.confluence.model.util.LicenseModelUtil; import java.util.Collections; -import java.util.LinkedHashMap; import java.util.List; -import java.util.Map; import static com.atlassian.confluence.setup.ConfluenceBootstrapConstants.DEFAULT_LICENSE_REGISTRY_KEY; @@ -46,17 +43,6 @@ public List setLicenses( return getLicenses(); } - @Override - public Map setLicenses( - final Map licenseInputs) { - - final Map result = new LinkedHashMap<>(); - for (final String key : licenseInputs.keySet()) { - result.put(LicenseKeyRedactor.redact(key), addLicense(key)); - } - return result; - } - @Override public LicenseModel addLicense( final String licenseKey) { diff --git a/crowd/src/main/java/com/deftdevs/bootstrapi/crowd/service/LicensesServiceImpl.java b/crowd/src/main/java/com/deftdevs/bootstrapi/crowd/service/LicensesServiceImpl.java index 3998434e..31a0d9cc 100644 --- a/crowd/src/main/java/com/deftdevs/bootstrapi/crowd/service/LicensesServiceImpl.java +++ b/crowd/src/main/java/com/deftdevs/bootstrapi/crowd/service/LicensesServiceImpl.java @@ -6,12 +6,9 @@ import com.deftdevs.bootstrapi.commons.exception.web.InternalServerErrorException; import com.deftdevs.bootstrapi.commons.model.LicenseModel; import com.deftdevs.bootstrapi.commons.service.api.LicensesService; -import com.deftdevs.bootstrapi.commons.util.LicenseKeyRedactor; import java.util.Collections; -import java.util.LinkedHashMap; import java.util.List; -import java.util.Map; public class LicensesServiceImpl implements LicensesService { @@ -43,17 +40,6 @@ public List setLicenses( ); } - @Override - public Map setLicenses( - final Map licenseInputs) { - - final Map result = new LinkedHashMap<>(); - for (final String key : licenseInputs.keySet()) { - result.put(LicenseKeyRedactor.redact(key), addLicense(key)); - } - return result; - } - public LicenseModel addLicense( final String licenseKey) { diff --git a/jira/src/main/java/com/deftdevs/bootstrapi/jira/service/LicensesServiceImpl.java b/jira/src/main/java/com/deftdevs/bootstrapi/jira/service/LicensesServiceImpl.java index 6f50abfb..bdf8badc 100644 --- a/jira/src/main/java/com/deftdevs/bootstrapi/jira/service/LicensesServiceImpl.java +++ b/jira/src/main/java/com/deftdevs/bootstrapi/jira/service/LicensesServiceImpl.java @@ -3,12 +3,9 @@ import com.atlassian.jira.license.JiraLicenseManager; import com.deftdevs.bootstrapi.commons.model.LicenseModel; import com.deftdevs.bootstrapi.commons.service.api.LicensesService; -import com.deftdevs.bootstrapi.commons.util.LicenseKeyRedactor; import com.deftdevs.bootstrapi.jira.model.util.LicenseModelUtil; -import java.util.LinkedHashMap; import java.util.List; -import java.util.Map; import java.util.stream.Collectors; import java.util.stream.StreamSupport; @@ -39,17 +36,6 @@ public List setLicenses( return getLicenses(); } - @Override - public Map setLicenses( - final Map licenseInputs) { - - final Map result = new LinkedHashMap<>(); - for (final String key : licenseInputs.keySet()) { - result.put(LicenseKeyRedactor.redact(key), addLicense(key)); - } - return result; - } - @Override public LicenseModel addLicense( final String license) { From c8ab876f12b1b8d2fd10a7878005f5d46b37bff9 Mon Sep 17 00:00:00 2001 From: Patrick Hobusch Date: Sun, 5 Jul 2026 21:50:40 +0200 Subject: [PATCH 03/18] Replace crowd functional test fixtures with model example constants SettingsGeneralModel.EXAMPLE_1_MINIMAL and MailServerSmtpModel.EXAMPLE_2_MINIMAL describe the field subset supported by all products, so the crowd functional tests no longer need their own fixture class or inline builders. --- .../commons/model/MailServerSmtpModel.java | 10 ++++++ .../commons/model/SettingsGeneralModel.java | 6 ++++ .../crowd/rest/FuncTestExamples.java | 31 ------------------- .../rest/MailServerSmtpResourceFuncTest.java | 9 +----- .../rest/SettingsGeneralResourceFuncTest.java | 2 +- .../crowd/rest/_AllResourceFuncTest.java | 4 +-- 6 files changed, 20 insertions(+), 42 deletions(-) delete mode 100644 crowd/src/test/java/it/com/deftdevs/bootstrapi/crowd/rest/FuncTestExamples.java diff --git a/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/MailServerSmtpModel.java b/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/MailServerSmtpModel.java index faf3bfe1..0881ba1d 100644 --- a/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/MailServerSmtpModel.java +++ b/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/MailServerSmtpModel.java @@ -57,4 +57,14 @@ public boolean getUseTls() { .timeout(5000L) .build(); + // restricted to the fields supported by all products (e.g. Crowd does not echo name and protocol) + public static final MailServerSmtpModel EXAMPLE_2_MINIMAL = MailServerSmtpModel.builder() + .from(EXAMPLE_2.getFrom()) + .prefix(EXAMPLE_2.getPrefix()) + .host(EXAMPLE_2.getHost()) + .port(EXAMPLE_2.getPort()) + .useTls(EXAMPLE_2.getUseTls()) + .timeout(EXAMPLE_2.getTimeout()) + .build(); + } diff --git a/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/SettingsGeneralModel.java b/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/SettingsGeneralModel.java index db8e9dbe..ca330741 100644 --- a/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/SettingsGeneralModel.java +++ b/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/SettingsGeneralModel.java @@ -64,4 +64,10 @@ public String getMode() { .externalUserManagement(true) .build(); + // restricted to the fields supported by all products (e.g. Crowd) + public static final SettingsGeneralModel EXAMPLE_1_MINIMAL = SettingsGeneralModel.builder() + .title(EXAMPLE_1.getTitle()) + .baseUrl(EXAMPLE_1.getBaseUrl()) + .build(); + } diff --git a/crowd/src/test/java/it/com/deftdevs/bootstrapi/crowd/rest/FuncTestExamples.java b/crowd/src/test/java/it/com/deftdevs/bootstrapi/crowd/rest/FuncTestExamples.java deleted file mode 100644 index 78543077..00000000 --- a/crowd/src/test/java/it/com/deftdevs/bootstrapi/crowd/rest/FuncTestExamples.java +++ /dev/null @@ -1,31 +0,0 @@ -package it.com.deftdevs.bootstrapi.crowd.rest; - -import com.deftdevs.bootstrapi.commons.model.MailServerSmtpModel; -import com.deftdevs.bootstrapi.commons.model.SettingsGeneralModel; - -/** - * Example entities for functional tests. Unlike the {@code EXAMPLE_*} constants on the - * models (which document the API), these values describe the integration test - * environment — the mail server points at the greenmail service started for - * integration test runs (see {@code ci.yaml}), and the general settings are limited - * to the fields supported by Crowd. - */ -public final class FuncTestExamples { - - public static final SettingsGeneralModel SETTINGS_GENERAL = SettingsGeneralModel.builder() - .baseUrl(SettingsGeneralModel.EXAMPLE_1.getBaseUrl()) - .title(SettingsGeneralModel.EXAMPLE_1.getTitle()) - .build(); - - public static final MailServerSmtpModel MAIL_SERVER_SMTP_GREENMAIL = MailServerSmtpModel.builder() - .from("test@example.com") - .prefix("[Test]") - .host("localhost") - .port(3025) - .useTls(false) - .timeout(5000L) - .build(); - - private FuncTestExamples() { - } -} diff --git a/crowd/src/test/java/it/com/deftdevs/bootstrapi/crowd/rest/MailServerSmtpResourceFuncTest.java b/crowd/src/test/java/it/com/deftdevs/bootstrapi/crowd/rest/MailServerSmtpResourceFuncTest.java index b075bb11..73ae9866 100644 --- a/crowd/src/test/java/it/com/deftdevs/bootstrapi/crowd/rest/MailServerSmtpResourceFuncTest.java +++ b/crowd/src/test/java/it/com/deftdevs/bootstrapi/crowd/rest/MailServerSmtpResourceFuncTest.java @@ -17,13 +17,6 @@ protected void testGetMailServerSmtpNotConfigured() { @Override protected MailServerSmtpModel getExampleModel() { - return MailServerSmtpModel.builder() - .from("test@example.com") - .prefix("[Test]") - .host("localhost") - .port(3025) - .useTls(false) - .timeout(5000L) - .build(); + return MailServerSmtpModel.EXAMPLE_2_MINIMAL; } } diff --git a/crowd/src/test/java/it/com/deftdevs/bootstrapi/crowd/rest/SettingsGeneralResourceFuncTest.java b/crowd/src/test/java/it/com/deftdevs/bootstrapi/crowd/rest/SettingsGeneralResourceFuncTest.java index 622bb6d1..12f22981 100644 --- a/crowd/src/test/java/it/com/deftdevs/bootstrapi/crowd/rest/SettingsGeneralResourceFuncTest.java +++ b/crowd/src/test/java/it/com/deftdevs/bootstrapi/crowd/rest/SettingsGeneralResourceFuncTest.java @@ -7,7 +7,7 @@ public class SettingsGeneralResourceFuncTest extends AbstractSettingsGeneralReso @Override protected SettingsGeneralModel getExampleModel() { - return FuncTestExamples.SETTINGS_GENERAL; + return SettingsGeneralModel.EXAMPLE_1_MINIMAL; } } diff --git a/crowd/src/test/java/it/com/deftdevs/bootstrapi/crowd/rest/_AllResourceFuncTest.java b/crowd/src/test/java/it/com/deftdevs/bootstrapi/crowd/rest/_AllResourceFuncTest.java index ffee15c4..ccc5cdab 100644 --- a/crowd/src/test/java/it/com/deftdevs/bootstrapi/crowd/rest/_AllResourceFuncTest.java +++ b/crowd/src/test/java/it/com/deftdevs/bootstrapi/crowd/rest/_AllResourceFuncTest.java @@ -27,7 +27,7 @@ protected Object getExampleAllModel() { @Override protected SettingsGeneralModel getExampleSettingsGeneralModel() { - return FuncTestExamples.SETTINGS_GENERAL; + return SettingsGeneralModel.EXAMPLE_1_MINIMAL; } @Test @@ -38,7 +38,7 @@ void testSetAllAppliesMultipleEntities() throws Exception { .general(getExampleSettingsGeneralModel()) .build()) .mailServer(MailServerModel.builder() - .smtp(FuncTestExamples.MAIL_SERVER_SMTP_GREENMAIL) + .smtp(MailServerSmtpModel.EXAMPLE_2_MINIMAL) .build()) .mailTemplates(MailTemplatesModel.EXAMPLE_1) .sessionConfig(SessionConfigModel.EXAMPLE_2) From fdda605c47cffa8e69bd6eb6b0895c6b88e19ecf Mon Sep 17 00:00:00 2001 From: Patrick Hobusch Date: Sun, 5 Jul 2026 21:50:40 +0200 Subject: [PATCH 04/18] Document the RFC stance of the 207 aggregation 207 Multi-Status originates in RFC 4918 (WebDAV) where it implies an XML multistatus body; RFC 9110 permits any registered code, and 207 with a JSON body carrying per-part statuses is established bulk-endpoint practice. The alternatives are worse: plain 200 hides partial failure from status-code-only clients, 4xx/5xx misrepresents the successfully applied sub-fields. --- .../commons/rest/_AbstractAllResourceImpl.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/commons/src/main/java/com/deftdevs/bootstrapi/commons/rest/_AbstractAllResourceImpl.java b/commons/src/main/java/com/deftdevs/bootstrapi/commons/rest/_AbstractAllResourceImpl.java index 45a5febf..4e6bc762 100644 --- a/commons/src/main/java/com/deftdevs/bootstrapi/commons/rest/_AbstractAllResourceImpl.java +++ b/commons/src/main/java/com/deftdevs/bootstrapi/commons/rest/_AbstractAllResourceImpl.java @@ -41,6 +41,18 @@ public Response setAll( * 207 is used to signal "partial success" — callers must inspect the * per-field {@code status} map in the response body to see which * fields succeeded and which failed. + *

+ * On RFC conformance: 207 Multi-Status is defined by RFC 4918 (WebDAV), + * where it implies an XML {@code multistatus} body. RFC 9110 permits any + * registered status code over HTTP, and using 207 with a JSON body + * carrying the per-part statuses is established practice for bulk-style + * REST endpoints. It was chosen deliberately over the alternatives: + * a plain 200 would hide partial failure from clients that only check the + * status code, and a 4xx/5xx would misrepresent the sub-fields that were + * applied successfully (this endpoint is not transactional). Note that + * 207 belongs to the 2xx success class, so scripted callers (e.g. + * {@code curl -f}) treat partial success as success unless they inspect + * the body. */ static int computeOverallStatus( final Map statusMap) { From 663236a919d480ee7203d745e8a673deabcfd464 Mon Sep 17 00:00:00 2001 From: Patrick Hobusch Date: Sun, 5 Jul 2026 21:53:31 +0200 Subject: [PATCH 05/18] Merge the product settings composites into one implementation each Confluence and Crowd had a SettingsServiceImpl next to a composite *SettingsServiceImpl that only delegated to it, unlike Jira where one implementation implements the whole product settings interface. Each product now has a single settings implementation and a single settings bean; the branding implementation stays internal to it, keeping by-type injection of the narrow settings interfaces unambiguous in the REST layer. Also drops the double blank lines in the Confluence implementation. --- .../confluence/config/ServiceConfig.java | 9 +- .../ConfluenceSettingsServiceImpl.java | 90 ------------------- .../service/SettingsServiceImpl.java | 52 ++++++++--- .../ConfluenceSettingsServiceTest.java | 16 ++-- .../service/SettingsServiceTest.java | 5 +- .../crowd/config/ServiceConfig.java | 23 ++--- .../service/CrowdSettingsServiceImpl.java | 49 ---------- .../crowd/service/SettingsServiceImpl.java | 29 +++++- .../service/CrowdSettingsServiceTest.java | 25 +++--- .../crowd/service/SettingsServiceTest.java | 5 +- 10 files changed, 107 insertions(+), 196 deletions(-) delete mode 100644 confluence/src/main/java/com/deftdevs/bootstrapi/confluence/service/ConfluenceSettingsServiceImpl.java delete mode 100644 crowd/src/main/java/com/deftdevs/bootstrapi/crowd/service/CrowdSettingsServiceImpl.java diff --git a/confluence/src/main/java/com/deftdevs/bootstrapi/confluence/config/ServiceConfig.java b/confluence/src/main/java/com/deftdevs/bootstrapi/confluence/config/ServiceConfig.java index aa43ae92..7bb8765e 100644 --- a/confluence/src/main/java/com/deftdevs/bootstrapi/confluence/config/ServiceConfig.java +++ b/confluence/src/main/java/com/deftdevs/bootstrapi/confluence/config/ServiceConfig.java @@ -58,18 +58,13 @@ public ConfluenceSettingsService confluenceSettingsService() { // The branding service is deliberately NOT its own bean: ConfluenceSettingsService // extends SettingsBrandingService, so a standalone branding bean would make by-type // injection of SettingsBrandingService ambiguous in the REST layer. - return new ConfluenceSettingsServiceImpl( - settingsService(), + return new SettingsServiceImpl( + atlassianConfig.globalSettingsManager(), new SettingsBrandingServiceImpl( atlassianConfig.colourSchemeManager(), atlassianConfig.siteLogoManager())); } - @Bean - public SettingsServiceImpl settingsService() { - return new SettingsServiceImpl(atlassianConfig.globalSettingsManager()); - } - @Bean public DirectoriesService directoriesService() { return new DirectoriesServiceImpl( diff --git a/confluence/src/main/java/com/deftdevs/bootstrapi/confluence/service/ConfluenceSettingsServiceImpl.java b/confluence/src/main/java/com/deftdevs/bootstrapi/confluence/service/ConfluenceSettingsServiceImpl.java deleted file mode 100644 index b3e7e586..00000000 --- a/confluence/src/main/java/com/deftdevs/bootstrapi/confluence/service/ConfluenceSettingsServiceImpl.java +++ /dev/null @@ -1,90 +0,0 @@ -package com.deftdevs.bootstrapi.confluence.service; - -import com.deftdevs.bootstrapi.commons.model.SettingsBrandingColorSchemeModel; -import com.deftdevs.bootstrapi.commons.model.SettingsGeneralModel; -import com.deftdevs.bootstrapi.commons.model.SettingsSecurityModel; -import com.deftdevs.bootstrapi.commons.service.api.SettingsBrandingService; -import com.deftdevs.bootstrapi.confluence.model.SettingsCustomHtmlModel; -import com.deftdevs.bootstrapi.confluence.service.api.ConfluenceSettingsService; - -import java.io.InputStream; - -public class ConfluenceSettingsServiceImpl implements ConfluenceSettingsService { - - private final SettingsServiceImpl settingsService; - private final SettingsBrandingService settingsBrandingService; - - /** - * The general/security/custom-html operations are provided by {@link SettingsServiceImpl}, - * which intentionally exposes {@code getCustomHtml}/{@code setCustomHtml} as - * non-interface methods (there is no Confluence-shared interface for them). - * That forces the concrete type here. - */ - public ConfluenceSettingsServiceImpl( - final SettingsServiceImpl settingsService, - final SettingsBrandingService settingsBrandingService) { - - this.settingsService = settingsService; - this.settingsBrandingService = settingsBrandingService; - } - - @Override - public SettingsGeneralModel getSettingsGeneral() { - return settingsService.getSettingsGeneral(); - } - - @Override - public SettingsGeneralModel setSettingsGeneral(final SettingsGeneralModel settingsGeneralModel) { - return settingsService.setSettingsGeneral(settingsGeneralModel); - } - - @Override - public SettingsSecurityModel getSettingsSecurity() { - return settingsService.getSettingsSecurity(); - } - - @Override - public SettingsSecurityModel setSettingsSecurity(final SettingsSecurityModel settingsSecurityModel) { - return settingsService.setSettingsSecurity(settingsSecurityModel); - } - - @Override - public SettingsCustomHtmlModel getCustomHtml() { - return settingsService.getCustomHtml(); - } - - @Override - public SettingsCustomHtmlModel setCustomHtml(final SettingsCustomHtmlModel settingsCustomHtmlModel) { - return settingsService.setCustomHtml(settingsCustomHtmlModel); - } - - @Override - public SettingsBrandingColorSchemeModel getColourScheme() { - return settingsBrandingService.getColourScheme(); - } - - @Override - public SettingsBrandingColorSchemeModel setColourScheme(final SettingsBrandingColorSchemeModel colourSchemeModel) { - return settingsBrandingService.setColourScheme(colourSchemeModel); - } - - @Override - public InputStream getLogo() { - return settingsBrandingService.getLogo(); - } - - @Override - public void setLogo(final InputStream inputStream) { - settingsBrandingService.setLogo(inputStream); - } - - @Override - public InputStream getFavicon() { - return settingsBrandingService.getFavicon(); - } - - @Override - public void setFavicon(final InputStream inputStream) { - settingsBrandingService.setFavicon(inputStream); - } -} diff --git a/confluence/src/main/java/com/deftdevs/bootstrapi/confluence/service/SettingsServiceImpl.java b/confluence/src/main/java/com/deftdevs/bootstrapi/confluence/service/SettingsServiceImpl.java index cedaaaab..a53d3b0a 100644 --- a/confluence/src/main/java/com/deftdevs/bootstrapi/confluence/service/SettingsServiceImpl.java +++ b/confluence/src/main/java/com/deftdevs/bootstrapi/confluence/service/SettingsServiceImpl.java @@ -3,25 +3,29 @@ import com.atlassian.confluence.setup.settings.CustomHtmlSettings; import com.atlassian.confluence.setup.settings.GlobalSettingsManager; import com.atlassian.confluence.setup.settings.Settings; +import com.deftdevs.bootstrapi.commons.model.SettingsBrandingColorSchemeModel; import com.deftdevs.bootstrapi.commons.model.SettingsGeneralModel; import com.deftdevs.bootstrapi.commons.model.SettingsSecurityModel; -import com.deftdevs.bootstrapi.commons.service.api.SettingsGeneralService; -import com.deftdevs.bootstrapi.commons.service.api.SettingsSecurityService; +import com.deftdevs.bootstrapi.commons.service.api.SettingsBrandingService; import com.deftdevs.bootstrapi.confluence.model.SettingsCustomHtmlModel; +import com.deftdevs.bootstrapi.confluence.service.api.ConfluenceSettingsService; + +import java.io.InputStream; import java.net.URI; -public class SettingsServiceImpl - implements SettingsGeneralService, SettingsSecurityService { +public class SettingsServiceImpl implements ConfluenceSettingsService { private final GlobalSettingsManager globalSettingsManager; + private final SettingsBrandingService settingsBrandingService; public SettingsServiceImpl( - final GlobalSettingsManager globalSettingsManager) { + final GlobalSettingsManager globalSettingsManager, + final SettingsBrandingService settingsBrandingService) { this.globalSettingsManager = globalSettingsManager; + this.settingsBrandingService = settingsBrandingService; } - @Override public SettingsGeneralModel getSettingsGeneral() { final Settings settings = globalSettingsManager.getGlobalSettings(); @@ -34,7 +38,6 @@ public SettingsGeneralModel getSettingsGeneral() { .build(); } - @Override public SettingsGeneralModel setSettingsGeneral(SettingsGeneralModel settingsModel) { final Settings settings = globalSettingsManager.getGlobalSettings(); @@ -60,7 +63,7 @@ public SettingsGeneralModel setSettingsGeneral(SettingsGeneralModel settingsMode return getSettingsGeneral(); } - + @Override public SettingsCustomHtmlModel getCustomHtml() { final CustomHtmlSettings customHtmlSettings = globalSettingsManager.getGlobalSettings().getCustomHtmlSettings(); @@ -71,7 +74,7 @@ public SettingsCustomHtmlModel getCustomHtml() { .build(); } - + @Override public SettingsCustomHtmlModel setCustomHtml( final SettingsCustomHtmlModel settingsCustomHtmlModel) { @@ -95,7 +98,6 @@ public SettingsCustomHtmlModel setCustomHtml( return getCustomHtml(); } - @Override public SettingsSecurityModel getSettingsSecurity() { final Settings settings = globalSettingsManager.getGlobalSettings(); @@ -106,7 +108,6 @@ public SettingsSecurityModel getSettingsSecurity() { .build(); } - @Override public SettingsSecurityModel setSettingsSecurity( final SettingsSecurityModel settingsSecurityModel) { @@ -124,4 +125,33 @@ public SettingsSecurityModel setSettingsSecurity( return getSettingsSecurity(); } + @Override + public SettingsBrandingColorSchemeModel getColourScheme() { + return settingsBrandingService.getColourScheme(); + } + + @Override + public SettingsBrandingColorSchemeModel setColourScheme(final SettingsBrandingColorSchemeModel colourSchemeModel) { + return settingsBrandingService.setColourScheme(colourSchemeModel); + } + + @Override + public InputStream getLogo() { + return settingsBrandingService.getLogo(); + } + + @Override + public void setLogo(final InputStream inputStream) { + settingsBrandingService.setLogo(inputStream); + } + + @Override + public InputStream getFavicon() { + return settingsBrandingService.getFavicon(); + } + + @Override + public void setFavicon(final InputStream inputStream) { + settingsBrandingService.setFavicon(inputStream); + } } diff --git a/confluence/src/test/java/com/deftdevs/bootstrapi/confluence/service/ConfluenceSettingsServiceTest.java b/confluence/src/test/java/com/deftdevs/bootstrapi/confluence/service/ConfluenceSettingsServiceTest.java index c215f08a..62ee3f74 100644 --- a/confluence/src/test/java/com/deftdevs/bootstrapi/confluence/service/ConfluenceSettingsServiceTest.java +++ b/confluence/src/test/java/com/deftdevs/bootstrapi/confluence/service/ConfluenceSettingsServiceTest.java @@ -1,5 +1,6 @@ package com.deftdevs.bootstrapi.confluence.service; +import com.atlassian.confluence.setup.settings.GlobalSettingsManager; import com.deftdevs.bootstrapi.commons.exception.web.BadRequestException; import com.deftdevs.bootstrapi.commons.model.SettingsBrandingColorSchemeModel; import com.deftdevs.bootstrapi.commons.model.SettingsGeneralModel; @@ -22,22 +23,23 @@ import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; @ExtendWith(MockitoExtension.class) class ConfluenceSettingsServiceTest { @Mock - private SettingsServiceImpl settingsService; + private GlobalSettingsManager globalSettingsManager; @Mock private SettingsBrandingService settingsBrandingService; - private ConfluenceSettingsServiceImpl confluenceSettingsService; + private SettingsServiceImpl settingsService; @BeforeEach void setup() { - confluenceSettingsService = new ConfluenceSettingsServiceImpl(settingsService, settingsBrandingService); + settingsService = spy(new SettingsServiceImpl(globalSettingsManager, settingsBrandingService)); } @Test @@ -47,7 +49,7 @@ void testGetSettings() { doReturn(SettingsBrandingColorSchemeModel.EXAMPLE_1).when(settingsBrandingService).getColourScheme(); doReturn(SettingsCustomHtmlModel.EXAMPLE_1).when(settingsService).getCustomHtml(); - final SettingsModel settingsModel = confluenceSettingsService.getSettings(); + final SettingsModel settingsModel = settingsService.getSettings(); assertEquals(SettingsGeneralModel.EXAMPLE_1, settingsModel.getGeneral()); assertEquals(SettingsSecurityModel.EXAMPLE_1, settingsModel.getSecurity()); @@ -62,7 +64,7 @@ void testSetSettingsAppliesAllSubFields() { doReturn(SettingsBrandingColorSchemeModel.EXAMPLE_1).when(settingsBrandingService).setColourScheme(SettingsBrandingColorSchemeModel.EXAMPLE_1); doReturn(SettingsCustomHtmlModel.EXAMPLE_1).when(settingsService).setCustomHtml(SettingsCustomHtmlModel.EXAMPLE_1); - final ServiceResult result = confluenceSettingsService.setSettings(new SettingsModel( + final ServiceResult result = settingsService.setSettings(new SettingsModel( SettingsGeneralModel.EXAMPLE_1, SettingsSecurityModel.EXAMPLE_1, SettingsBrandingColorSchemeModel.EXAMPLE_1, @@ -81,7 +83,7 @@ void testSetSettingsAppliesAllSubFields() { @Test void testSetSettingsSkipsNullSubFields() { - final ServiceResult result = confluenceSettingsService.setSettings(new SettingsModel()); + final ServiceResult result = settingsService.setSettings(new SettingsModel()); assertTrue(result.getStatus().isEmpty()); verify(settingsService, never()).setSettingsGeneral(SettingsGeneralModel.EXAMPLE_1); @@ -93,7 +95,7 @@ void testSetSettingsRecordsPerSubFieldFailure() { doThrow(new BadRequestException("invalid colour scheme")) .when(settingsBrandingService).setColourScheme(SettingsBrandingColorSchemeModel.EXAMPLE_1); - final ServiceResult result = confluenceSettingsService.setSettings(new SettingsModel( + final ServiceResult result = settingsService.setSettings(new SettingsModel( SettingsGeneralModel.EXAMPLE_1, null, SettingsBrandingColorSchemeModel.EXAMPLE_1, diff --git a/confluence/src/test/java/com/deftdevs/bootstrapi/confluence/service/SettingsServiceTest.java b/confluence/src/test/java/com/deftdevs/bootstrapi/confluence/service/SettingsServiceTest.java index c0315750..45fa08c0 100644 --- a/confluence/src/test/java/com/deftdevs/bootstrapi/confluence/service/SettingsServiceTest.java +++ b/confluence/src/test/java/com/deftdevs/bootstrapi/confluence/service/SettingsServiceTest.java @@ -26,11 +26,14 @@ class SettingsServiceTest { @Mock private GlobalSettingsManager globalSettingsManager; + @Mock + private com.deftdevs.bootstrapi.commons.service.api.SettingsBrandingService settingsBrandingService; + private SettingsServiceImpl settingsService; @BeforeEach public void setup() { - settingsService = new SettingsServiceImpl(globalSettingsManager); + settingsService = new SettingsServiceImpl(globalSettingsManager, settingsBrandingService); } @Test diff --git a/crowd/src/main/java/com/deftdevs/bootstrapi/crowd/config/ServiceConfig.java b/crowd/src/main/java/com/deftdevs/bootstrapi/crowd/config/ServiceConfig.java index 296440e0..cedb0db1 100644 --- a/crowd/src/main/java/com/deftdevs/bootstrapi/crowd/config/ServiceConfig.java +++ b/crowd/src/main/java/com/deftdevs/bootstrapi/crowd/config/ServiceConfig.java @@ -19,13 +19,8 @@ public class ServiceConfig { @Bean public _AllService<_AllModel> _allService() { - // The composite settings service is deliberately NOT a bean: its type extends - // CrowdSettingsGeneralService and CrowdSettingsBrandingService, so exposing it - // would make by-type injection of those interfaces ambiguous in the REST layer. return new _AllServiceImpl( - new CrowdSettingsServiceImpl( - crowdSettingsGeneralService(), - settingsBrandingService()), + crowdSettingsService(), directoriesService(), applicationsService(), applicationLinksService(), @@ -54,15 +49,15 @@ public ApplicationsService applicationsService() { } @Bean - public CrowdSettingsBrandingService settingsBrandingService() { - return new SettingsBrandingServiceImpl( - atlassianConfig.propertyManager()); - } - - @Bean - public CrowdSettingsGeneralService crowdSettingsGeneralService() { + public CrowdSettingsService crowdSettingsService() { + // The single settings bean implements the composite interface; the branding + // implementation is deliberately NOT its own bean, so by-type injection of + // CrowdSettingsGeneralService and CrowdSettingsBrandingService in the REST + // layer resolves unambiguously to this bean. return new SettingsServiceImpl( - atlassianConfig.propertyManager()); + atlassianConfig.propertyManager(), + new SettingsBrandingServiceImpl( + atlassianConfig.propertyManager())); } @Bean diff --git a/crowd/src/main/java/com/deftdevs/bootstrapi/crowd/service/CrowdSettingsServiceImpl.java b/crowd/src/main/java/com/deftdevs/bootstrapi/crowd/service/CrowdSettingsServiceImpl.java deleted file mode 100644 index 477e9f9b..00000000 --- a/crowd/src/main/java/com/deftdevs/bootstrapi/crowd/service/CrowdSettingsServiceImpl.java +++ /dev/null @@ -1,49 +0,0 @@ -package com.deftdevs.bootstrapi.crowd.service; - -import com.deftdevs.bootstrapi.commons.model.SettingsGeneralModel; -import com.deftdevs.bootstrapi.crowd.model.SettingsBrandingLoginPageModel; -import com.deftdevs.bootstrapi.crowd.service.api.CrowdSettingsBrandingService; -import com.deftdevs.bootstrapi.crowd.service.api.CrowdSettingsGeneralService; -import com.deftdevs.bootstrapi.crowd.service.api.CrowdSettingsService; - -import java.io.InputStream; - -public class CrowdSettingsServiceImpl implements CrowdSettingsService { - - private final CrowdSettingsGeneralService settingsService; - private final CrowdSettingsBrandingService settingsBrandingService; - - public CrowdSettingsServiceImpl( - final CrowdSettingsGeneralService settingsService, - final CrowdSettingsBrandingService settingsBrandingService) { - - this.settingsService = settingsService; - this.settingsBrandingService = settingsBrandingService; - } - - @Override - public SettingsGeneralModel getSettingsGeneral() { - return settingsService.getSettingsGeneral(); - } - - @Override - public SettingsGeneralModel setSettingsGeneral(final SettingsGeneralModel settingsGeneralModel) { - return settingsService.setSettingsGeneral(settingsGeneralModel); - } - - @Override - public SettingsBrandingLoginPageModel getLoginPage() { - return settingsBrandingService.getLoginPage(); - } - - @Override - public SettingsBrandingLoginPageModel setLoginPage(final SettingsBrandingLoginPageModel settingsBrandingLoginPageModel) { - return settingsBrandingService.setLoginPage(settingsBrandingLoginPageModel); - } - - @Override - public void setLogo(final InputStream inputStream) { - settingsBrandingService.setLogo(inputStream); - } - -} diff --git a/crowd/src/main/java/com/deftdevs/bootstrapi/crowd/service/SettingsServiceImpl.java b/crowd/src/main/java/com/deftdevs/bootstrapi/crowd/service/SettingsServiceImpl.java index a8cee89d..f32a0fd5 100644 --- a/crowd/src/main/java/com/deftdevs/bootstrapi/crowd/service/SettingsServiceImpl.java +++ b/crowd/src/main/java/com/deftdevs/bootstrapi/crowd/service/SettingsServiceImpl.java @@ -4,17 +4,23 @@ import com.atlassian.crowd.manager.property.PropertyManagerException; import com.deftdevs.bootstrapi.commons.exception.web.InternalServerErrorException; import com.deftdevs.bootstrapi.commons.model.SettingsGeneralModel; -import com.deftdevs.bootstrapi.crowd.service.api.CrowdSettingsGeneralService; +import com.deftdevs.bootstrapi.crowd.model.SettingsBrandingLoginPageModel; +import com.deftdevs.bootstrapi.crowd.service.api.CrowdSettingsBrandingService; +import com.deftdevs.bootstrapi.crowd.service.api.CrowdSettingsService; -public class SettingsServiceImpl - implements CrowdSettingsGeneralService { +import java.io.InputStream; + +public class SettingsServiceImpl implements CrowdSettingsService { private final PropertyManager propertyManager; + private final CrowdSettingsBrandingService settingsBrandingService; public SettingsServiceImpl( - final PropertyManager propertyManager) { + final PropertyManager propertyManager, + final CrowdSettingsBrandingService settingsBrandingService) { this.propertyManager = propertyManager; + this.settingsBrandingService = settingsBrandingService; } @Override @@ -40,4 +46,19 @@ public SettingsGeneralModel setSettingsGeneral(SettingsGeneralModel settingsMode return getSettingsGeneral(); } + @Override + public SettingsBrandingLoginPageModel getLoginPage() { + return settingsBrandingService.getLoginPage(); + } + + @Override + public SettingsBrandingLoginPageModel setLoginPage(final SettingsBrandingLoginPageModel settingsBrandingLoginPageModel) { + return settingsBrandingService.setLoginPage(settingsBrandingLoginPageModel); + } + + @Override + public void setLogo(final InputStream inputStream) { + settingsBrandingService.setLogo(inputStream); + } + } diff --git a/crowd/src/test/java/com/deftdevs/bootstrapi/crowd/service/CrowdSettingsServiceTest.java b/crowd/src/test/java/com/deftdevs/bootstrapi/crowd/service/CrowdSettingsServiceTest.java index 85e932e4..66580ac5 100644 --- a/crowd/src/test/java/com/deftdevs/bootstrapi/crowd/service/CrowdSettingsServiceTest.java +++ b/crowd/src/test/java/com/deftdevs/bootstrapi/crowd/service/CrowdSettingsServiceTest.java @@ -1,12 +1,12 @@ package com.deftdevs.bootstrapi.crowd.service; +import com.atlassian.crowd.manager.property.PropertyManager; import com.deftdevs.bootstrapi.commons.exception.web.BadRequestException; import com.deftdevs.bootstrapi.commons.model.SettingsGeneralModel; import com.deftdevs.bootstrapi.commons.model.type.ServiceResult; import com.deftdevs.bootstrapi.crowd.model.SettingsBrandingLoginPageModel; import com.deftdevs.bootstrapi.crowd.model.SettingsModel; import com.deftdevs.bootstrapi.crowd.service.api.CrowdSettingsBrandingService; -import com.deftdevs.bootstrapi.crowd.service.api.CrowdSettingsGeneralService; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -21,30 +21,31 @@ import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; @ExtendWith(MockitoExtension.class) class CrowdSettingsServiceTest { @Mock - private CrowdSettingsGeneralService settingsGeneralService; + private PropertyManager propertyManager; @Mock private CrowdSettingsBrandingService settingsBrandingService; - private CrowdSettingsServiceImpl crowdSettingsService; + private SettingsServiceImpl settingsService; @BeforeEach void setup() { - crowdSettingsService = new CrowdSettingsServiceImpl(settingsGeneralService, settingsBrandingService); + settingsService = spy(new SettingsServiceImpl(propertyManager, settingsBrandingService)); } @Test void testGetSettings() { - doReturn(SettingsGeneralModel.EXAMPLE_1).when(settingsGeneralService).getSettingsGeneral(); + doReturn(SettingsGeneralModel.EXAMPLE_1).when(settingsService).getSettingsGeneral(); doReturn(SettingsBrandingLoginPageModel.EXAMPLE_1).when(settingsBrandingService).getLoginPage(); - final SettingsModel settingsModel = crowdSettingsService.getSettings(); + final SettingsModel settingsModel = settingsService.getSettings(); assertEquals(SettingsGeneralModel.EXAMPLE_1, settingsModel.getGeneral()); assertEquals(SettingsBrandingLoginPageModel.EXAMPLE_1, settingsModel.getBranding()); @@ -52,10 +53,10 @@ void testGetSettings() { @Test void testSetSettingsAppliesAllSubFields() { - doReturn(SettingsGeneralModel.EXAMPLE_1).when(settingsGeneralService).setSettingsGeneral(SettingsGeneralModel.EXAMPLE_1); + doReturn(SettingsGeneralModel.EXAMPLE_1).when(settingsService).setSettingsGeneral(SettingsGeneralModel.EXAMPLE_1); doReturn(SettingsBrandingLoginPageModel.EXAMPLE_1).when(settingsBrandingService).setLoginPage(SettingsBrandingLoginPageModel.EXAMPLE_1); - final ServiceResult result = crowdSettingsService.setSettings(new SettingsModel( + final ServiceResult result = settingsService.setSettings(new SettingsModel( SettingsGeneralModel.EXAMPLE_1, SettingsBrandingLoginPageModel.EXAMPLE_1)); @@ -67,20 +68,20 @@ void testSetSettingsAppliesAllSubFields() { @Test void testSetSettingsSkipsNullSubFields() { - final ServiceResult result = crowdSettingsService.setSettings(new SettingsModel()); + final ServiceResult result = settingsService.setSettings(new SettingsModel()); assertTrue(result.getStatus().isEmpty()); - verify(settingsGeneralService, never()).setSettingsGeneral(SettingsGeneralModel.EXAMPLE_1); + verify(settingsService, never()).setSettingsGeneral(SettingsGeneralModel.EXAMPLE_1); verify(settingsBrandingService, never()).setLoginPage(SettingsBrandingLoginPageModel.EXAMPLE_1); } @Test void testSetSettingsRecordsPerSubFieldFailure() { - doReturn(SettingsGeneralModel.EXAMPLE_1).when(settingsGeneralService).setSettingsGeneral(SettingsGeneralModel.EXAMPLE_1); + doReturn(SettingsGeneralModel.EXAMPLE_1).when(settingsService).setSettingsGeneral(SettingsGeneralModel.EXAMPLE_1); doThrow(new BadRequestException("invalid login page")) .when(settingsBrandingService).setLoginPage(SettingsBrandingLoginPageModel.EXAMPLE_1); - final ServiceResult result = crowdSettingsService.setSettings(new SettingsModel( + final ServiceResult result = settingsService.setSettings(new SettingsModel( SettingsGeneralModel.EXAMPLE_1, SettingsBrandingLoginPageModel.EXAMPLE_1)); diff --git a/crowd/src/test/java/com/deftdevs/bootstrapi/crowd/service/SettingsServiceTest.java b/crowd/src/test/java/com/deftdevs/bootstrapi/crowd/service/SettingsServiceTest.java index c1af5d19..32350e43 100644 --- a/crowd/src/test/java/com/deftdevs/bootstrapi/crowd/service/SettingsServiceTest.java +++ b/crowd/src/test/java/com/deftdevs/bootstrapi/crowd/service/SettingsServiceTest.java @@ -23,13 +23,16 @@ public class SettingsServiceTest { @Mock private PropertyManager propertyManager; + @Mock + private com.deftdevs.bootstrapi.crowd.service.api.CrowdSettingsBrandingService settingsBrandingService; + public static final SettingsGeneralModel SETTINGS_MODEL = EXAMPLE_1_NO_MODE; private SettingsServiceImpl settingsService; @BeforeEach public void setup() { - settingsService = new SettingsServiceImpl(propertyManager); + settingsService = new SettingsServiceImpl(propertyManager, settingsBrandingService); } @Test From 6fb6389953fba591678ad9172bc7e51ac1229a35 Mon Sep 17 00:00:00 2001 From: Patrick Hobusch Date: Sun, 5 Jul 2026 21:54:42 +0200 Subject: [PATCH 06/18] Drop the authentication service and resource type parameters Every binding of the IdP and SSO type parameters across the authentication interfaces, abstract implementations and resources used the same two types, so the generics carried no information and forced unchecked casts in the composite default methods. With plain wildcard signatures the casts and their @SuppressWarnings disappear; defensive LinkedHashMap copies bridge the wildcard maps into the wire model. --- .../AbstractAuthenticationResourceImpl.java | 19 +++++----- .../rest/api/AuthenticationResource.java | 14 +++---- .../AbstractAuthenticationService.java | 37 +++++++++---------- .../service/api/AuthenticationService.java | 29 +++++++-------- .../rest/AuthenticationResourceTest.java | 2 +- .../impl/TestAuthenticationResourceImpl.java | 4 +- .../api/AuthenticationServiceTest.java | 2 +- .../rest/AuthenticationResourceImpl.java | 4 +- .../service/AuthenticationServiceImpl.java | 2 +- .../api/ConfluenceAuthenticationService.java | 4 +- .../jira/rest/AuthenticationResourceImpl.java | 4 +- .../service/AuthenticationServiceImpl.java | 2 +- .../api/JiraAuthenticationService.java | 4 +- 13 files changed, 57 insertions(+), 70 deletions(-) diff --git a/commons/src/main/java/com/deftdevs/bootstrapi/commons/rest/AbstractAuthenticationResourceImpl.java b/commons/src/main/java/com/deftdevs/bootstrapi/commons/rest/AbstractAuthenticationResourceImpl.java index cb4c3247..665568ef 100644 --- a/commons/src/main/java/com/deftdevs/bootstrapi/commons/rest/AbstractAuthenticationResourceImpl.java +++ b/commons/src/main/java/com/deftdevs/bootstrapi/commons/rest/AbstractAuthenticationResourceImpl.java @@ -7,37 +7,36 @@ import java.util.Map; -public abstract class AbstractAuthenticationResourceImpl> - implements AuthenticationResource { +public abstract class AbstractAuthenticationResourceImpl implements AuthenticationResource { - private final S authenticationService; + private final AuthenticationService authenticationService; protected AbstractAuthenticationResourceImpl( - final S authenticationService) { + final AuthenticationService authenticationService) { this.authenticationService = authenticationService; } @Override - public Map getAuthenticationIdps() { + public Map getAuthenticationIdps() { return authenticationService.getAuthenticationIdps(); } @Override - public Map setAuthenticationIdps( - final Map authenticationIdpModels) { + public Map setAuthenticationIdps( + final Map authenticationIdpModels) { return authenticationService.setAuthenticationIdps(authenticationIdpModels); } @Override - public SM getAuthenticationSso() { + public AuthenticationSsoModel getAuthenticationSso() { return authenticationService.getAuthenticationSso(); } @Override - public SM setAuthenticationSso( - final SM authenticationSsoModel) { + public AuthenticationSsoModel setAuthenticationSso( + final AuthenticationSsoModel authenticationSsoModel) { return authenticationService.setAuthenticationSso(authenticationSsoModel); } diff --git a/commons/src/main/java/com/deftdevs/bootstrapi/commons/rest/api/AuthenticationResource.java b/commons/src/main/java/com/deftdevs/bootstrapi/commons/rest/api/AuthenticationResource.java index a122265b..564ab83c 100644 --- a/commons/src/main/java/com/deftdevs/bootstrapi/commons/rest/api/AuthenticationResource.java +++ b/commons/src/main/java/com/deftdevs/bootstrapi/commons/rest/api/AuthenticationResource.java @@ -18,7 +18,7 @@ import javax.ws.rs.core.MediaType; import java.util.Map; -public interface AuthenticationResource { +public interface AuthenticationResource { @GET @Path(BootstrAPI.AUTHENTICATION_IDPS) @@ -36,7 +36,7 @@ public interface AuthenticationResource getAuthenticationIdps(); + Map getAuthenticationIdps(); @PATCH @Path(BootstrAPI.AUTHENTICATION_IDPS) @@ -55,8 +55,8 @@ public interface AuthenticationResource setAuthenticationIdps( - final Map authenticationIdpModels); + Map setAuthenticationIdps( + final Map authenticationIdpModels); @GET @Path(BootstrAPI.AUTHENTICATION_SSO) @@ -74,7 +74,7 @@ public interface AuthenticationResource - implements AuthenticationService { +public abstract class AbstractAuthenticationService implements AuthenticationService { protected final IdpConfigService idpConfigService; protected final SsoConfigService ssoConfigService; @@ -29,21 +28,21 @@ protected AbstractAuthenticationService( } @Override - public Map getAuthenticationIdps() { + public Map getAuthenticationIdps() { return idpConfigService.getIdpConfigs().stream() .map(this::toAuthenticationIdpModel) .collect(Collectors.toMap(AbstractAuthenticationIdpModel::getName, Function.identity())); } @Override - public Map setAuthenticationIdps( - final Map authenticationIdpModels) { + public Map setAuthenticationIdps( + final Map authenticationIdpModels) { - final Map resultIdpModels = new LinkedHashMap<>(); + final Map resultIdpModels = new LinkedHashMap<>(); - for (Map.Entry entry : authenticationIdpModels.entrySet()) { + for (Map.Entry entry : authenticationIdpModels.entrySet()) { final String idpName = entry.getKey(); - final IB idpModel = entry.getValue(); + final AbstractAuthenticationIdpModel idpModel = entry.getValue(); if (idpModel == null) { // declarative no-op: null model + existing entity → return as-is; @@ -59,7 +58,7 @@ protected AbstractAuthenticationService( idpModel.setName(idpName); } - final IB resultIdpModel = setAuthenticationIdp(idpModel); + final AbstractAuthenticationIdpModel resultIdpModel = setAuthenticationIdp(idpModel); resultIdpModels.put(resultIdpModel.getName(), resultIdpModel); } @@ -67,8 +66,8 @@ protected AbstractAuthenticationService( } @Override - public IB setAuthenticationIdp( - final IB authenticationIdpModel) { + public AbstractAuthenticationIdpModel setAuthenticationIdp( + final AbstractAuthenticationIdpModel authenticationIdpModel) { if (authenticationIdpModel.getName() == null || authenticationIdpModel.getName().trim().isEmpty()) { throw new BadRequestException("The name cannot be empty"); @@ -88,13 +87,13 @@ public IB setAuthenticationIdp( } @Override - public SB getAuthenticationSso() { + public AuthenticationSsoModel getAuthenticationSso() { return toAuthenticationSsoModel(ssoConfigService.getSsoConfig()); } @Override - public SB setAuthenticationSso( - final SB authenticationSsoModel) { + public AuthenticationSsoModel setAuthenticationSso( + final AuthenticationSsoModel authenticationSsoModel) { final SsoConfig existingSsoConfig = ssoConfigService.getSsoConfig(); final SsoConfig ssoConfig = toSsoConfig(authenticationSsoModel, existingSsoConfig); @@ -113,14 +112,14 @@ IdpConfig findIdpConfigByName( return idpConfigsByName.get(name); } - protected abstract IB toAuthenticationIdpModel(IdpConfig idpConfig); + protected abstract AbstractAuthenticationIdpModel toAuthenticationIdpModel(IdpConfig idpConfig); - protected abstract IdpConfig toIdpConfig(IB authenticationIdpModel); + protected abstract IdpConfig toIdpConfig(AbstractAuthenticationIdpModel authenticationIdpModel); - protected abstract IdpConfig toIdpConfig(IB authenticationIdpModel, IdpConfig existingIdpConfig); + protected abstract IdpConfig toIdpConfig(AbstractAuthenticationIdpModel authenticationIdpModel, IdpConfig existingIdpConfig); - protected abstract SB toAuthenticationSsoModel(SsoConfig ssoConfig); + protected abstract AuthenticationSsoModel toAuthenticationSsoModel(SsoConfig ssoConfig); - protected abstract SsoConfig toSsoConfig(SB authenticationSsoModel, SsoConfig existingSsoConfig); + protected abstract SsoConfig toSsoConfig(AuthenticationSsoModel authenticationSsoModel, SsoConfig existingSsoConfig); } diff --git a/commons/src/main/java/com/deftdevs/bootstrapi/commons/service/api/AuthenticationService.java b/commons/src/main/java/com/deftdevs/bootstrapi/commons/service/api/AuthenticationService.java index a248a904..0eb6eaaa 100644 --- a/commons/src/main/java/com/deftdevs/bootstrapi/commons/service/api/AuthenticationService.java +++ b/commons/src/main/java/com/deftdevs/bootstrapi/commons/service/api/AuthenticationService.java @@ -3,23 +3,21 @@ import com.deftdevs.bootstrapi.commons.model.AbstractAuthenticationIdpModel; import com.deftdevs.bootstrapi.commons.model.AuthenticationModel; import com.deftdevs.bootstrapi.commons.model.AuthenticationSsoModel; -import com.deftdevs.bootstrapi.commons.model.type._AllModelStatus; import com.deftdevs.bootstrapi.commons.model.type.ServiceResult; +import com.deftdevs.bootstrapi.commons.model.type._AllModelStatus; import com.deftdevs.bootstrapi.commons.util.ServiceResultUtil; import java.util.LinkedHashMap; import java.util.Map; -public interface AuthenticationService { +public interface AuthenticationService { - @SuppressWarnings("unchecked") default AuthenticationModel getAuthentication() { return new AuthenticationModel( - (Map) getAuthenticationIdps(), + new LinkedHashMap<>(getAuthenticationIdps()), getAuthenticationSso()); } - @SuppressWarnings("unchecked") default ServiceResult setAuthentication(final AuthenticationModel authenticationModel) { final AuthenticationModel result = new AuthenticationModel(); final Map status = new LinkedHashMap<>(); @@ -27,8 +25,7 @@ default ServiceResult setAuthentication(final Authenticatio if (authenticationModel.getIdps() != null) { final String key = ServiceResultUtil.subEntityKey(AbstractAuthenticationIdpModel.class); try { - result.setIdps((Map) setAuthenticationIdps( - (Map) authenticationModel.getIdps())); + result.setIdps(new LinkedHashMap<>(setAuthenticationIdps(authenticationModel.getIdps()))); status.put(key, _AllModelStatus.success()); } catch (Exception e) { status.put(key, ServiceResultUtil.toErrorStatus(key, e)); @@ -37,7 +34,7 @@ default ServiceResult setAuthentication(final Authenticatio if (authenticationModel.getSso() != null) { final String key = ServiceResultUtil.subEntityKey(AuthenticationSsoModel.class); try { - result.setSso(setAuthenticationSso((SB) authenticationModel.getSso())); + result.setSso(setAuthenticationSso(authenticationModel.getSso())); status.put(key, _AllModelStatus.success()); } catch (Exception e) { status.put(key, ServiceResultUtil.toErrorStatus(key, e)); @@ -46,17 +43,17 @@ default ServiceResult setAuthentication(final Authenticatio return new ServiceResult<>(result, status); } - Map getAuthenticationIdps(); + Map getAuthenticationIdps(); - Map setAuthenticationIdps( - Map authenticationIdpModels); + Map setAuthenticationIdps( + Map authenticationIdpModels); - IB setAuthenticationIdp( - IB authenticationIdpModel); + AbstractAuthenticationIdpModel setAuthenticationIdp( + AbstractAuthenticationIdpModel authenticationIdpModel); - SB getAuthenticationSso(); + AuthenticationSsoModel getAuthenticationSso(); - SB setAuthenticationSso( - SB authenticationSsoModel); + AuthenticationSsoModel setAuthenticationSso( + AuthenticationSsoModel authenticationSsoModel); } diff --git a/commons/src/test/java/com/deftdevs/bootstrapi/commons/rest/AuthenticationResourceTest.java b/commons/src/test/java/com/deftdevs/bootstrapi/commons/rest/AuthenticationResourceTest.java index cd091657..ad27b9e3 100644 --- a/commons/src/test/java/com/deftdevs/bootstrapi/commons/rest/AuthenticationResourceTest.java +++ b/commons/src/test/java/com/deftdevs/bootstrapi/commons/rest/AuthenticationResourceTest.java @@ -21,7 +21,7 @@ class AuthenticationResourceTest { @Mock - private AuthenticationService authenticationService; + private AuthenticationService authenticationService; private TestAuthenticationResourceImpl resource; diff --git a/commons/src/test/java/com/deftdevs/bootstrapi/commons/rest/impl/TestAuthenticationResourceImpl.java b/commons/src/test/java/com/deftdevs/bootstrapi/commons/rest/impl/TestAuthenticationResourceImpl.java index 3fdb67d0..fd5d56da 100644 --- a/commons/src/test/java/com/deftdevs/bootstrapi/commons/rest/impl/TestAuthenticationResourceImpl.java +++ b/commons/src/test/java/com/deftdevs/bootstrapi/commons/rest/impl/TestAuthenticationResourceImpl.java @@ -5,10 +5,10 @@ import com.deftdevs.bootstrapi.commons.rest.AbstractAuthenticationResourceImpl; import com.deftdevs.bootstrapi.commons.service.api.AuthenticationService; -public class TestAuthenticationResourceImpl extends AbstractAuthenticationResourceImpl> { +public class TestAuthenticationResourceImpl extends AbstractAuthenticationResourceImpl { public TestAuthenticationResourceImpl( - AuthenticationService authenticationService) { + AuthenticationService authenticationService) { super(authenticationService); } diff --git a/commons/src/test/java/com/deftdevs/bootstrapi/commons/service/api/AuthenticationServiceTest.java b/commons/src/test/java/com/deftdevs/bootstrapi/commons/service/api/AuthenticationServiceTest.java index 050052df..1bc700a8 100644 --- a/commons/src/test/java/com/deftdevs/bootstrapi/commons/service/api/AuthenticationServiceTest.java +++ b/commons/src/test/java/com/deftdevs/bootstrapi/commons/service/api/AuthenticationServiceTest.java @@ -22,7 +22,7 @@ class AuthenticationServiceTest { - private AuthenticationService authenticationService; + private AuthenticationService authenticationService; private AbstractAuthenticationIdpModel idpModel; private AuthenticationSsoModel ssoModel; diff --git a/confluence/src/main/java/com/deftdevs/bootstrapi/confluence/rest/AuthenticationResourceImpl.java b/confluence/src/main/java/com/deftdevs/bootstrapi/confluence/rest/AuthenticationResourceImpl.java index 25da5d2d..f012982a 100644 --- a/confluence/src/main/java/com/deftdevs/bootstrapi/confluence/rest/AuthenticationResourceImpl.java +++ b/confluence/src/main/java/com/deftdevs/bootstrapi/confluence/rest/AuthenticationResourceImpl.java @@ -2,8 +2,6 @@ import com.atlassian.plugins.rest.api.security.annotation.SystemAdminOnly; import com.deftdevs.bootstrapi.commons.constants.BootstrAPI; -import com.deftdevs.bootstrapi.commons.model.AbstractAuthenticationIdpModel; -import com.deftdevs.bootstrapi.commons.model.AuthenticationSsoModel; import com.deftdevs.bootstrapi.commons.rest.AbstractAuthenticationResourceImpl; import com.deftdevs.bootstrapi.confluence.service.api.ConfluenceAuthenticationService; @@ -12,7 +10,7 @@ @Path(BootstrAPI.AUTHENTICATION) @SystemAdminOnly -public class AuthenticationResourceImpl extends AbstractAuthenticationResourceImpl { +public class AuthenticationResourceImpl extends AbstractAuthenticationResourceImpl { @Inject public AuthenticationResourceImpl( diff --git a/confluence/src/main/java/com/deftdevs/bootstrapi/confluence/service/AuthenticationServiceImpl.java b/confluence/src/main/java/com/deftdevs/bootstrapi/confluence/service/AuthenticationServiceImpl.java index 66462a14..7279d6f5 100644 --- a/confluence/src/main/java/com/deftdevs/bootstrapi/confluence/service/AuthenticationServiceImpl.java +++ b/confluence/src/main/java/com/deftdevs/bootstrapi/confluence/service/AuthenticationServiceImpl.java @@ -12,7 +12,7 @@ import com.deftdevs.bootstrapi.confluence.service.api.ConfluenceAuthenticationService; public class AuthenticationServiceImpl - extends AbstractAuthenticationService + extends AbstractAuthenticationService implements ConfluenceAuthenticationService { public AuthenticationServiceImpl( diff --git a/confluence/src/main/java/com/deftdevs/bootstrapi/confluence/service/api/ConfluenceAuthenticationService.java b/confluence/src/main/java/com/deftdevs/bootstrapi/confluence/service/api/ConfluenceAuthenticationService.java index c548b34e..66771481 100644 --- a/confluence/src/main/java/com/deftdevs/bootstrapi/confluence/service/api/ConfluenceAuthenticationService.java +++ b/confluence/src/main/java/com/deftdevs/bootstrapi/confluence/service/api/ConfluenceAuthenticationService.java @@ -1,10 +1,8 @@ package com.deftdevs.bootstrapi.confluence.service.api; -import com.deftdevs.bootstrapi.commons.model.AbstractAuthenticationIdpModel; -import com.deftdevs.bootstrapi.commons.model.AuthenticationSsoModel; import com.deftdevs.bootstrapi.commons.service.api.AuthenticationService; public interface ConfluenceAuthenticationService extends - AuthenticationService { + AuthenticationService { } diff --git a/jira/src/main/java/com/deftdevs/bootstrapi/jira/rest/AuthenticationResourceImpl.java b/jira/src/main/java/com/deftdevs/bootstrapi/jira/rest/AuthenticationResourceImpl.java index 76a31824..fe1fc850 100644 --- a/jira/src/main/java/com/deftdevs/bootstrapi/jira/rest/AuthenticationResourceImpl.java +++ b/jira/src/main/java/com/deftdevs/bootstrapi/jira/rest/AuthenticationResourceImpl.java @@ -2,8 +2,6 @@ import com.atlassian.plugins.rest.api.security.annotation.SystemAdminOnly; import com.deftdevs.bootstrapi.commons.constants.BootstrAPI; -import com.deftdevs.bootstrapi.commons.model.AbstractAuthenticationIdpModel; -import com.deftdevs.bootstrapi.commons.model.AuthenticationSsoModel; import com.deftdevs.bootstrapi.commons.rest.AbstractAuthenticationResourceImpl; import com.deftdevs.bootstrapi.jira.service.api.JiraAuthenticationService; @@ -12,7 +10,7 @@ @Path(BootstrAPI.AUTHENTICATION) @SystemAdminOnly -public class AuthenticationResourceImpl extends AbstractAuthenticationResourceImpl { +public class AuthenticationResourceImpl extends AbstractAuthenticationResourceImpl { @Inject public AuthenticationResourceImpl( diff --git a/jira/src/main/java/com/deftdevs/bootstrapi/jira/service/AuthenticationServiceImpl.java b/jira/src/main/java/com/deftdevs/bootstrapi/jira/service/AuthenticationServiceImpl.java index 9ce75727..27a4a3c3 100644 --- a/jira/src/main/java/com/deftdevs/bootstrapi/jira/service/AuthenticationServiceImpl.java +++ b/jira/src/main/java/com/deftdevs/bootstrapi/jira/service/AuthenticationServiceImpl.java @@ -12,7 +12,7 @@ import com.deftdevs.bootstrapi.jira.service.api.JiraAuthenticationService; public class AuthenticationServiceImpl - extends AbstractAuthenticationService + extends AbstractAuthenticationService implements JiraAuthenticationService { public AuthenticationServiceImpl( diff --git a/jira/src/main/java/com/deftdevs/bootstrapi/jira/service/api/JiraAuthenticationService.java b/jira/src/main/java/com/deftdevs/bootstrapi/jira/service/api/JiraAuthenticationService.java index c4c733ab..f07762d6 100644 --- a/jira/src/main/java/com/deftdevs/bootstrapi/jira/service/api/JiraAuthenticationService.java +++ b/jira/src/main/java/com/deftdevs/bootstrapi/jira/service/api/JiraAuthenticationService.java @@ -1,10 +1,8 @@ package com.deftdevs.bootstrapi.jira.service.api; -import com.deftdevs.bootstrapi.commons.model.AbstractAuthenticationIdpModel; -import com.deftdevs.bootstrapi.commons.model.AuthenticationSsoModel; import com.deftdevs.bootstrapi.commons.service.api.AuthenticationService; public interface JiraAuthenticationService extends - AuthenticationService { + AuthenticationService { } From acf33a84ef0ac08740c9767fbb4b5863797edb5f Mon Sep 17 00:00:00 2001 From: Patrick Hobusch Date: Sun, 5 Jul 2026 22:01:54 +0200 Subject: [PATCH 07/18] Key sub-field statuses by the request's JSON field paths Status-map keys previously mirrored REST path segments and were derived from @XmlRootElement names by dash-splitting, which forced root-element renames, a group-aware special case for settings/custom-html and hardcoded key strings in tests. They now mirror the JSON structure of the request instead: composite services record their sub-fields under the field's own name (general, smtp, idps) via a shared ServiceResultUtil.setSubEntity helper that also removes the duplicated try/catch blocks, and the _all service prefixes them with the composite's field name when merging (settings/general, mailServer/smtp). Top-level fields are keyed by their field name (permissionsGlobal, applicationLinks). No layer needs to know its parents, the dash-splitting utilities and the questioned root-element changes are gone, and a status key always matches the part of the payload it refers to. --- .../commons/constants/BootstrAPI.java | 3 +- .../model/AbstractAuthenticationIdpModel.java | 2 +- .../SettingsBrandingColorSchemeModel.java | 2 +- .../service/_AbstractAllServiceImpl.java | 41 +++++---- .../service/api/AuthenticationService.java | 23 ++--- .../service/api/MailServerService.java | 25 ++---- .../commons/util/ServiceResultUtil.java | 86 ++++++------------- .../service/_AbstractAllServiceImplTest.java | 49 +++++------ .../api/AuthenticationServiceTest.java | 8 +- .../service/api/MailServerServiceTest.java | 8 +- .../commons/util/ServiceResultUtilTest.java | 86 ++++++------------- .../rest/Abstract_AllResourceFuncTest.java | 4 +- confluence/Apis/AllApi.md | 2 +- .../confluence/service/_AllServiceImpl.java | 19 ++-- .../api/ConfluenceSettingsService.java | 49 +++-------- .../confluence/rest/_AllResourceTest.java | 6 +- .../ConfluenceSettingsServiceTest.java | 12 +-- .../service/_AllServiceImplTest.java | 14 +-- .../confluence/rest/_AllResourceFuncTest.java | 9 +- crowd/Apis/AllApi.md | 2 +- .../crowd/service/_AllServiceImpl.java | 26 ++---- .../service/api/CrowdSettingsService.java | 27 ++---- .../crowd/rest/_AllResourceTest.java | 6 +- .../service/CrowdSettingsServiceTest.java | 8 +- .../crowd/service/_AllServiceImplTest.java | 16 ++-- .../crowd/rest/_AllResourceFuncTest.java | 12 ++- jira/Apis/AllApi.md | 2 +- .../jira/service/_AllServiceImpl.java | 19 ++-- .../jira/service/api/JiraSettingsService.java | 36 ++------ .../jira/rest/_AllResourceTest.java | 6 +- .../jira/service/JiraSettingsServiceTest.java | 10 +-- .../jira/service/_AllServiceImplTest.java | 14 +-- .../jira/rest/_AllResourceFuncTest.java | 10 +-- 33 files changed, 234 insertions(+), 408 deletions(-) diff --git a/commons/src/main/java/com/deftdevs/bootstrapi/commons/constants/BootstrAPI.java b/commons/src/main/java/com/deftdevs/bootstrapi/commons/constants/BootstrAPI.java index 8792b473..f0dce14e 100644 --- a/commons/src/main/java/com/deftdevs/bootstrapi/commons/constants/BootstrAPI.java +++ b/commons/src/main/java/com/deftdevs/bootstrapi/commons/constants/BootstrAPI.java @@ -65,7 +65,8 @@ public class BootstrAPI { public static final String ERROR_COLLECTION_RESPONSE_DESCRIPTION = "Returns a list of error messages."; public static final String _ALL_PUT_SUMMARY = "Apply a complete configuration"; public static final String _ALL_PUT_RESPONSE_DESCRIPTION = "Returns the updated configuration. The per-sub-field outcome is reported in the" - + " 'status' map (2xx for success, 4xx/5xx for failure with a human-readable 'message' and optional 'details')." + + " 'status' map, keyed by the request's field paths (e.g. 'settings/general', 'mailServer/smtp';" + + " 2xx for success, 4xx/5xx for failure with a human-readable 'message' and optional 'details')." + " License keys in the response are redacted (e.g. 'AAAB...wxyz#a1b2')."; public static final String _ALL_PUT_PARTIAL_RESPONSE_DESCRIPTION = "Configuration applied partially. Inspect the per-sub-field 'status' map" + " in the response body to see which fields succeeded and which failed."; diff --git a/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/AbstractAuthenticationIdpModel.java b/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/AbstractAuthenticationIdpModel.java index 86ee05b1..ba5828d8 100644 --- a/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/AbstractAuthenticationIdpModel.java +++ b/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/AbstractAuthenticationIdpModel.java @@ -17,7 +17,7 @@ @SuperBuilder @NoArgsConstructor @AllArgsConstructor -@XmlRootElement(name = "authentication-idps") +@XmlRootElement @JsonTypeInfo( use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, diff --git a/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/SettingsBrandingColorSchemeModel.java b/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/SettingsBrandingColorSchemeModel.java index 66740ab0..29fe6548 100644 --- a/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/SettingsBrandingColorSchemeModel.java +++ b/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/SettingsBrandingColorSchemeModel.java @@ -16,7 +16,7 @@ @Builder @NoArgsConstructor @AllArgsConstructor -@XmlRootElement(name = SETTINGS + "-" + SETTINGS_BRANDING) +@XmlRootElement(name = SETTINGS + "-" + SETTINGS_BRANDING + "-" + COLOR_SCHEME) @XmlAccessorType(XmlAccessType.FIELD) public class SettingsBrandingColorSchemeModel { diff --git a/commons/src/main/java/com/deftdevs/bootstrapi/commons/service/_AbstractAllServiceImpl.java b/commons/src/main/java/com/deftdevs/bootstrapi/commons/service/_AbstractAllServiceImpl.java index 1f6c938c..addd018c 100644 --- a/commons/src/main/java/com/deftdevs/bootstrapi/commons/service/_AbstractAllServiceImpl.java +++ b/commons/src/main/java/com/deftdevs/bootstrapi/commons/service/_AbstractAllServiceImpl.java @@ -1,18 +1,27 @@ package com.deftdevs.bootstrapi.commons.service; +import com.deftdevs.bootstrapi.commons.model.type.ServiceResult; import com.deftdevs.bootstrapi.commons.model.type._AllModelAccessor; import com.deftdevs.bootstrapi.commons.model.type._AllModelStatus; -import com.deftdevs.bootstrapi.commons.model.type.ServiceResult; import com.deftdevs.bootstrapi.commons.service.api._AllService; import com.deftdevs.bootstrapi.commons.util.ServiceResultUtil; +import java.util.LinkedHashMap; import java.util.Map; import java.util.function.Consumer; import java.util.function.Function; +/** + * Base class for the product _all services. Status-map keys mirror the JSON + * structure of the request: top-level fields are recorded under their field + * name ({@code directories}, {@code permissionsGlobal}), and the sub-field + * statuses returned by composite services are prefixed with the composite's + * field name ({@code settings/general}, {@code mailServer/smtp}). + */ public abstract class _AbstractAllServiceImpl<_AllModel extends _AllModelAccessor> implements _AllService<_AllModel> { protected void setEntityWithStatus( + final String field, final I input, final Function> updateFunction, final Consumer resultConsumer, @@ -25,20 +34,19 @@ protected void setEntityWithStatus( try { final ServiceResult serviceResult = updateFunction.apply(input); resultConsumer.accept(serviceResult.getModel()); - statusMap.putAll(serviceResult.getStatus()); + serviceResult.getStatus().forEach( + (subField, status) -> statusMap.put(field + "/" + subField, status)); } catch (Exception e) { // The composite updateFunction normally produces fine-grained // sub-field status entries itself. If it threw before doing so - // (e.g. NPE on a sub-field accessor, or a cast failure on a - // generic parameter), attribute the failure to the top-level - // group derived from the input model's @XmlRootElement name. - final String fallbackKey = ServiceResultUtil.entityType(input.getClass()); - statusMap.put(fallbackKey, ServiceResultUtil.toErrorStatus(fallbackKey, e)); + // (e.g. NPE on a sub-field accessor), attribute the failure to + // the composite's field as a whole. + statusMap.put(field, ServiceResultUtil.toErrorStatus(field, e)); } } protected _AllModelStatus setEntity( - final String entityType, + final String field, final I input, final Function updateFunction, final Consumer resultConsumer, @@ -52,18 +60,17 @@ protected _AllModelStatus setEntity( final O updatedEntity = updateFunction.apply(input); resultConsumer.accept(updatedEntity); final _AllModelStatus status = _AllModelStatus.success(); - statusMap.put(entityType, status); + statusMap.put(field, status); return status; } catch (Exception e) { - final _AllModelStatus status = ServiceResultUtil.toErrorStatus(entityType, e); - statusMap.put(entityType, status); + final _AllModelStatus status = ServiceResultUtil.toErrorStatus(field, e); + statusMap.put(field, status); return status; } } - @SuppressWarnings("unchecked") protected _AllModelStatus setEntities( - final String entityType, + final String field, final Map entityMap, final Function, Map> updateFunction, final Consumer> resultConsumer, @@ -75,13 +82,13 @@ protected _AllModelStatus setEntities( try { final Map updatedEntities = updateFunction.apply(entityMap); - resultConsumer.accept((Map) updatedEntities); + resultConsumer.accept(new LinkedHashMap<>(updatedEntities)); final _AllModelStatus status = _AllModelStatus.success(); - statusMap.put(entityType, status); + statusMap.put(field, status); return status; } catch (Exception e) { - final _AllModelStatus status = ServiceResultUtil.toErrorStatus(entityType, e); - statusMap.put(entityType, status); + final _AllModelStatus status = ServiceResultUtil.toErrorStatus(field, e); + statusMap.put(field, status); return status; } } diff --git a/commons/src/main/java/com/deftdevs/bootstrapi/commons/service/api/AuthenticationService.java b/commons/src/main/java/com/deftdevs/bootstrapi/commons/service/api/AuthenticationService.java index 0eb6eaaa..81df15c6 100644 --- a/commons/src/main/java/com/deftdevs/bootstrapi/commons/service/api/AuthenticationService.java +++ b/commons/src/main/java/com/deftdevs/bootstrapi/commons/service/api/AuthenticationService.java @@ -22,24 +22,11 @@ default ServiceResult setAuthentication(final Authenticatio final AuthenticationModel result = new AuthenticationModel(); final Map status = new LinkedHashMap<>(); - if (authenticationModel.getIdps() != null) { - final String key = ServiceResultUtil.subEntityKey(AbstractAuthenticationIdpModel.class); - try { - result.setIdps(new LinkedHashMap<>(setAuthenticationIdps(authenticationModel.getIdps()))); - status.put(key, _AllModelStatus.success()); - } catch (Exception e) { - status.put(key, ServiceResultUtil.toErrorStatus(key, e)); - } - } - if (authenticationModel.getSso() != null) { - final String key = ServiceResultUtil.subEntityKey(AuthenticationSsoModel.class); - try { - result.setSso(setAuthenticationSso(authenticationModel.getSso())); - status.put(key, _AllModelStatus.success()); - } catch (Exception e) { - status.put(key, ServiceResultUtil.toErrorStatus(key, e)); - } - } + ServiceResultUtil.setSubEntity(status, "idps", authenticationModel.getIdps(), + idps -> new LinkedHashMap<>(setAuthenticationIdps(idps)), result::setIdps); + ServiceResultUtil.setSubEntity(status, "sso", authenticationModel.getSso(), + this::setAuthenticationSso, result::setSso); + return new ServiceResult<>(result, status); } diff --git a/commons/src/main/java/com/deftdevs/bootstrapi/commons/service/api/MailServerService.java b/commons/src/main/java/com/deftdevs/bootstrapi/commons/service/api/MailServerService.java index 160fcfbd..6bee120c 100644 --- a/commons/src/main/java/com/deftdevs/bootstrapi/commons/service/api/MailServerService.java +++ b/commons/src/main/java/com/deftdevs/bootstrapi/commons/service/api/MailServerService.java @@ -3,8 +3,8 @@ import com.deftdevs.bootstrapi.commons.model.MailServerModel; import com.deftdevs.bootstrapi.commons.model.MailServerPopModel; import com.deftdevs.bootstrapi.commons.model.MailServerSmtpModel; -import com.deftdevs.bootstrapi.commons.model.type._AllModelStatus; import com.deftdevs.bootstrapi.commons.model.type.ServiceResult; +import com.deftdevs.bootstrapi.commons.model.type._AllModelStatus; import com.deftdevs.bootstrapi.commons.util.ServiceResultUtil; import java.util.LinkedHashMap; @@ -20,24 +20,11 @@ default ServiceResult setMailServer(final MailServerModel mailS final MailServerModel result = new MailServerModel(); final Map status = new LinkedHashMap<>(); - if (mailServerModel.getSmtp() != null) { - final String key = ServiceResultUtil.subEntityKey(MailServerSmtpModel.class); - try { - result.setSmtp(setMailServerSmtp(mailServerModel.getSmtp())); - status.put(key, _AllModelStatus.success()); - } catch (Exception e) { - status.put(key, ServiceResultUtil.toErrorStatus(key, e)); - } - } - if (mailServerModel.getPop() != null) { - final String key = ServiceResultUtil.subEntityKey(MailServerPopModel.class); - try { - result.setPop(setMailServerPop(mailServerModel.getPop())); - status.put(key, _AllModelStatus.success()); - } catch (Exception e) { - status.put(key, ServiceResultUtil.toErrorStatus(key, e)); - } - } + ServiceResultUtil.setSubEntity(status, "smtp", mailServerModel.getSmtp(), + this::setMailServerSmtp, result::setSmtp); + ServiceResultUtil.setSubEntity(status, "pop", mailServerModel.getPop(), + this::setMailServerPop, result::setPop); + return new ServiceResult<>(result, status); } diff --git a/commons/src/main/java/com/deftdevs/bootstrapi/commons/util/ServiceResultUtil.java b/commons/src/main/java/com/deftdevs/bootstrapi/commons/util/ServiceResultUtil.java index 24d1f9e0..ea4de72d 100644 --- a/commons/src/main/java/com/deftdevs/bootstrapi/commons/util/ServiceResultUtil.java +++ b/commons/src/main/java/com/deftdevs/bootstrapi/commons/util/ServiceResultUtil.java @@ -6,76 +6,44 @@ import javax.ws.rs.WebApplicationException; import javax.ws.rs.core.Response; -import javax.xml.bind.annotation.XmlRootElement; +import java.util.Map; +import java.util.function.Consumer; +import java.util.function.UnaryOperator; public class ServiceResultUtil { private static final Logger log = LoggerFactory.getLogger(ServiceResultUtil.class); /** - * Returns the raw {@code @XmlRootElement(name = ...)} of a model class. - * Used as the top-level status-map key for non-composite entries - * (e.g. {@code mail-templates}, {@code session-config}). Dashes in the - * name are preserved — composite "group/sub" keys are produced by the - * {@link #subEntityKey(Class)} overload instead. + * Applies one sub-field of a composite update and records its outcome in + * the status map: a success entry when the update function returns, or an + * error entry (via {@link #toErrorStatus(String, Exception)}) when it + * throws. Does nothing when {@code input} is null, i.e. when the field was + * not part of the request. + *

+ * The key is the sub-field's name within the composite's model, so that + * status keys line up with the JSON structure of the request. Composites + * record their sub-fields under local keys ({@code general}, {@code smtp}); + * the _all endpoint prefixes them with the composite's own field name when + * merging ({@code settings/general}, {@code mailServer/smtp}). */ - public static String entityType(final Class entityClass) { - final XmlRootElement rootElement = entityClass.getAnnotation(XmlRootElement.class); - if (rootElement == null || "##default".equals(rootElement.name())) { - throw new IllegalArgumentException( - entityClass.getName() + " must have @XmlRootElement with explicit name"); - } - return rootElement.name(); - } + public static void setSubEntity( + final Map statusMap, + final String key, + final T input, + final UnaryOperator updateFunction, + final Consumer resultConsumer) { - /** - * Returns the status-map key for a sub-field of a composite group, - * derived from the leaf model's {@code @XmlRootElement(name = ...)} - * by replacing the last dash with a slash. The convention assumes - * sub-field models are named {@code -}, where the group - * itself may contain dashes but the sub-name may not - * (e.g. {@code mail-server-smtp} → {@code mail-server/smtp}). - * Single-dash names map straightforwardly - * (e.g. {@code settings-general} → {@code settings/general}, - * {@code authentication-idps} → {@code authentication/idps}, - * {@code permissions-global} → {@code permissions/global}). - * If the sub-name itself contains dashes (e.g. {@code settings-custom-html}), - * use {@link #subEntityKey(Class, Class)} instead. - * - * @throws IllegalArgumentException if the model name contains no dash - * (i.e. the model is not a sub-field model — use {@link #entityType(Class)} instead) - */ - public static String subEntityKey(final Class entityClass) { - final String name = entityType(entityClass); - final int dash = name.lastIndexOf('-'); - if (dash < 0) { - throw new IllegalArgumentException( - entityClass.getName() + " is not a sub-field model: name '" + name - + "' has no dash separator"); + if (input == null) { + return; } - return name.substring(0, dash) + "/" + name.substring(dash + 1); - } - /** - * Returns the status-map key {@code /} for a sub-field model - * whose {@code @XmlRootElement} name is prefixed by the group model's - * {@code @XmlRootElement} name. Unlike {@link #subEntityKey(Class)}, this - * works for sub-names that themselves contain dashes - * (e.g. group {@code settings} + leaf {@code settings-custom-html} - * → {@code settings/custom-html}). - * - * @throws IllegalArgumentException if the leaf model's name does not - * start with the group model's name followed by a dash - */ - public static String subEntityKey(final Class groupClass, final Class entityClass) { - final String groupName = entityType(groupClass); - final String name = entityType(entityClass); - if (!name.startsWith(groupName + "-")) { - throw new IllegalArgumentException( - entityClass.getName() + " is not a sub-field model of group '" + groupName - + "': name '" + name + "' does not start with '" + groupName + "-'"); + try { + resultConsumer.accept(updateFunction.apply(input)); + statusMap.put(key, _AllModelStatus.success()); + } catch (Exception e) { + statusMap.put(key, toErrorStatus(key, e)); } - return groupName + "/" + name.substring(groupName.length() + 1); } public static _AllModelStatus toErrorStatus(final String entityType, final Exception e) { diff --git a/commons/src/test/java/com/deftdevs/bootstrapi/commons/service/_AbstractAllServiceImplTest.java b/commons/src/test/java/com/deftdevs/bootstrapi/commons/service/_AbstractAllServiceImplTest.java index 0668496e..ace4db68 100644 --- a/commons/src/test/java/com/deftdevs/bootstrapi/commons/service/_AbstractAllServiceImplTest.java +++ b/commons/src/test/java/com/deftdevs/bootstrapi/commons/service/_AbstractAllServiceImplTest.java @@ -1,6 +1,5 @@ package com.deftdevs.bootstrapi.commons.service; -import com.deftdevs.bootstrapi.commons.model.SettingsGeneralModel; import com.deftdevs.bootstrapi.commons.model.type.ServiceResult; import com.deftdevs.bootstrapi.commons.model.type._AllModelAccessor; import com.deftdevs.bootstrapi.commons.model.type._AllModelStatus; @@ -47,7 +46,7 @@ public TestAllModel setAll(final TestAllModel allModel) { void setEntitySkipsNullInput() { final Map statusMap = new HashMap<>(); - final _AllModelStatus status = service.setEntity("entity", null, + final _AllModelStatus status = service.setEntity("permissionsGlobal", null, input -> input, result -> {}, statusMap); assertNull(status); @@ -59,34 +58,34 @@ void setEntitySuccessRecordsStatusAndResult() { final Map statusMap = new HashMap<>(); final AtomicReference result = new AtomicReference<>(); - final _AllModelStatus status = service.setEntity("entity", "input", + final _AllModelStatus status = service.setEntity("permissionsGlobal", "input", input -> input + "-updated", result::set, statusMap); assertEquals(Response.Status.OK.getStatusCode(), status.getStatus()); assertEquals("input-updated", result.get()); - assertSame(status, statusMap.get("entity")); + assertSame(status, statusMap.get("permissionsGlobal")); } @Test void setEntityUsesWebApplicationExceptionStatusCode() { final Map statusMap = new HashMap<>(); - final _AllModelStatus status = service.setEntity("entity", "input", + final _AllModelStatus status = service.setEntity("permissionsGlobal", "input", input -> { throw new WebApplicationException("conflict", Response.Status.CONFLICT); }, result -> {}, statusMap); assertEquals(Response.Status.CONFLICT.getStatusCode(), status.getStatus()); - assertEquals("Failed to apply entity configuration", status.getMessage()); - assertSame(status, statusMap.get("entity")); + assertEquals("Failed to apply permissionsGlobal configuration", status.getMessage()); + assertSame(status, statusMap.get("permissionsGlobal")); } @Test void setEntityMapsUnexpectedExceptionTo500WithoutDetails() { final Map statusMap = new HashMap<>(); - final _AllModelStatus status = service.setEntity("entity", "input", + final _AllModelStatus status = service.setEntity("permissionsGlobal", "input", input -> { throw new IllegalStateException("internal details that must not leak"); }, @@ -102,9 +101,9 @@ void setEntityMapsUnexpectedExceptionTo500WithoutDetails() { void setEntitiesSkipsNullAndEmptyMap() { final Map statusMap = new HashMap<>(); - assertNull(service.setEntities("entities", null, + assertNull(service.setEntities("directories", null, entities -> entities, result -> {}, statusMap)); - assertNull(service.setEntities("entities", Collections.emptyMap(), + assertNull(service.setEntities("directories", Collections.emptyMap(), entities -> entities, result -> {}, statusMap)); assertTrue(statusMap.isEmpty()); } @@ -115,12 +114,12 @@ void setEntitiesSuccessRecordsStatusAndResult() { final AtomicReference> result = new AtomicReference<>(); final Map entities = Collections.singletonMap("key", "value"); - final _AllModelStatus status = service.setEntities("entities", entities, + final _AllModelStatus status = service.setEntities("directories", entities, input -> input, result::set, statusMap); assertEquals(Response.Status.OK.getStatusCode(), status.getStatus()); assertEquals(entities, result.get()); - assertSame(status, statusMap.get("entities")); + assertSame(status, statusMap.get("directories")); } @Test @@ -128,14 +127,14 @@ void setEntitiesRecordsErrorStatusOnException() { final Map statusMap = new HashMap<>(); final Map entities = Collections.singletonMap("key", "value"); - final _AllModelStatus status = service.setEntities("entities", entities, + final _AllModelStatus status = service.setEntities("directories", entities, input -> { throw new WebApplicationException(Response.Status.BAD_REQUEST); }, result -> {}, statusMap); assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), status.getStatus()); - assertSame(status, statusMap.get("entities")); + assertSame(status, statusMap.get("directories")); } // setEntityWithStatus @@ -144,7 +143,7 @@ void setEntitiesRecordsErrorStatusOnException() { void setEntityWithStatusSkipsNullInput() { final Map statusMap = new HashMap<>(); - service.setEntityWithStatus(null, + service.setEntityWithStatus("settings", null, input -> new ServiceResult<>(input, Collections.emptyMap()), result -> {}, statusMap); @@ -152,15 +151,15 @@ void setEntityWithStatusSkipsNullInput() { } @Test - void setEntityWithStatusMergesSubFieldStatuses() { + void setEntityWithStatusPrefixesSubFieldStatusesWithField() { final Map statusMap = new HashMap<>(); final AtomicReference result = new AtomicReference<>(); final Map subStatus = new LinkedHashMap<>(); - subStatus.put("settings/general", _AllModelStatus.success()); - subStatus.put("settings/security", _AllModelStatus.error(Response.Status.BAD_REQUEST, "bad", null)); + subStatus.put("general", _AllModelStatus.success()); + subStatus.put("security", _AllModelStatus.error(Response.Status.BAD_REQUEST, "bad", null)); - service.setEntityWithStatus("input", + service.setEntityWithStatus("settings", "input", input -> new ServiceResult<>(input + "-updated", subStatus), result::set, statusMap); @@ -171,30 +170,28 @@ void setEntityWithStatusMergesSubFieldStatuses() { } @Test - void setEntityWithStatusFallsBackToRootElementNameOnThrow() { + void setEntityWithStatusFallsBackToFieldKeyOnThrow() { final Map statusMap = new HashMap<>(); - service.setEntityWithStatus(SettingsGeneralModel.EXAMPLE_1, + service.setEntityWithStatus("settings", "input", input -> { throw new IllegalStateException("boom"); }, result -> {}, statusMap); - // fallback key is the input model's @XmlRootElement name - final _AllModelStatus status = statusMap.get("settings-general"); - assertEquals(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode(), status.getStatus()); + assertEquals(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode(), statusMap.get("settings").getStatus()); } @Test void setEntityWithStatusUsesWebApplicationExceptionStatusCodeOnThrow() { final Map statusMap = new HashMap<>(); - service.setEntityWithStatus(SettingsGeneralModel.EXAMPLE_1, + service.setEntityWithStatus("settings", "input", input -> { throw new WebApplicationException(Response.Status.CONFLICT); }, result -> {}, statusMap); - assertEquals(Response.Status.CONFLICT.getStatusCode(), statusMap.get("settings-general").getStatus()); + assertEquals(Response.Status.CONFLICT.getStatusCode(), statusMap.get("settings").getStatus()); } } diff --git a/commons/src/test/java/com/deftdevs/bootstrapi/commons/service/api/AuthenticationServiceTest.java b/commons/src/test/java/com/deftdevs/bootstrapi/commons/service/api/AuthenticationServiceTest.java index 1bc700a8..774f0449 100644 --- a/commons/src/test/java/com/deftdevs/bootstrapi/commons/service/api/AuthenticationServiceTest.java +++ b/commons/src/test/java/com/deftdevs/bootstrapi/commons/service/api/AuthenticationServiceTest.java @@ -59,8 +59,8 @@ void testSetAuthenticationAppliesIdpsAndSso() { assertEquals(idpModels, result.getModel().getIdps()); assertEquals(ssoModel, result.getModel().getSso()); - assertEquals(Response.Status.OK.getStatusCode(), result.getStatus().get("authentication/idps").getStatus()); - assertEquals(Response.Status.OK.getStatusCode(), result.getStatus().get("authentication/sso").getStatus()); + assertEquals(Response.Status.OK.getStatusCode(), result.getStatus().get("idps").getStatus()); + assertEquals(Response.Status.OK.getStatusCode(), result.getStatus().get("sso").getStatus()); } @Test @@ -82,7 +82,7 @@ void testSetAuthenticationRecordsPerSubFieldFailure() { assertEquals(idpModels, result.getModel().getIdps()); assertNull(result.getModel().getSso()); - assertEquals(Response.Status.OK.getStatusCode(), result.getStatus().get("authentication/idps").getStatus()); - assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), result.getStatus().get("authentication/sso").getStatus()); + assertEquals(Response.Status.OK.getStatusCode(), result.getStatus().get("idps").getStatus()); + assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), result.getStatus().get("sso").getStatus()); } } diff --git a/commons/src/test/java/com/deftdevs/bootstrapi/commons/service/api/MailServerServiceTest.java b/commons/src/test/java/com/deftdevs/bootstrapi/commons/service/api/MailServerServiceTest.java index 63b4a32c..82ac147f 100644 --- a/commons/src/test/java/com/deftdevs/bootstrapi/commons/service/api/MailServerServiceTest.java +++ b/commons/src/test/java/com/deftdevs/bootstrapi/commons/service/api/MailServerServiceTest.java @@ -50,8 +50,8 @@ void testSetMailServerAppliesSmtpAndPop() { assertEquals(MailServerSmtpModel.EXAMPLE_1, result.getModel().getSmtp()); assertEquals(MailServerPopModel.EXAMPLE_1, result.getModel().getPop()); - assertEquals(Response.Status.OK.getStatusCode(), result.getStatus().get("mail-server/smtp").getStatus()); - assertEquals(Response.Status.OK.getStatusCode(), result.getStatus().get("mail-server/pop").getStatus()); + assertEquals(Response.Status.OK.getStatusCode(), result.getStatus().get("smtp").getStatus()); + assertEquals(Response.Status.OK.getStatusCode(), result.getStatus().get("pop").getStatus()); } @Test @@ -74,7 +74,7 @@ void testSetMailServerRecordsPerSubFieldFailure() { assertNull(result.getModel().getSmtp()); assertEquals(MailServerPopModel.EXAMPLE_1, result.getModel().getPop()); - assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), result.getStatus().get("mail-server/smtp").getStatus()); - assertEquals(Response.Status.OK.getStatusCode(), result.getStatus().get("mail-server/pop").getStatus()); + assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), result.getStatus().get("smtp").getStatus()); + assertEquals(Response.Status.OK.getStatusCode(), result.getStatus().get("pop").getStatus()); } } diff --git a/commons/src/test/java/com/deftdevs/bootstrapi/commons/util/ServiceResultUtilTest.java b/commons/src/test/java/com/deftdevs/bootstrapi/commons/util/ServiceResultUtilTest.java index 813acb24..4a013462 100644 --- a/commons/src/test/java/com/deftdevs/bootstrapi/commons/util/ServiceResultUtilTest.java +++ b/commons/src/test/java/com/deftdevs/bootstrapi/commons/util/ServiceResultUtilTest.java @@ -5,83 +5,51 @@ import javax.ws.rs.WebApplicationException; import javax.ws.rs.core.Response; -import javax.xml.bind.annotation.XmlRootElement; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.concurrent.atomic.AtomicReference; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; class ServiceResultUtilTest { - @XmlRootElement(name = "leaf") - private static class LeafModel { - } - - @XmlRootElement(name = "settings-general") - private static class SubFieldSingleDash { - } - - @XmlRootElement(name = "mail-server-smtp") - private static class SubFieldMultiDash { - } - - @XmlRootElement(name = "settings") - private static class GroupModel { - } - - @XmlRootElement(name = "settings-custom-html") - private static class SubFieldWithDashedSubName { - } - - @XmlRootElement - private static class UnnamedModel { - } - - private static class UnannotatedModel { - } - @Test - void entityTypeReturnsRawName() { - assertEquals("leaf", ServiceResultUtil.entityType(LeafModel.class)); - } + void setSubEntitySkipsNullInput() { + final Map statusMap = new LinkedHashMap<>(); - @Test - void entityTypeRejectsMissingAnnotation() { - assertThrows(IllegalArgumentException.class, - () -> ServiceResultUtil.entityType(UnannotatedModel.class)); - } + ServiceResultUtil.setSubEntity(statusMap, "general", null, + input -> input, result -> {}); - @Test - void entityTypeRejectsDefaultName() { - assertThrows(IllegalArgumentException.class, - () -> ServiceResultUtil.entityType(UnnamedModel.class)); + assertTrue(statusMap.isEmpty()); } @Test - void subEntityKeySplitsOnLastDash() { - assertEquals("settings/general", ServiceResultUtil.subEntityKey(SubFieldSingleDash.class)); - assertEquals("mail-server/smtp", ServiceResultUtil.subEntityKey(SubFieldMultiDash.class)); - } + void setSubEntityRecordsSuccessAndResult() { + final Map statusMap = new LinkedHashMap<>(); + final AtomicReference result = new AtomicReference<>(); - @Test - void subEntityKeyRejectsNonCompositeName() { - assertThrows(IllegalArgumentException.class, - () -> ServiceResultUtil.subEntityKey(LeafModel.class)); - } + ServiceResultUtil.setSubEntity(statusMap, "general", "input", + input -> input + "-updated", result::set); - @Test - void groupAwareSubEntityKeyStripsGroupPrefix() { - assertEquals("settings/custom-html", - ServiceResultUtil.subEntityKey(GroupModel.class, SubFieldWithDashedSubName.class)); - assertEquals("settings/general", - ServiceResultUtil.subEntityKey(GroupModel.class, SubFieldSingleDash.class)); + assertEquals("input-updated", result.get()); + assertEquals(Response.Status.OK.getStatusCode(), statusMap.get("general").getStatus()); } @Test - void groupAwareSubEntityKeyRejectsNonMatchingGroup() { - assertThrows(IllegalArgumentException.class, - () -> ServiceResultUtil.subEntityKey(GroupModel.class, SubFieldMultiDash.class)); + void setSubEntityRecordsErrorStatusOnException() { + final Map statusMap = new LinkedHashMap<>(); + final AtomicReference result = new AtomicReference<>(); + + ServiceResultUtil.setSubEntity(statusMap, "general", "input", + input -> { + throw new WebApplicationException("bad", Response.Status.BAD_REQUEST); + }, + result::set); + + assertNull(result.get()); + assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), statusMap.get("general").getStatus()); } @Test diff --git a/commons/src/test/java/it/com/deftdevs/bootstrapi/commons/rest/Abstract_AllResourceFuncTest.java b/commons/src/test/java/it/com/deftdevs/bootstrapi/commons/rest/Abstract_AllResourceFuncTest.java index e3d46d99..75abfbfe 100644 --- a/commons/src/test/java/it/com/deftdevs/bootstrapi/commons/rest/Abstract_AllResourceFuncTest.java +++ b/commons/src/test/java/it/com/deftdevs/bootstrapi/commons/rest/Abstract_AllResourceFuncTest.java @@ -1,7 +1,6 @@ package it.com.deftdevs.bootstrapi.commons.rest; import com.deftdevs.bootstrapi.commons.model.SettingsGeneralModel; -import com.deftdevs.bootstrapi.commons.util.ServiceResultUtil; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.jupiter.api.Test; @@ -31,8 +30,7 @@ public abstract class Abstract_AllResourceFuncTest { /** The _all endpoint is the REST root itself. */ private static final String ALL_PATH = ""; - protected static final String SETTINGS_GENERAL_KEY = - ServiceResultUtil.subEntityKey(SettingsGeneralModel.class); + protected static final String SETTINGS_GENERAL_KEY = "settings/general"; private final ObjectMapper objectMapper = new ObjectMapper(); diff --git a/confluence/Apis/AllApi.md b/confluence/Apis/AllApi.md index 8174af80..9619e201 100644 --- a/confluence/Apis/AllApi.md +++ b/confluence/Apis/AllApi.md @@ -13,7 +13,7 @@ All URIs are relative to *https://CONFLUENCE_URL/rest/bootstrapi/1* Apply a complete configuration - Returns the updated configuration. The per-sub-field outcome is reported in the 'status' map (2xx for success, 4xx/5xx for failure with a human-readable 'message' and optional 'details'). License keys in the response are redacted (e.g. 'AAAB...wxyz#a1b2'). + Returns the updated configuration. The per-sub-field outcome is reported in the 'status' map, keyed by the request's field paths (e.g. 'settings/general', 'mailServer/smtp'; 2xx for success, 4xx/5xx for failure with a human-readable 'message' and optional 'details'). License keys in the response are redacted (e.g. 'AAAB...wxyz#a1b2'). ### Parameters diff --git a/confluence/src/main/java/com/deftdevs/bootstrapi/confluence/service/_AllServiceImpl.java b/confluence/src/main/java/com/deftdevs/bootstrapi/confluence/service/_AllServiceImpl.java index 91d3ea1d..e2c56658 100644 --- a/confluence/src/main/java/com/deftdevs/bootstrapi/confluence/service/_AllServiceImpl.java +++ b/confluence/src/main/java/com/deftdevs/bootstrapi/confluence/service/_AllServiceImpl.java @@ -1,6 +1,5 @@ package com.deftdevs.bootstrapi.confluence.service; -import com.deftdevs.bootstrapi.commons.model.PermissionsGlobalModel; import com.deftdevs.bootstrapi.commons.model.type._AllModelStatus; import com.deftdevs.bootstrapi.commons.service._AbstractAllServiceImpl; import com.deftdevs.bootstrapi.commons.service.api.ApplicationLinksService; @@ -8,7 +7,6 @@ import com.deftdevs.bootstrapi.commons.service.api.LicensesService; import com.deftdevs.bootstrapi.commons.service.api.MailServerService; import com.deftdevs.bootstrapi.commons.service.api.PermissionsService; -import com.deftdevs.bootstrapi.commons.util.ServiceResultUtil; import com.deftdevs.bootstrapi.confluence.model._AllModel; import com.deftdevs.bootstrapi.confluence.service.api.ConfluenceAuthenticationService; import com.deftdevs.bootstrapi.confluence.service.api.ConfluenceSettingsService; @@ -16,9 +14,6 @@ import java.util.HashMap; import java.util.Map; -import static com.deftdevs.bootstrapi.commons.constants.BootstrAPI.APPLICATION_LINKS; -import static com.deftdevs.bootstrapi.commons.constants.BootstrAPI.DIRECTORIES; -import static com.deftdevs.bootstrapi.commons.constants.BootstrAPI.LICENSES; public class _AllServiceImpl extends _AbstractAllServiceImpl<_AllModel> { @@ -55,25 +50,25 @@ public _AllModel setAll( final _AllModel result = new _AllModel(); final Map statusMap = new HashMap<>(); - setEntityWithStatus(allModel.getSettings(), + setEntityWithStatus("settings", allModel.getSettings(), settingsService::setSettings, result::setSettings, statusMap); - setEntities(DIRECTORIES, allModel.getDirectories(), + setEntities("directories", allModel.getDirectories(), directoriesService::setDirectories, result::setDirectories, statusMap); - setEntities(APPLICATION_LINKS, allModel.getApplicationLinks(), + setEntities("applicationLinks", allModel.getApplicationLinks(), applicationLinksService::setApplicationLinks, result::setApplicationLinks, statusMap); - setEntityWithStatus(allModel.getAuthentication(), + setEntityWithStatus("authentication", allModel.getAuthentication(), authenticationService::setAuthentication, result::setAuthentication, statusMap); - setEntities(LICENSES, allModel.getLicenses(), + setEntities("licenses", allModel.getLicenses(), licensesService::setLicenses, result::setLicenses, statusMap); - setEntityWithStatus(allModel.getMailServer(), + setEntityWithStatus("mailServer", allModel.getMailServer(), mailServerService::setMailServer, result::setMailServer, statusMap); - setEntity(ServiceResultUtil.subEntityKey(PermissionsGlobalModel.class), allModel.getPermissionsGlobal(), + setEntity("permissionsGlobal", allModel.getPermissionsGlobal(), permissionsService::setPermissionsGlobal, result::setPermissionsGlobal, statusMap); result.setStatus(statusMap); diff --git a/confluence/src/main/java/com/deftdevs/bootstrapi/confluence/service/api/ConfluenceSettingsService.java b/confluence/src/main/java/com/deftdevs/bootstrapi/confluence/service/api/ConfluenceSettingsService.java index 3a0d764f..64577b24 100644 --- a/confluence/src/main/java/com/deftdevs/bootstrapi/confluence/service/api/ConfluenceSettingsService.java +++ b/confluence/src/main/java/com/deftdevs/bootstrapi/confluence/service/api/ConfluenceSettingsService.java @@ -1,10 +1,9 @@ package com.deftdevs.bootstrapi.confluence.service.api; -import com.deftdevs.bootstrapi.commons.model.SettingsBrandingColorSchemeModel; import com.deftdevs.bootstrapi.commons.model.SettingsGeneralModel; import com.deftdevs.bootstrapi.commons.model.SettingsSecurityModel; -import com.deftdevs.bootstrapi.commons.model.type._AllModelStatus; import com.deftdevs.bootstrapi.commons.model.type.ServiceResult; +import com.deftdevs.bootstrapi.commons.model.type._AllModelStatus; import com.deftdevs.bootstrapi.commons.service.api.SettingsBrandingService; import com.deftdevs.bootstrapi.commons.service.api.SettingsGeneralService; import com.deftdevs.bootstrapi.commons.service.api.SettingsSecurityService; @@ -34,43 +33,15 @@ default ServiceResult setSettings(final SettingsModel settingsMod final SettingsModel result = new SettingsModel(); final Map status = new LinkedHashMap<>(); - if (settingsModel.getGeneral() != null) { - final String key = ServiceResultUtil.subEntityKey(SettingsGeneralModel.class); - try { - result.setGeneral(setSettingsGeneral(settingsModel.getGeneral())); - status.put(key, _AllModelStatus.success()); - } catch (Exception e) { - status.put(key, ServiceResultUtil.toErrorStatus(key, e)); - } - } - if (settingsModel.getSecurity() != null) { - final String key = ServiceResultUtil.subEntityKey(SettingsSecurityModel.class); - try { - result.setSecurity(setSettingsSecurity(settingsModel.getSecurity())); - status.put(key, _AllModelStatus.success()); - } catch (Exception e) { - status.put(key, ServiceResultUtil.toErrorStatus(key, e)); - } - } - if (settingsModel.getBranding() != null) { - final String key = ServiceResultUtil.subEntityKey(SettingsBrandingColorSchemeModel.class); - try { - result.setBranding(setColourScheme(settingsModel.getBranding())); - status.put(key, _AllModelStatus.success()); - } catch (Exception e) { - status.put(key, ServiceResultUtil.toErrorStatus(key, e)); - } - } - if (settingsModel.getCustomHtml() != null) { - // sub-name "custom-html" contains a dash, so derive the key from the group model - final String key = ServiceResultUtil.subEntityKey(SettingsModel.class, SettingsCustomHtmlModel.class); - try { - result.setCustomHtml(setCustomHtml(settingsModel.getCustomHtml())); - status.put(key, _AllModelStatus.success()); - } catch (Exception e) { - status.put(key, ServiceResultUtil.toErrorStatus(key, e)); - } - } + ServiceResultUtil.setSubEntity(status, "general", settingsModel.getGeneral(), + this::setSettingsGeneral, result::setGeneral); + ServiceResultUtil.setSubEntity(status, "security", settingsModel.getSecurity(), + this::setSettingsSecurity, result::setSecurity); + ServiceResultUtil.setSubEntity(status, "branding", settingsModel.getBranding(), + this::setColourScheme, result::setBranding); + ServiceResultUtil.setSubEntity(status, "customHtml", settingsModel.getCustomHtml(), + this::setCustomHtml, result::setCustomHtml); + return new ServiceResult<>(result, status); } diff --git a/confluence/src/test/java/com/deftdevs/bootstrapi/confluence/rest/_AllResourceTest.java b/confluence/src/test/java/com/deftdevs/bootstrapi/confluence/rest/_AllResourceTest.java index 021f9c48..03827a6c 100644 --- a/confluence/src/test/java/com/deftdevs/bootstrapi/confluence/rest/_AllResourceTest.java +++ b/confluence/src/test/java/com/deftdevs/bootstrapi/confluence/rest/_AllResourceTest.java @@ -39,7 +39,7 @@ public void setup() { allModel.setSettings(settings); final Map status = new HashMap<>(); - status.put(SETTINGS + "/" + SETTINGS_GENERAL, _AllModelStatus.success()); + status.put("settings/general", _AllModelStatus.success()); status.put(DIRECTORIES, _AllModelStatus.success()); status.put(APPLICATION_LINKS, _AllModelStatus.success()); allModel.setStatus(status); @@ -62,7 +62,7 @@ public void testSetAll() { public void testSetAllReturns500WhenAnySubFieldFailedWithServerError() { final _AllModel result = new _AllModel(); final Map status = new HashMap<>(); - status.put(SETTINGS + "/" + SETTINGS_GENERAL, _AllModelStatus.success()); + status.put("settings/general", _AllModelStatus.success()); status.put(DIRECTORIES, _AllModelStatus.error(Response.Status.INTERNAL_SERVER_ERROR, "boom", null)); result.setStatus(status); @@ -76,7 +76,7 @@ public void testSetAllReturns500WhenAnySubFieldFailedWithServerError() { public void testSetAllReturns207OnMixedSuccessAndClientError() { final _AllModel result = new _AllModel(); final Map status = new HashMap<>(); - status.put(SETTINGS + "/" + SETTINGS_GENERAL, _AllModelStatus.success()); + status.put("settings/general", _AllModelStatus.success()); status.put(DIRECTORIES, _AllModelStatus.error(Response.Status.BAD_REQUEST, "bad", null)); result.setStatus(status); diff --git a/confluence/src/test/java/com/deftdevs/bootstrapi/confluence/service/ConfluenceSettingsServiceTest.java b/confluence/src/test/java/com/deftdevs/bootstrapi/confluence/service/ConfluenceSettingsServiceTest.java index 62ee3f74..9ada7180 100644 --- a/confluence/src/test/java/com/deftdevs/bootstrapi/confluence/service/ConfluenceSettingsServiceTest.java +++ b/confluence/src/test/java/com/deftdevs/bootstrapi/confluence/service/ConfluenceSettingsServiceTest.java @@ -75,10 +75,10 @@ void testSetSettingsAppliesAllSubFields() { assertEquals(SettingsBrandingColorSchemeModel.EXAMPLE_1, result.getModel().getBranding()); assertEquals(SettingsCustomHtmlModel.EXAMPLE_1, result.getModel().getCustomHtml()); - assertEquals(Response.Status.OK.getStatusCode(), result.getStatus().get("settings/general").getStatus()); - assertEquals(Response.Status.OK.getStatusCode(), result.getStatus().get("settings/security").getStatus()); - assertEquals(Response.Status.OK.getStatusCode(), result.getStatus().get("settings/branding").getStatus()); - assertEquals(Response.Status.OK.getStatusCode(), result.getStatus().get("settings/custom-html").getStatus()); + assertEquals(Response.Status.OK.getStatusCode(), result.getStatus().get("general").getStatus()); + assertEquals(Response.Status.OK.getStatusCode(), result.getStatus().get("security").getStatus()); + assertEquals(Response.Status.OK.getStatusCode(), result.getStatus().get("branding").getStatus()); + assertEquals(Response.Status.OK.getStatusCode(), result.getStatus().get("customHtml").getStatus()); } @Test @@ -103,7 +103,7 @@ void testSetSettingsRecordsPerSubFieldFailure() { assertEquals(SettingsGeneralModel.EXAMPLE_1, result.getModel().getGeneral()); assertNull(result.getModel().getBranding()); - assertEquals(Response.Status.OK.getStatusCode(), result.getStatus().get("settings/general").getStatus()); - assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), result.getStatus().get("settings/branding").getStatus()); + assertEquals(Response.Status.OK.getStatusCode(), result.getStatus().get("general").getStatus()); + assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), result.getStatus().get("branding").getStatus()); } } diff --git a/confluence/src/test/java/com/deftdevs/bootstrapi/confluence/service/_AllServiceImplTest.java b/confluence/src/test/java/com/deftdevs/bootstrapi/confluence/service/_AllServiceImplTest.java index d99768ea..6fc93306 100644 --- a/confluence/src/test/java/com/deftdevs/bootstrapi/confluence/service/_AllServiceImplTest.java +++ b/confluence/src/test/java/com/deftdevs/bootstrapi/confluence/service/_AllServiceImplTest.java @@ -102,16 +102,16 @@ void testSetAllAppliesAllFields() { final PermissionsGlobalModel permissionsGlobal = new PermissionsGlobalModel(); doReturn(new ServiceResult<>(settings, - Collections.singletonMap("settings/general", _AllModelStatus.success()))) + Collections.singletonMap("general", _AllModelStatus.success()))) .when(settingsService).setSettings(settings); doReturn(directories).when(directoriesService).setDirectories(directories); doReturn(applicationLinks).when(applicationLinksService).setApplicationLinks(applicationLinks); doReturn(new ServiceResult<>(authentication, - Collections.singletonMap("authentication/sso", _AllModelStatus.success()))) + Collections.singletonMap("sso", _AllModelStatus.success()))) .when(authenticationService).setAuthentication(authentication); doReturn(redactedLicenses).when(licensesService).setLicenses(licenses); doReturn(new ServiceResult<>(mailServer, - Collections.singletonMap("mail-server/smtp", _AllModelStatus.success()))) + Collections.singletonMap("smtp", _AllModelStatus.success()))) .when(mailServerService).setMailServer(mailServer); doReturn(permissionsGlobal).when(permissionsService).setPermissionsGlobal(permissionsGlobal); @@ -138,11 +138,11 @@ void testSetAllAppliesAllFields() { assertEquals(7, status.size()); assertEquals(Response.Status.OK.getStatusCode(), status.get("settings/general").getStatus()); assertEquals(Response.Status.OK.getStatusCode(), status.get("directories").getStatus()); - assertEquals(Response.Status.OK.getStatusCode(), status.get("application-links").getStatus()); + assertEquals(Response.Status.OK.getStatusCode(), status.get("applicationLinks").getStatus()); assertEquals(Response.Status.OK.getStatusCode(), status.get("authentication/sso").getStatus()); assertEquals(Response.Status.OK.getStatusCode(), status.get("licenses").getStatus()); - assertEquals(Response.Status.OK.getStatusCode(), status.get("mail-server/smtp").getStatus()); - assertEquals(Response.Status.OK.getStatusCode(), status.get("permissions/global").getStatus()); + assertEquals(Response.Status.OK.getStatusCode(), status.get("mailServer/smtp").getStatus()); + assertEquals(Response.Status.OK.getStatusCode(), status.get("permissionsGlobal").getStatus()); } @Test @@ -153,7 +153,7 @@ void testSetAllRecordsFailureAndContinuesWithOtherFields() { Collections.singletonMap("licenseKey", LicenseModel.EXAMPLE_1); doReturn(new ServiceResult<>(settings, - Collections.singletonMap("settings/general", _AllModelStatus.success()))) + Collections.singletonMap("general", _AllModelStatus.success()))) .when(settingsService).setSettings(settings); doThrow(new WebApplicationException(Response.Status.CONFLICT)) .when(licensesService).setLicenses(licenses); diff --git a/confluence/src/test/java/it/com/deftdevs/bootstrapi/confluence/rest/_AllResourceFuncTest.java b/confluence/src/test/java/it/com/deftdevs/bootstrapi/confluence/rest/_AllResourceFuncTest.java index e689b0bf..df2128cf 100644 --- a/confluence/src/test/java/it/com/deftdevs/bootstrapi/confluence/rest/_AllResourceFuncTest.java +++ b/confluence/src/test/java/it/com/deftdevs/bootstrapi/confluence/rest/_AllResourceFuncTest.java @@ -4,7 +4,6 @@ import com.deftdevs.bootstrapi.commons.model.MailServerPopModel; import com.deftdevs.bootstrapi.commons.model.MailServerSmtpModel; import com.deftdevs.bootstrapi.commons.model.SettingsGeneralModel; -import com.deftdevs.bootstrapi.commons.util.ServiceResultUtil; import com.deftdevs.bootstrapi.confluence.model.SettingsCustomHtmlModel; import com.deftdevs.bootstrapi.confluence.model.SettingsModel; import com.deftdevs.bootstrapi.confluence.model._AllModel; @@ -43,9 +42,9 @@ void testSetAllAppliesMultipleEntities() throws Exception { .build(); assertSetAllApplied(allModel, Arrays.asList( - ServiceResultUtil.subEntityKey(SettingsGeneralModel.class), - ServiceResultUtil.subEntityKey(SettingsModel.class, SettingsCustomHtmlModel.class), - ServiceResultUtil.subEntityKey(MailServerSmtpModel.class), - ServiceResultUtil.subEntityKey(MailServerPopModel.class))); + "settings/general", + "settings/customHtml", + "mailServer/smtp", + "mailServer/pop")); } } diff --git a/crowd/Apis/AllApi.md b/crowd/Apis/AllApi.md index c56ea6de..91725ac0 100644 --- a/crowd/Apis/AllApi.md +++ b/crowd/Apis/AllApi.md @@ -13,7 +13,7 @@ All URIs are relative to *https://CROWD_URL/rest/bootstrapi/1* Apply a complete configuration - Returns the updated configuration. The per-sub-field outcome is reported in the 'status' map (2xx for success, 4xx/5xx for failure with a human-readable 'message' and optional 'details'). License keys in the response are redacted (e.g. 'AAAB...wxyz#a1b2'). + Returns the updated configuration. The per-sub-field outcome is reported in the 'status' map, keyed by the request's field paths (e.g. 'settings/general', 'mailServer/smtp'; 2xx for success, 4xx/5xx for failure with a human-readable 'message' and optional 'details'). License keys in the response are redacted (e.g. 'AAAB...wxyz#a1b2'). ### Parameters diff --git a/crowd/src/main/java/com/deftdevs/bootstrapi/crowd/service/_AllServiceImpl.java b/crowd/src/main/java/com/deftdevs/bootstrapi/crowd/service/_AllServiceImpl.java index f2aec961..30af0e15 100644 --- a/crowd/src/main/java/com/deftdevs/bootstrapi/crowd/service/_AllServiceImpl.java +++ b/crowd/src/main/java/com/deftdevs/bootstrapi/crowd/service/_AllServiceImpl.java @@ -6,9 +6,6 @@ import com.deftdevs.bootstrapi.commons.service.api.DirectoriesService; import com.deftdevs.bootstrapi.commons.service.api.LicensesService; import com.deftdevs.bootstrapi.commons.service.api.MailServerService; -import com.deftdevs.bootstrapi.commons.util.ServiceResultUtil; -import com.deftdevs.bootstrapi.crowd.model.MailTemplatesModel; -import com.deftdevs.bootstrapi.crowd.model.SessionConfigModel; import com.deftdevs.bootstrapi.crowd.model._AllModel; import com.deftdevs.bootstrapi.crowd.service.api.ApplicationsService; import com.deftdevs.bootstrapi.crowd.service.api.CrowdSettingsService; @@ -19,11 +16,6 @@ import java.util.HashMap; import java.util.Map; -import static com.deftdevs.bootstrapi.commons.constants.BootstrAPI.APPLICATIONS; -import static com.deftdevs.bootstrapi.commons.constants.BootstrAPI.APPLICATION_LINKS; -import static com.deftdevs.bootstrapi.commons.constants.BootstrAPI.DIRECTORIES; -import static com.deftdevs.bootstrapi.commons.constants.BootstrAPI.LICENSES; -import static com.deftdevs.bootstrapi.commons.constants.BootstrAPI.TRUSTED_PROXIES; public class _AllServiceImpl extends _AbstractAllServiceImpl<_AllModel> { @@ -66,31 +58,31 @@ public _AllModel setAll( final _AllModel result = new _AllModel(); final Map statusMap = new HashMap<>(); - setEntityWithStatus(allModel.getSettings(), + setEntityWithStatus("settings", allModel.getSettings(), settingsService::setSettings, result::setSettings, statusMap); - setEntities(DIRECTORIES, allModel.getDirectories(), + setEntities("directories", allModel.getDirectories(), directoriesService::setDirectories, result::setDirectories, statusMap); - setEntities(APPLICATIONS, allModel.getApplications(), + setEntities("applications", allModel.getApplications(), applicationsService::setApplications, result::setApplications, statusMap); - setEntities(APPLICATION_LINKS, allModel.getApplicationLinks(), + setEntities("applicationLinks", allModel.getApplicationLinks(), applicationLinksService::setApplicationLinks, result::setApplicationLinks, statusMap); - setEntities(LICENSES, allModel.getLicenses(), + setEntities("licenses", allModel.getLicenses(), licensesService::setLicenses, result::setLicenses, statusMap); - setEntityWithStatus(allModel.getMailServer(), + setEntityWithStatus("mailServer", allModel.getMailServer(), mailServerService::setMailServer, result::setMailServer, statusMap); - setEntity(ServiceResultUtil.entityType(MailTemplatesModel.class), allModel.getMailTemplates(), + setEntity("mailTemplates", allModel.getMailTemplates(), mailTemplatesService::setMailTemplates, result::setMailTemplates, statusMap); - setEntity(ServiceResultUtil.entityType(SessionConfigModel.class), allModel.getSessionConfig(), + setEntity("sessionConfig", allModel.getSessionConfig(), sessionConfigService::setSessionConfig, result::setSessionConfig, statusMap); - setEntity(TRUSTED_PROXIES, allModel.getTrustedProxies(), + setEntity("trustedProxies", allModel.getTrustedProxies(), trustedProxiesService::setTrustedProxies, result::setTrustedProxies, statusMap); result.setStatus(statusMap); diff --git a/crowd/src/main/java/com/deftdevs/bootstrapi/crowd/service/api/CrowdSettingsService.java b/crowd/src/main/java/com/deftdevs/bootstrapi/crowd/service/api/CrowdSettingsService.java index 1bea779f..06a62fae 100644 --- a/crowd/src/main/java/com/deftdevs/bootstrapi/crowd/service/api/CrowdSettingsService.java +++ b/crowd/src/main/java/com/deftdevs/bootstrapi/crowd/service/api/CrowdSettingsService.java @@ -1,10 +1,8 @@ package com.deftdevs.bootstrapi.crowd.service.api; -import com.deftdevs.bootstrapi.commons.model.SettingsGeneralModel; -import com.deftdevs.bootstrapi.commons.model.type._AllModelStatus; import com.deftdevs.bootstrapi.commons.model.type.ServiceResult; +import com.deftdevs.bootstrapi.commons.model.type._AllModelStatus; import com.deftdevs.bootstrapi.commons.util.ServiceResultUtil; -import com.deftdevs.bootstrapi.crowd.model.SettingsBrandingLoginPageModel; import com.deftdevs.bootstrapi.crowd.model.SettingsModel; import java.util.LinkedHashMap; @@ -22,24 +20,11 @@ default ServiceResult setSettings(final SettingsModel settingsMod final SettingsModel result = new SettingsModel(); final Map status = new LinkedHashMap<>(); - if (settingsModel.getGeneral() != null) { - final String key = ServiceResultUtil.subEntityKey(SettingsGeneralModel.class); - try { - result.setGeneral(setSettingsGeneral(settingsModel.getGeneral())); - status.put(key, _AllModelStatus.success()); - } catch (Exception e) { - status.put(key, ServiceResultUtil.toErrorStatus(key, e)); - } - } - if (settingsModel.getBranding() != null) { - final String key = ServiceResultUtil.subEntityKey(SettingsBrandingLoginPageModel.class); - try { - result.setBranding(setLoginPage(settingsModel.getBranding())); - status.put(key, _AllModelStatus.success()); - } catch (Exception e) { - status.put(key, ServiceResultUtil.toErrorStatus(key, e)); - } - } + ServiceResultUtil.setSubEntity(status, "general", settingsModel.getGeneral(), + this::setSettingsGeneral, result::setGeneral); + ServiceResultUtil.setSubEntity(status, "branding", settingsModel.getBranding(), + this::setLoginPage, result::setBranding); + return new ServiceResult<>(result, status); } diff --git a/crowd/src/test/java/com/deftdevs/bootstrapi/crowd/rest/_AllResourceTest.java b/crowd/src/test/java/com/deftdevs/bootstrapi/crowd/rest/_AllResourceTest.java index 826e0d3a..dda04f16 100644 --- a/crowd/src/test/java/com/deftdevs/bootstrapi/crowd/rest/_AllResourceTest.java +++ b/crowd/src/test/java/com/deftdevs/bootstrapi/crowd/rest/_AllResourceTest.java @@ -44,7 +44,7 @@ public void setup() { allModel.setApplications(applications); final Map status = new HashMap<>(); - status.put(SETTINGS + "/" + SETTINGS_GENERAL, _AllModelStatus.success()); + status.put("settings/general", _AllModelStatus.success()); status.put(DIRECTORIES, _AllModelStatus.success()); status.put(APPLICATIONS, _AllModelStatus.success()); allModel.setStatus(status); @@ -67,7 +67,7 @@ public void testSetAll() { public void testSetAllReturns500WhenAnySubFieldFailedWithServerError() { final _AllModel result = new _AllModel(); final Map status = new HashMap<>(); - status.put(SETTINGS + "/" + SETTINGS_GENERAL, _AllModelStatus.success()); + status.put("settings/general", _AllModelStatus.success()); status.put(DIRECTORIES, _AllModelStatus.error(Response.Status.INTERNAL_SERVER_ERROR, "boom", null)); result.setStatus(status); @@ -81,7 +81,7 @@ public void testSetAllReturns500WhenAnySubFieldFailedWithServerError() { public void testSetAllReturns207OnMixedSuccessAndClientError() { final _AllModel result = new _AllModel(); final Map status = new HashMap<>(); - status.put(SETTINGS + "/" + SETTINGS_GENERAL, _AllModelStatus.success()); + status.put("settings/general", _AllModelStatus.success()); status.put(DIRECTORIES, _AllModelStatus.error(Response.Status.BAD_REQUEST, "bad", null)); result.setStatus(status); diff --git a/crowd/src/test/java/com/deftdevs/bootstrapi/crowd/service/CrowdSettingsServiceTest.java b/crowd/src/test/java/com/deftdevs/bootstrapi/crowd/service/CrowdSettingsServiceTest.java index 66580ac5..36a40f4c 100644 --- a/crowd/src/test/java/com/deftdevs/bootstrapi/crowd/service/CrowdSettingsServiceTest.java +++ b/crowd/src/test/java/com/deftdevs/bootstrapi/crowd/service/CrowdSettingsServiceTest.java @@ -62,8 +62,8 @@ void testSetSettingsAppliesAllSubFields() { assertEquals(SettingsGeneralModel.EXAMPLE_1, result.getModel().getGeneral()); assertEquals(SettingsBrandingLoginPageModel.EXAMPLE_1, result.getModel().getBranding()); - assertEquals(Response.Status.OK.getStatusCode(), result.getStatus().get("settings/general").getStatus()); - assertEquals(Response.Status.OK.getStatusCode(), result.getStatus().get("settings/branding").getStatus()); + assertEquals(Response.Status.OK.getStatusCode(), result.getStatus().get("general").getStatus()); + assertEquals(Response.Status.OK.getStatusCode(), result.getStatus().get("branding").getStatus()); } @Test @@ -87,7 +87,7 @@ void testSetSettingsRecordsPerSubFieldFailure() { assertEquals(SettingsGeneralModel.EXAMPLE_1, result.getModel().getGeneral()); assertNull(result.getModel().getBranding()); - assertEquals(Response.Status.OK.getStatusCode(), result.getStatus().get("settings/general").getStatus()); - assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), result.getStatus().get("settings/branding").getStatus()); + assertEquals(Response.Status.OK.getStatusCode(), result.getStatus().get("general").getStatus()); + assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), result.getStatus().get("branding").getStatus()); } } diff --git a/crowd/src/test/java/com/deftdevs/bootstrapi/crowd/service/_AllServiceImplTest.java b/crowd/src/test/java/com/deftdevs/bootstrapi/crowd/service/_AllServiceImplTest.java index 2e09f792..3dd4edf1 100644 --- a/crowd/src/test/java/com/deftdevs/bootstrapi/crowd/service/_AllServiceImplTest.java +++ b/crowd/src/test/java/com/deftdevs/bootstrapi/crowd/service/_AllServiceImplTest.java @@ -118,14 +118,14 @@ void testSetAllAppliesAllFields() { final List trustedProxies = Collections.singletonList("192.168.0.1"); doReturn(new ServiceResult<>(settings, - Collections.singletonMap("settings/general", _AllModelStatus.success()))) + Collections.singletonMap("general", _AllModelStatus.success()))) .when(settingsService).setSettings(settings); doReturn(directories).when(directoriesService).setDirectories(directories); doReturn(applications).when(applicationsService).setApplications(applications); doReturn(applicationLinks).when(applicationLinksService).setApplicationLinks(applicationLinks); doReturn(redactedLicenses).when(licensesService).setLicenses(licenses); doReturn(new ServiceResult<>(mailServer, - Collections.singletonMap("mail-server/smtp", _AllModelStatus.success()))) + Collections.singletonMap("smtp", _AllModelStatus.success()))) .when(mailServerService).setMailServer(mailServer); doReturn(mailTemplates).when(mailTemplatesService).setMailTemplates(mailTemplates); doReturn(sessionConfig).when(sessionConfigService).setSessionConfig(sessionConfig); @@ -159,12 +159,12 @@ void testSetAllAppliesAllFields() { assertEquals(Response.Status.OK.getStatusCode(), status.get("settings/general").getStatus()); assertEquals(Response.Status.OK.getStatusCode(), status.get("directories").getStatus()); assertEquals(Response.Status.OK.getStatusCode(), status.get("applications").getStatus()); - assertEquals(Response.Status.OK.getStatusCode(), status.get("application-links").getStatus()); + assertEquals(Response.Status.OK.getStatusCode(), status.get("applicationLinks").getStatus()); assertEquals(Response.Status.OK.getStatusCode(), status.get("licenses").getStatus()); - assertEquals(Response.Status.OK.getStatusCode(), status.get("mail-server/smtp").getStatus()); - assertEquals(Response.Status.OK.getStatusCode(), status.get("mail-templates").getStatus()); - assertEquals(Response.Status.OK.getStatusCode(), status.get("session-config").getStatus()); - assertEquals(Response.Status.OK.getStatusCode(), status.get("trusted-proxies").getStatus()); + assertEquals(Response.Status.OK.getStatusCode(), status.get("mailServer/smtp").getStatus()); + assertEquals(Response.Status.OK.getStatusCode(), status.get("mailTemplates").getStatus()); + assertEquals(Response.Status.OK.getStatusCode(), status.get("sessionConfig").getStatus()); + assertEquals(Response.Status.OK.getStatusCode(), status.get("trustedProxies").getStatus()); } @Test @@ -175,7 +175,7 @@ void testSetAllRecordsFailureAndContinuesWithOtherFields() { Collections.singletonMap("licenseKey", LicenseModel.EXAMPLE_1); doReturn(new ServiceResult<>(settings, - Collections.singletonMap("settings/general", _AllModelStatus.success()))) + Collections.singletonMap("general", _AllModelStatus.success()))) .when(settingsService).setSettings(settings); doThrow(new WebApplicationException(Response.Status.CONFLICT)) .when(licensesService).setLicenses(licenses); diff --git a/crowd/src/test/java/it/com/deftdevs/bootstrapi/crowd/rest/_AllResourceFuncTest.java b/crowd/src/test/java/it/com/deftdevs/bootstrapi/crowd/rest/_AllResourceFuncTest.java index ccc5cdab..90e8b640 100644 --- a/crowd/src/test/java/it/com/deftdevs/bootstrapi/crowd/rest/_AllResourceFuncTest.java +++ b/crowd/src/test/java/it/com/deftdevs/bootstrapi/crowd/rest/_AllResourceFuncTest.java @@ -1,10 +1,8 @@ package it.com.deftdevs.bootstrapi.crowd.rest; -import com.deftdevs.bootstrapi.commons.constants.BootstrAPI; import com.deftdevs.bootstrapi.commons.model.MailServerModel; import com.deftdevs.bootstrapi.commons.model.MailServerSmtpModel; import com.deftdevs.bootstrapi.commons.model.SettingsGeneralModel; -import com.deftdevs.bootstrapi.commons.util.ServiceResultUtil; import com.deftdevs.bootstrapi.crowd.model.MailTemplatesModel; import com.deftdevs.bootstrapi.crowd.model.SessionConfigModel; import com.deftdevs.bootstrapi.crowd.model.SettingsModel; @@ -46,10 +44,10 @@ void testSetAllAppliesMultipleEntities() throws Exception { .build(); assertSetAllApplied(allModel, Arrays.asList( - ServiceResultUtil.subEntityKey(SettingsGeneralModel.class), - ServiceResultUtil.subEntityKey(MailServerSmtpModel.class), - ServiceResultUtil.entityType(MailTemplatesModel.class), - ServiceResultUtil.entityType(SessionConfigModel.class), - BootstrAPI.TRUSTED_PROXIES)); + "settings/general", + "mailServer/smtp", + "mailTemplates", + "sessionConfig", + "trustedProxies")); } } diff --git a/jira/Apis/AllApi.md b/jira/Apis/AllApi.md index 1c17cae5..200ebd0b 100644 --- a/jira/Apis/AllApi.md +++ b/jira/Apis/AllApi.md @@ -13,7 +13,7 @@ All URIs are relative to *https://JIRA_URL/rest/bootstrapi/1* Apply a complete configuration - Returns the updated configuration. The per-sub-field outcome is reported in the 'status' map (2xx for success, 4xx/5xx for failure with a human-readable 'message' and optional 'details'). License keys in the response are redacted (e.g. 'AAAB...wxyz#a1b2'). + Returns the updated configuration. The per-sub-field outcome is reported in the 'status' map, keyed by the request's field paths (e.g. 'settings/general', 'mailServer/smtp'; 2xx for success, 4xx/5xx for failure with a human-readable 'message' and optional 'details'). License keys in the response are redacted (e.g. 'AAAB...wxyz#a1b2'). ### Parameters diff --git a/jira/src/main/java/com/deftdevs/bootstrapi/jira/service/_AllServiceImpl.java b/jira/src/main/java/com/deftdevs/bootstrapi/jira/service/_AllServiceImpl.java index 3b154105..b7c62262 100644 --- a/jira/src/main/java/com/deftdevs/bootstrapi/jira/service/_AllServiceImpl.java +++ b/jira/src/main/java/com/deftdevs/bootstrapi/jira/service/_AllServiceImpl.java @@ -1,6 +1,5 @@ package com.deftdevs.bootstrapi.jira.service; -import com.deftdevs.bootstrapi.commons.model.PermissionsGlobalModel; import com.deftdevs.bootstrapi.commons.model.type._AllModelStatus; import com.deftdevs.bootstrapi.commons.service._AbstractAllServiceImpl; import com.deftdevs.bootstrapi.commons.service.api.ApplicationLinksService; @@ -8,7 +7,6 @@ import com.deftdevs.bootstrapi.commons.service.api.LicensesService; import com.deftdevs.bootstrapi.commons.service.api.MailServerService; import com.deftdevs.bootstrapi.commons.service.api.PermissionsService; -import com.deftdevs.bootstrapi.commons.util.ServiceResultUtil; import com.deftdevs.bootstrapi.jira.model._AllModel; import com.deftdevs.bootstrapi.jira.service.api.JiraAuthenticationService; import com.deftdevs.bootstrapi.jira.service.api.JiraSettingsService; @@ -16,9 +14,6 @@ import java.util.HashMap; import java.util.Map; -import static com.deftdevs.bootstrapi.commons.constants.BootstrAPI.APPLICATION_LINKS; -import static com.deftdevs.bootstrapi.commons.constants.BootstrAPI.DIRECTORIES; -import static com.deftdevs.bootstrapi.commons.constants.BootstrAPI.LICENSES; public class _AllServiceImpl extends _AbstractAllServiceImpl<_AllModel> { @@ -55,25 +50,25 @@ public _AllModel setAll( final _AllModel result = new _AllModel(); final Map statusMap = new HashMap<>(); - setEntityWithStatus(allModel.getSettings(), + setEntityWithStatus("settings", allModel.getSettings(), settingsService::setSettings, result::setSettings, statusMap); - setEntities(DIRECTORIES, allModel.getDirectories(), + setEntities("directories", allModel.getDirectories(), directoriesService::setDirectories, result::setDirectories, statusMap); - setEntities(APPLICATION_LINKS, allModel.getApplicationLinks(), + setEntities("applicationLinks", allModel.getApplicationLinks(), applicationLinksService::setApplicationLinks, result::setApplicationLinks, statusMap); - setEntityWithStatus(allModel.getAuthentication(), + setEntityWithStatus("authentication", allModel.getAuthentication(), authenticationService::setAuthentication, result::setAuthentication, statusMap); - setEntities(LICENSES, allModel.getLicenses(), + setEntities("licenses", allModel.getLicenses(), licensesService::setLicenses, result::setLicenses, statusMap); - setEntityWithStatus(allModel.getMailServer(), + setEntityWithStatus("mailServer", allModel.getMailServer(), mailServerService::setMailServer, result::setMailServer, statusMap); - setEntity(ServiceResultUtil.subEntityKey(PermissionsGlobalModel.class), allModel.getPermissionsGlobal(), + setEntity("permissionsGlobal", allModel.getPermissionsGlobal(), permissionsService::setPermissionsGlobal, result::setPermissionsGlobal, statusMap); result.setStatus(statusMap); diff --git a/jira/src/main/java/com/deftdevs/bootstrapi/jira/service/api/JiraSettingsService.java b/jira/src/main/java/com/deftdevs/bootstrapi/jira/service/api/JiraSettingsService.java index ed439921..ce6ec671 100644 --- a/jira/src/main/java/com/deftdevs/bootstrapi/jira/service/api/JiraSettingsService.java +++ b/jira/src/main/java/com/deftdevs/bootstrapi/jira/service/api/JiraSettingsService.java @@ -2,8 +2,8 @@ import com.deftdevs.bootstrapi.commons.model.SettingsGeneralModel; import com.deftdevs.bootstrapi.commons.model.SettingsSecurityModel; -import com.deftdevs.bootstrapi.commons.model.type._AllModelStatus; import com.deftdevs.bootstrapi.commons.model.type.ServiceResult; +import com.deftdevs.bootstrapi.commons.model.type._AllModelStatus; import com.deftdevs.bootstrapi.commons.service.api.SettingsGeneralService; import com.deftdevs.bootstrapi.commons.service.api.SettingsSecurityService; import com.deftdevs.bootstrapi.commons.util.ServiceResultUtil; @@ -30,33 +30,13 @@ default ServiceResult setSettings(final SettingsModel settingsMod final SettingsModel result = new SettingsModel(); final Map status = new LinkedHashMap<>(); - if (settingsModel.getGeneral() != null) { - final String key = ServiceResultUtil.subEntityKey(SettingsGeneralModel.class); - try { - result.setGeneral(setSettingsGeneral(settingsModel.getGeneral())); - status.put(key, _AllModelStatus.success()); - } catch (Exception e) { - status.put(key, ServiceResultUtil.toErrorStatus(key, e)); - } - } - if (settingsModel.getSecurity() != null) { - final String key = ServiceResultUtil.subEntityKey(SettingsSecurityModel.class); - try { - result.setSecurity(setSettingsSecurity(settingsModel.getSecurity())); - status.put(key, _AllModelStatus.success()); - } catch (Exception e) { - status.put(key, ServiceResultUtil.toErrorStatus(key, e)); - } - } - if (settingsModel.getBanner() != null) { - final String key = ServiceResultUtil.subEntityKey(SettingsBannerModel.class); - try { - result.setBanner(setSettingsBanner(settingsModel.getBanner())); - status.put(key, _AllModelStatus.success()); - } catch (Exception e) { - status.put(key, ServiceResultUtil.toErrorStatus(key, e)); - } - } + ServiceResultUtil.setSubEntity(status, "general", settingsModel.getGeneral(), + this::setSettingsGeneral, result::setGeneral); + ServiceResultUtil.setSubEntity(status, "security", settingsModel.getSecurity(), + this::setSettingsSecurity, result::setSecurity); + ServiceResultUtil.setSubEntity(status, "banner", settingsModel.getBanner(), + this::setSettingsBanner, result::setBanner); + return new ServiceResult<>(result, status); } diff --git a/jira/src/test/java/com/deftdevs/bootstrapi/jira/rest/_AllResourceTest.java b/jira/src/test/java/com/deftdevs/bootstrapi/jira/rest/_AllResourceTest.java index c5fe65f0..27973789 100644 --- a/jira/src/test/java/com/deftdevs/bootstrapi/jira/rest/_AllResourceTest.java +++ b/jira/src/test/java/com/deftdevs/bootstrapi/jira/rest/_AllResourceTest.java @@ -39,7 +39,7 @@ public void setup() { allModel.setSettings(settings); final Map status = new HashMap<>(); - status.put(SETTINGS + "/" + SETTINGS_GENERAL, _AllModelStatus.success()); + status.put("settings/general", _AllModelStatus.success()); status.put(DIRECTORIES, _AllModelStatus.success()); status.put(APPLICATION_LINKS, _AllModelStatus.success()); allModel.setStatus(status); @@ -62,7 +62,7 @@ public void testSetAll() { public void testSetAllReturns500WhenAnySubFieldFailedWithServerError() { final _AllModel result = new _AllModel(); final Map status = new HashMap<>(); - status.put(SETTINGS + "/" + SETTINGS_GENERAL, _AllModelStatus.success()); + status.put("settings/general", _AllModelStatus.success()); status.put(DIRECTORIES, _AllModelStatus.error(Response.Status.INTERNAL_SERVER_ERROR, "boom", null)); result.setStatus(status); @@ -76,7 +76,7 @@ public void testSetAllReturns500WhenAnySubFieldFailedWithServerError() { public void testSetAllReturns207OnMixedSuccessAndClientError() { final _AllModel result = new _AllModel(); final Map status = new HashMap<>(); - status.put(SETTINGS + "/" + SETTINGS_GENERAL, _AllModelStatus.success()); + status.put("settings/general", _AllModelStatus.success()); status.put(DIRECTORIES, _AllModelStatus.error(Response.Status.BAD_REQUEST, "bad", null)); result.setStatus(status); diff --git a/jira/src/test/java/com/deftdevs/bootstrapi/jira/service/JiraSettingsServiceTest.java b/jira/src/test/java/com/deftdevs/bootstrapi/jira/service/JiraSettingsServiceTest.java index 72e05366..88bf6a36 100644 --- a/jira/src/test/java/com/deftdevs/bootstrapi/jira/service/JiraSettingsServiceTest.java +++ b/jira/src/test/java/com/deftdevs/bootstrapi/jira/service/JiraSettingsServiceTest.java @@ -139,9 +139,9 @@ void testSetSettingsAppliesAllSubFields() { assertEquals(SettingsGeneralModel.EXAMPLE_1, result.getModel().getGeneral()); assertEquals(SettingsSecurityModel.EXAMPLE_1, result.getModel().getSecurity()); assertEquals(SettingsBannerModel.EXAMPLE_1, result.getModel().getBanner()); - assertEquals(Response.Status.OK.getStatusCode(), result.getStatus().get("settings/general").getStatus()); - assertEquals(Response.Status.OK.getStatusCode(), result.getStatus().get("settings/security").getStatus()); - assertEquals(Response.Status.OK.getStatusCode(), result.getStatus().get("settings/banner").getStatus()); + assertEquals(Response.Status.OK.getStatusCode(), result.getStatus().get("general").getStatus()); + assertEquals(Response.Status.OK.getStatusCode(), result.getStatus().get("security").getStatus()); + assertEquals(Response.Status.OK.getStatusCode(), result.getStatus().get("banner").getStatus()); } @Test @@ -167,8 +167,8 @@ void testSetSettingsRecordsPerSubFieldFailure() { assertEquals(SettingsGeneralModel.EXAMPLE_1, result.getModel().getGeneral()); assertNull(result.getModel().getBanner()); - assertEquals(Response.Status.OK.getStatusCode(), result.getStatus().get("settings/general").getStatus()); - assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), result.getStatus().get("settings/banner").getStatus()); + assertEquals(Response.Status.OK.getStatusCode(), result.getStatus().get("general").getStatus()); + assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), result.getStatus().get("banner").getStatus()); } } diff --git a/jira/src/test/java/com/deftdevs/bootstrapi/jira/service/_AllServiceImplTest.java b/jira/src/test/java/com/deftdevs/bootstrapi/jira/service/_AllServiceImplTest.java index 71496718..3dd4b66d 100644 --- a/jira/src/test/java/com/deftdevs/bootstrapi/jira/service/_AllServiceImplTest.java +++ b/jira/src/test/java/com/deftdevs/bootstrapi/jira/service/_AllServiceImplTest.java @@ -102,16 +102,16 @@ void testSetAllAppliesAllFields() { final PermissionsGlobalModel permissionsGlobal = new PermissionsGlobalModel(); doReturn(new ServiceResult<>(settings, - Collections.singletonMap("settings/general", _AllModelStatus.success()))) + Collections.singletonMap("general", _AllModelStatus.success()))) .when(settingsService).setSettings(settings); doReturn(directories).when(directoriesService).setDirectories(directories); doReturn(applicationLinks).when(applicationLinksService).setApplicationLinks(applicationLinks); doReturn(new ServiceResult<>(authentication, - Collections.singletonMap("authentication/sso", _AllModelStatus.success()))) + Collections.singletonMap("sso", _AllModelStatus.success()))) .when(authenticationService).setAuthentication(authentication); doReturn(redactedLicenses).when(licensesService).setLicenses(licenses); doReturn(new ServiceResult<>(mailServer, - Collections.singletonMap("mail-server/smtp", _AllModelStatus.success()))) + Collections.singletonMap("smtp", _AllModelStatus.success()))) .when(mailServerService).setMailServer(mailServer); doReturn(permissionsGlobal).when(permissionsService).setPermissionsGlobal(permissionsGlobal); @@ -138,11 +138,11 @@ void testSetAllAppliesAllFields() { assertEquals(7, status.size()); assertEquals(Response.Status.OK.getStatusCode(), status.get("settings/general").getStatus()); assertEquals(Response.Status.OK.getStatusCode(), status.get("directories").getStatus()); - assertEquals(Response.Status.OK.getStatusCode(), status.get("application-links").getStatus()); + assertEquals(Response.Status.OK.getStatusCode(), status.get("applicationLinks").getStatus()); assertEquals(Response.Status.OK.getStatusCode(), status.get("authentication/sso").getStatus()); assertEquals(Response.Status.OK.getStatusCode(), status.get("licenses").getStatus()); - assertEquals(Response.Status.OK.getStatusCode(), status.get("mail-server/smtp").getStatus()); - assertEquals(Response.Status.OK.getStatusCode(), status.get("permissions/global").getStatus()); + assertEquals(Response.Status.OK.getStatusCode(), status.get("mailServer/smtp").getStatus()); + assertEquals(Response.Status.OK.getStatusCode(), status.get("permissionsGlobal").getStatus()); } @Test @@ -153,7 +153,7 @@ void testSetAllRecordsFailureAndContinuesWithOtherFields() { Collections.singletonMap("licenseKey", LicenseModel.EXAMPLE_1); doReturn(new ServiceResult<>(settings, - Collections.singletonMap("settings/general", _AllModelStatus.success()))) + Collections.singletonMap("general", _AllModelStatus.success()))) .when(settingsService).setSettings(settings); doThrow(new WebApplicationException(Response.Status.CONFLICT)) .when(licensesService).setLicenses(licenses); diff --git a/jira/src/test/java/it/com/deftdevs/bootstrapi/jira/rest/_AllResourceFuncTest.java b/jira/src/test/java/it/com/deftdevs/bootstrapi/jira/rest/_AllResourceFuncTest.java index 82212de9..48c964a8 100644 --- a/jira/src/test/java/it/com/deftdevs/bootstrapi/jira/rest/_AllResourceFuncTest.java +++ b/jira/src/test/java/it/com/deftdevs/bootstrapi/jira/rest/_AllResourceFuncTest.java @@ -3,8 +3,6 @@ import com.deftdevs.bootstrapi.commons.model.MailServerModel; import com.deftdevs.bootstrapi.commons.model.MailServerPopModel; import com.deftdevs.bootstrapi.commons.model.MailServerSmtpModel; -import com.deftdevs.bootstrapi.commons.model.SettingsGeneralModel; -import com.deftdevs.bootstrapi.commons.util.ServiceResultUtil; import com.deftdevs.bootstrapi.jira.model.SettingsBannerModel; import com.deftdevs.bootstrapi.jira.model.SettingsModel; import com.deftdevs.bootstrapi.jira.model._AllModel; @@ -38,9 +36,9 @@ void testSetAllAppliesMultipleEntities() throws Exception { .build(); assertSetAllApplied(allModel, Arrays.asList( - ServiceResultUtil.subEntityKey(SettingsGeneralModel.class), - ServiceResultUtil.subEntityKey(SettingsBannerModel.class), - ServiceResultUtil.subEntityKey(MailServerSmtpModel.class), - ServiceResultUtil.subEntityKey(MailServerPopModel.class))); + "settings/general", + "settings/banner", + "mailServer/smtp", + "mailServer/pop")); } } From 6d7990ea8152b87e53a11a43af9755d38b2cdfe0 Mon Sep 17 00:00:00 2001 From: Patrick Hobusch Date: Sun, 5 Jul 2026 22:20:15 +0200 Subject: [PATCH 08/18] Aggregate the overall status as unanimous-or-207 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous aggregation was asymmetric: a 5xx part turned the whole response into a 500 even though other parts were applied, while 4xx parts in a mixed outcome yielded 207. Following RFC 4918's own convention, where a multistatus is used whenever parts end differently regardless of status class, the rule is now uniform: the unanimous code when all parts agree (all 200 → 200, all failed alike → that 4xx/5xx), 207 for any mix. --- .../rest/_AbstractAllResourceImpl.java | 66 +++++++------------ .../rest/_AbstractAllResourceImplTest.java | 18 ++++- .../confluence/rest/_AllResourceTest.java | 4 +- .../crowd/rest/_AllResourceTest.java | 4 +- .../jira/rest/_AllResourceTest.java | 4 +- 5 files changed, 45 insertions(+), 51 deletions(-) diff --git a/commons/src/main/java/com/deftdevs/bootstrapi/commons/rest/_AbstractAllResourceImpl.java b/commons/src/main/java/com/deftdevs/bootstrapi/commons/rest/_AbstractAllResourceImpl.java index 4e6bc762..e81e1926 100644 --- a/commons/src/main/java/com/deftdevs/bootstrapi/commons/rest/_AbstractAllResourceImpl.java +++ b/commons/src/main/java/com/deftdevs/bootstrapi/commons/rest/_AbstractAllResourceImpl.java @@ -33,24 +33,25 @@ public Response setAll( /** * Aggregates per-sub-field statuses into a single HTTP response code: *

    - *
  • All successful (or empty) → 200 OK.
  • - *
  • All entries share the same status code → that code.
  • - *
  • Any 5xx → 500 Internal Server Error.
  • - *
  • Mixed 2xx/4xx (or multiple distinct 4xx) → 207 Multi-Status.
  • + *
  • Empty status map → 200 OK.
  • + *
  • All entries share the same status code → that code + * (all succeeded → 200, all failed alike → that 4xx/5xx).
  • + *
  • Any mix of different codes → 207 Multi-Status.
  • *
- * 207 is used to signal "partial success" — callers must inspect the - * per-field {@code status} map in the response body to see which - * fields succeeded and which failed. + * 207 signals "outcomes differ" — callers must inspect the per-field + * {@code status} map in the response body to see which fields succeeded + * and which failed. *

* On RFC conformance: 207 Multi-Status is defined by RFC 4918 (WebDAV), - * where it implies an XML {@code multistatus} body. RFC 9110 permits any - * registered status code over HTTP, and using 207 with a JSON body - * carrying the per-part statuses is established practice for bulk-style - * REST endpoints. It was chosen deliberately over the alternatives: - * a plain 200 would hide partial failure from clients that only check the - * status code, and a 4xx/5xx would misrepresent the sub-fields that were - * applied successfully (this endpoint is not transactional). Note that - * 207 belongs to the 2xx success class, so scripted callers (e.g. + * where it implies an XML {@code multistatus} body and is likewise used + * whenever the parts of an operation end differently, whatever their + * status classes. RFC 9110 permits any registered status code over HTTP, + * and using 207 with a JSON body carrying the per-part statuses is + * established practice for bulk-style REST endpoints. A plain 200 would + * hide partial failure from clients that only check the status code, and + * a top-level 4xx/5xx would misrepresent the sub-fields that were applied + * successfully (this endpoint is not transactional). Note that 207 + * belongs to the 2xx success class, so scripted callers (e.g. * {@code curl -f}) treat partial success as success unless they inspect * the body. */ @@ -61,37 +62,14 @@ static int computeOverallStatus( return Response.Status.OK.getStatusCode(); } - boolean anyServerError = false; - boolean anyClientError = false; - boolean anySuccess = false; - Integer firstClientErrorCode = null; - boolean clientErrorCodesDiffer = false; - - for (_AllModelStatus entry : statusMap.values()) { - final int code = entry.getStatus(); - if (code >= 500) { - anyServerError = true; - } else if (code >= 400) { - anyClientError = true; - if (firstClientErrorCode == null) { - firstClientErrorCode = code; - } else if (firstClientErrorCode != code) { - clientErrorCodesDiffer = true; - } - } else if (code >= 200 && code < 300) { - anySuccess = true; - } - } - - if (anyServerError) { - return Response.Status.INTERNAL_SERVER_ERROR.getStatusCode(); - } - if (anyClientError) { - if (anySuccess || clientErrorCodesDiffer) { + Integer unanimousCode = null; + for (final _AllModelStatus entry : statusMap.values()) { + if (unanimousCode == null) { + unanimousCode = entry.getStatus(); + } else if (unanimousCode != entry.getStatus()) { return MULTI_STATUS; } - return firstClientErrorCode; } - return Response.Status.OK.getStatusCode(); + return unanimousCode; } } diff --git a/commons/src/test/java/com/deftdevs/bootstrapi/commons/rest/_AbstractAllResourceImplTest.java b/commons/src/test/java/com/deftdevs/bootstrapi/commons/rest/_AbstractAllResourceImplTest.java index f7742bb9..8adea3a7 100644 --- a/commons/src/test/java/com/deftdevs/bootstrapi/commons/rest/_AbstractAllResourceImplTest.java +++ b/commons/src/test/java/com/deftdevs/bootstrapi/commons/rest/_AbstractAllResourceImplTest.java @@ -31,14 +31,30 @@ void allSuccessYieldsOk() { } @Test - void anyServerErrorYields500() { + void mixedOutcomesIncludingServerErrorYield207() { final Map map = new LinkedHashMap<>(); map.put("a", _AllModelStatus.success()); map.put("b", _AllModelStatus.error(Response.Status.BAD_REQUEST, "x", null)); map.put("c", _AllModelStatus.error(Response.Status.INTERNAL_SERVER_ERROR, "x", null)); + assertEquals(_AbstractAllResourceImpl.MULTI_STATUS, _AbstractAllResourceImpl.computeOverallStatus(map)); + } + + @Test + void allSameServerErrorBubblesThat() { + final Map map = new LinkedHashMap<>(); + map.put("a", _AllModelStatus.error(Response.Status.INTERNAL_SERVER_ERROR, "x", null)); + map.put("b", _AllModelStatus.error(Response.Status.INTERNAL_SERVER_ERROR, "y", null)); assertEquals(500, _AbstractAllResourceImpl.computeOverallStatus(map)); } + @Test + void mixedSuccessAndServerErrorYields207() { + final Map map = new LinkedHashMap<>(); + map.put("a", _AllModelStatus.success()); + map.put("b", _AllModelStatus.error(Response.Status.INTERNAL_SERVER_ERROR, "x", null)); + assertEquals(_AbstractAllResourceImpl.MULTI_STATUS, _AbstractAllResourceImpl.computeOverallStatus(map)); + } + @Test void mixedSuccessAndClientErrorYields207() { final Map map = new LinkedHashMap<>(); diff --git a/confluence/src/test/java/com/deftdevs/bootstrapi/confluence/rest/_AllResourceTest.java b/confluence/src/test/java/com/deftdevs/bootstrapi/confluence/rest/_AllResourceTest.java index 03827a6c..95d792de 100644 --- a/confluence/src/test/java/com/deftdevs/bootstrapi/confluence/rest/_AllResourceTest.java +++ b/confluence/src/test/java/com/deftdevs/bootstrapi/confluence/rest/_AllResourceTest.java @@ -59,7 +59,7 @@ public void testSetAll() { } @Test - public void testSetAllReturns500WhenAnySubFieldFailedWithServerError() { + public void testSetAllReturns207OnMixedSuccessAndServerError() { final _AllModel result = new _AllModel(); final Map status = new HashMap<>(); status.put("settings/general", _AllModelStatus.success()); @@ -69,7 +69,7 @@ public void testSetAllReturns500WhenAnySubFieldFailedWithServerError() { doReturn(result).when(allService).setAll(any()); final Response response = allResource.setAll(allModel); - assertEquals(500, response.getStatus()); + assertEquals(207, response.getStatus()); } @Test diff --git a/crowd/src/test/java/com/deftdevs/bootstrapi/crowd/rest/_AllResourceTest.java b/crowd/src/test/java/com/deftdevs/bootstrapi/crowd/rest/_AllResourceTest.java index dda04f16..6b781de3 100644 --- a/crowd/src/test/java/com/deftdevs/bootstrapi/crowd/rest/_AllResourceTest.java +++ b/crowd/src/test/java/com/deftdevs/bootstrapi/crowd/rest/_AllResourceTest.java @@ -64,7 +64,7 @@ public void testSetAll() { } @Test - public void testSetAllReturns500WhenAnySubFieldFailedWithServerError() { + public void testSetAllReturns207OnMixedSuccessAndServerError() { final _AllModel result = new _AllModel(); final Map status = new HashMap<>(); status.put("settings/general", _AllModelStatus.success()); @@ -74,7 +74,7 @@ public void testSetAllReturns500WhenAnySubFieldFailedWithServerError() { doReturn(result).when(allService).setAll(any()); final Response response = allResource.setAll(allModel); - assertEquals(500, response.getStatus()); + assertEquals(207, response.getStatus()); } @Test diff --git a/jira/src/test/java/com/deftdevs/bootstrapi/jira/rest/_AllResourceTest.java b/jira/src/test/java/com/deftdevs/bootstrapi/jira/rest/_AllResourceTest.java index 27973789..bba239e0 100644 --- a/jira/src/test/java/com/deftdevs/bootstrapi/jira/rest/_AllResourceTest.java +++ b/jira/src/test/java/com/deftdevs/bootstrapi/jira/rest/_AllResourceTest.java @@ -59,7 +59,7 @@ public void testSetAll() { } @Test - public void testSetAllReturns500WhenAnySubFieldFailedWithServerError() { + public void testSetAllReturns207OnMixedSuccessAndServerError() { final _AllModel result = new _AllModel(); final Map status = new HashMap<>(); status.put("settings/general", _AllModelStatus.success()); @@ -69,7 +69,7 @@ public void testSetAllReturns500WhenAnySubFieldFailedWithServerError() { doReturn(result).when(allService).setAll(any()); final Response response = allResource.setAll(allModel); - assertEquals(500, response.getStatus()); + assertEquals(207, response.getStatus()); } @Test From 3c6790b2073943539e88d2053a448df00ed82607 Mon Sep 17 00:00:00 2001 From: Patrick Hobusch Date: Mon, 6 Jul 2026 09:18:16 +0200 Subject: [PATCH 09/18] Extract commons base models for the _all and settings hierarchies _AbstractAllModel carries the configuration fields every product supports (settings via type parameter, directories, applicationLinks, licenses, mailServer) plus the response status map; the Confluence and Jira _AllModel were byte-identical and now only add their product-specific fields, as does Crowd. AbstractSettingsModel is the canonical settings group schema (general, security, branding) extended by Confluence; Jira and Crowd keep their reduced field sets. The affected models move from @Builder to @SuperBuilder, so the two remaining all-args constructions become builders. --- .../commons/model/AbstractSettingsModel.java | 30 +++++++++++++ .../commons/model/_AbstractAllModel.java | 42 +++++++++++++++++++ .../confluence/model/SettingsModel.java | 23 +++------- .../confluence/model/_AllModel.java | 36 +++------------- .../api/ConfluenceSettingsService.java | 8 +++- .../ConfluenceSettingsServiceTest.java | 20 ++++----- .../bootstrapi/crowd/model/_AllModel.java | 35 +++------------- .../bootstrapi/jira/model/_AllModel.java | 36 +++------------- 8 files changed, 112 insertions(+), 118 deletions(-) create mode 100644 commons/src/main/java/com/deftdevs/bootstrapi/commons/model/AbstractSettingsModel.java create mode 100644 commons/src/main/java/com/deftdevs/bootstrapi/commons/model/_AbstractAllModel.java diff --git a/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/AbstractSettingsModel.java b/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/AbstractSettingsModel.java new file mode 100644 index 00000000..ee2b478e --- /dev/null +++ b/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/AbstractSettingsModel.java @@ -0,0 +1,30 @@ +package com.deftdevs.bootstrapi.commons.model; + +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +import javax.xml.bind.annotation.XmlElement; + +/** + * Canonical settings group schema. Products extend it with the sub-fields + * they support; sub-models declare their position in the group via + * {@code @SubEntityOf(AbstractSettingsModel.class)} even when a product's + * concrete settings model does not extend this class, since the field names + * are aligned by construction. + */ +@Data +@SuperBuilder +@NoArgsConstructor +public abstract class AbstractSettingsModel { + + @XmlElement + private SettingsGeneralModel general; + + @XmlElement + private SettingsSecurityModel security; + + @XmlElement + private SettingsBrandingColorSchemeModel branding; + +} diff --git a/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/_AbstractAllModel.java b/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/_AbstractAllModel.java new file mode 100644 index 00000000..d2fb85fd --- /dev/null +++ b/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/_AbstractAllModel.java @@ -0,0 +1,42 @@ +package com.deftdevs.bootstrapi.commons.model; + +import com.deftdevs.bootstrapi.commons.model.type._AllModelAccessor; +import com.deftdevs.bootstrapi.commons.model.type._AllModelStatus; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +import javax.xml.bind.annotation.XmlElement; +import java.util.Map; + +/** + * Shared shape of the product _all models: the configuration fields every + * product supports plus the per-sub-field status map of the response. + * Products extend it with their product-specific fields. + * + * @param the product's settings model type + */ +@Data +@SuperBuilder +@NoArgsConstructor +public abstract class _AbstractAllModel implements _AllModelAccessor { + + @XmlElement + private S settings; + + @XmlElement + private Map directories; + + @XmlElement + private Map applicationLinks; + + @XmlElement + private Map licenses; + + @XmlElement + private MailServerModel mailServer; + + @XmlElement + private Map status; + +} diff --git a/confluence/src/main/java/com/deftdevs/bootstrapi/confluence/model/SettingsModel.java b/confluence/src/main/java/com/deftdevs/bootstrapi/confluence/model/SettingsModel.java index 9da3af29..ab097737 100644 --- a/confluence/src/main/java/com/deftdevs/bootstrapi/confluence/model/SettingsModel.java +++ b/confluence/src/main/java/com/deftdevs/bootstrapi/confluence/model/SettingsModel.java @@ -1,12 +1,10 @@ package com.deftdevs.bootstrapi.confluence.model; -import com.deftdevs.bootstrapi.commons.model.SettingsBrandingColorSchemeModel; -import com.deftdevs.bootstrapi.commons.model.SettingsGeneralModel; -import com.deftdevs.bootstrapi.commons.model.SettingsSecurityModel; -import lombok.AllArgsConstructor; -import lombok.Builder; +import com.deftdevs.bootstrapi.commons.model.AbstractSettingsModel; import lombok.Data; +import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; import javax.xml.bind.annotation.XmlElement; import javax.xml.bind.annotation.XmlRootElement; @@ -14,20 +12,11 @@ import static com.deftdevs.bootstrapi.commons.constants.BootstrAPI.SETTINGS; @Data -@Builder +@SuperBuilder @NoArgsConstructor -@AllArgsConstructor +@EqualsAndHashCode(callSuper = true) @XmlRootElement(name = SETTINGS) -public class SettingsModel { - - @XmlElement - private SettingsGeneralModel general; - - @XmlElement - private SettingsSecurityModel security; - - @XmlElement - private SettingsBrandingColorSchemeModel branding; +public class SettingsModel extends AbstractSettingsModel { @XmlElement private SettingsCustomHtmlModel customHtml; diff --git a/confluence/src/main/java/com/deftdevs/bootstrapi/confluence/model/_AllModel.java b/confluence/src/main/java/com/deftdevs/bootstrapi/confluence/model/_AllModel.java index a9131cce..eecc2e79 100644 --- a/confluence/src/main/java/com/deftdevs/bootstrapi/confluence/model/_AllModel.java +++ b/confluence/src/main/java/com/deftdevs/bootstrapi/confluence/model/_AllModel.java @@ -1,52 +1,28 @@ package com.deftdevs.bootstrapi.confluence.model; import com.deftdevs.bootstrapi.commons.constants.BootstrAPI; -import com.deftdevs.bootstrapi.commons.model.AbstractDirectoryModel; -import com.deftdevs.bootstrapi.commons.model.ApplicationLinkModel; +import com.deftdevs.bootstrapi.commons.model._AbstractAllModel; import com.deftdevs.bootstrapi.commons.model.AuthenticationModel; -import com.deftdevs.bootstrapi.commons.model.LicenseModel; -import com.deftdevs.bootstrapi.commons.model.MailServerModel; import com.deftdevs.bootstrapi.commons.model.PermissionsGlobalModel; -import com.deftdevs.bootstrapi.commons.model.type._AllModelAccessor; -import com.deftdevs.bootstrapi.commons.model.type._AllModelStatus; -import lombok.AllArgsConstructor; -import lombok.Builder; import lombok.Data; +import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; import javax.xml.bind.annotation.XmlElement; import javax.xml.bind.annotation.XmlRootElement; -import java.util.Map; @Data -@Builder +@SuperBuilder @NoArgsConstructor -@AllArgsConstructor +@EqualsAndHashCode(callSuper = true) @XmlRootElement(name = BootstrAPI._ALL) -public class _AllModel implements _AllModelAccessor { - - @XmlElement - private SettingsModel settings; - - @XmlElement - private Map directories; - - @XmlElement - private Map applicationLinks; +public class _AllModel extends _AbstractAllModel { @XmlElement private AuthenticationModel authentication; - @XmlElement - private Map licenses; - - @XmlElement - private MailServerModel mailServer; - @XmlElement private PermissionsGlobalModel permissionsGlobal; - @XmlElement - private Map status; - } diff --git a/confluence/src/main/java/com/deftdevs/bootstrapi/confluence/service/api/ConfluenceSettingsService.java b/confluence/src/main/java/com/deftdevs/bootstrapi/confluence/service/api/ConfluenceSettingsService.java index 64577b24..8a64e20a 100644 --- a/confluence/src/main/java/com/deftdevs/bootstrapi/confluence/service/api/ConfluenceSettingsService.java +++ b/confluence/src/main/java/com/deftdevs/bootstrapi/confluence/service/api/ConfluenceSettingsService.java @@ -25,8 +25,12 @@ SettingsCustomHtmlModel setCustomHtml( SettingsCustomHtmlModel settingsCustomHtmlModel); default SettingsModel getSettings() { - return new SettingsModel(getSettingsGeneral(), getSettingsSecurity(), - getColourScheme(), getCustomHtml()); + return SettingsModel.builder() + .general(getSettingsGeneral()) + .security(getSettingsSecurity()) + .branding(getColourScheme()) + .customHtml(getCustomHtml()) + .build(); } default ServiceResult setSettings(final SettingsModel settingsModel) { diff --git a/confluence/src/test/java/com/deftdevs/bootstrapi/confluence/service/ConfluenceSettingsServiceTest.java b/confluence/src/test/java/com/deftdevs/bootstrapi/confluence/service/ConfluenceSettingsServiceTest.java index 9ada7180..c7cfd1bb 100644 --- a/confluence/src/test/java/com/deftdevs/bootstrapi/confluence/service/ConfluenceSettingsServiceTest.java +++ b/confluence/src/test/java/com/deftdevs/bootstrapi/confluence/service/ConfluenceSettingsServiceTest.java @@ -64,11 +64,12 @@ void testSetSettingsAppliesAllSubFields() { doReturn(SettingsBrandingColorSchemeModel.EXAMPLE_1).when(settingsBrandingService).setColourScheme(SettingsBrandingColorSchemeModel.EXAMPLE_1); doReturn(SettingsCustomHtmlModel.EXAMPLE_1).when(settingsService).setCustomHtml(SettingsCustomHtmlModel.EXAMPLE_1); - final ServiceResult result = settingsService.setSettings(new SettingsModel( - SettingsGeneralModel.EXAMPLE_1, - SettingsSecurityModel.EXAMPLE_1, - SettingsBrandingColorSchemeModel.EXAMPLE_1, - SettingsCustomHtmlModel.EXAMPLE_1)); + final ServiceResult result = settingsService.setSettings(SettingsModel.builder() + .general(SettingsGeneralModel.EXAMPLE_1) + .security(SettingsSecurityModel.EXAMPLE_1) + .branding(SettingsBrandingColorSchemeModel.EXAMPLE_1) + .customHtml(SettingsCustomHtmlModel.EXAMPLE_1) + .build()); assertEquals(SettingsGeneralModel.EXAMPLE_1, result.getModel().getGeneral()); assertEquals(SettingsSecurityModel.EXAMPLE_1, result.getModel().getSecurity()); @@ -95,11 +96,10 @@ void testSetSettingsRecordsPerSubFieldFailure() { doThrow(new BadRequestException("invalid colour scheme")) .when(settingsBrandingService).setColourScheme(SettingsBrandingColorSchemeModel.EXAMPLE_1); - final ServiceResult result = settingsService.setSettings(new SettingsModel( - SettingsGeneralModel.EXAMPLE_1, - null, - SettingsBrandingColorSchemeModel.EXAMPLE_1, - null)); + final ServiceResult result = settingsService.setSettings(SettingsModel.builder() + .general(SettingsGeneralModel.EXAMPLE_1) + .branding(SettingsBrandingColorSchemeModel.EXAMPLE_1) + .build()); assertEquals(SettingsGeneralModel.EXAMPLE_1, result.getModel().getGeneral()); assertNull(result.getModel().getBranding()); diff --git a/crowd/src/main/java/com/deftdevs/bootstrapi/crowd/model/_AllModel.java b/crowd/src/main/java/com/deftdevs/bootstrapi/crowd/model/_AllModel.java index ab258631..1c4a4142 100644 --- a/crowd/src/main/java/com/deftdevs/bootstrapi/crowd/model/_AllModel.java +++ b/crowd/src/main/java/com/deftdevs/bootstrapi/crowd/model/_AllModel.java @@ -1,16 +1,11 @@ package com.deftdevs.bootstrapi.crowd.model; import com.deftdevs.bootstrapi.commons.constants.BootstrAPI; -import com.deftdevs.bootstrapi.commons.model.AbstractDirectoryModel; -import com.deftdevs.bootstrapi.commons.model.ApplicationLinkModel; -import com.deftdevs.bootstrapi.commons.model.LicenseModel; -import com.deftdevs.bootstrapi.commons.model.MailServerModel; -import com.deftdevs.bootstrapi.commons.model.type._AllModelAccessor; -import com.deftdevs.bootstrapi.commons.model.type._AllModelStatus; -import lombok.AllArgsConstructor; -import lombok.Builder; +import com.deftdevs.bootstrapi.commons.model._AbstractAllModel; import lombok.Data; +import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; import javax.xml.bind.annotation.XmlElement; import javax.xml.bind.annotation.XmlRootElement; @@ -18,30 +13,15 @@ import java.util.Map; @Data -@Builder +@SuperBuilder @NoArgsConstructor -@AllArgsConstructor +@EqualsAndHashCode(callSuper = true) @XmlRootElement(name = BootstrAPI._ALL) -public class _AllModel implements _AllModelAccessor { - - @XmlElement - private SettingsModel settings; - - @XmlElement - private Map directories; +public class _AllModel extends _AbstractAllModel { @XmlElement private Map applications; - @XmlElement - private Map applicationLinks; - - @XmlElement - private Map licenses; - - @XmlElement - private MailServerModel mailServer; - @XmlElement private MailTemplatesModel mailTemplates; @@ -51,7 +31,4 @@ public class _AllModel implements _AllModelAccessor { @XmlElement private List trustedProxies; - @XmlElement - private Map status; - } diff --git a/jira/src/main/java/com/deftdevs/bootstrapi/jira/model/_AllModel.java b/jira/src/main/java/com/deftdevs/bootstrapi/jira/model/_AllModel.java index eb49dfba..640565c9 100644 --- a/jira/src/main/java/com/deftdevs/bootstrapi/jira/model/_AllModel.java +++ b/jira/src/main/java/com/deftdevs/bootstrapi/jira/model/_AllModel.java @@ -1,52 +1,28 @@ package com.deftdevs.bootstrapi.jira.model; import com.deftdevs.bootstrapi.commons.constants.BootstrAPI; -import com.deftdevs.bootstrapi.commons.model.AbstractDirectoryModel; -import com.deftdevs.bootstrapi.commons.model.ApplicationLinkModel; +import com.deftdevs.bootstrapi.commons.model._AbstractAllModel; import com.deftdevs.bootstrapi.commons.model.AuthenticationModel; -import com.deftdevs.bootstrapi.commons.model.LicenseModel; -import com.deftdevs.bootstrapi.commons.model.MailServerModel; import com.deftdevs.bootstrapi.commons.model.PermissionsGlobalModel; -import com.deftdevs.bootstrapi.commons.model.type._AllModelAccessor; -import com.deftdevs.bootstrapi.commons.model.type._AllModelStatus; -import lombok.AllArgsConstructor; -import lombok.Builder; import lombok.Data; +import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; import javax.xml.bind.annotation.XmlElement; import javax.xml.bind.annotation.XmlRootElement; -import java.util.Map; @Data -@Builder +@SuperBuilder @NoArgsConstructor -@AllArgsConstructor +@EqualsAndHashCode(callSuper = true) @XmlRootElement(name = BootstrAPI._ALL) -public class _AllModel implements _AllModelAccessor { - - @XmlElement - private SettingsModel settings; - - @XmlElement - private Map directories; - - @XmlElement - private Map applicationLinks; +public class _AllModel extends _AbstractAllModel { @XmlElement private AuthenticationModel authentication; - @XmlElement - private Map licenses; - - @XmlElement - private MailServerModel mailServer; - @XmlElement private PermissionsGlobalModel permissionsGlobal; - @XmlElement - private Map status; - } From 751f37e60180a27465fea77ee589a2dbd454cb81 Mon Sep 17 00:00:00 2001 From: Patrick Hobusch Date: Mon, 6 Jul 2026 09:18:45 +0200 Subject: [PATCH 10/18] Derive status-map keys from the models via @SubEntityOf Sub-models declare their parent with @SubEntityOf(Parent.class); FieldNames resolves the serialized field name reflectively from the parent's (possibly inherited and generic) field declarations, so a composite update records a sub-field's status by passing the sub-model class alone: setSubEntity(status, MailServerPopModel.class, ...). The _all base service derives its top-level field names the same way from the concrete _AllModel bound by the product subclass, and prefixes composite sub-statuses with the composite's field name. No string keys, field constants or annotations with name segments remain anywhere; tests derive expected keys with FieldNames.pathOf(_AllModel.class, leaf.class), and any model change that breaks or ambiguates a derivation fails fast in the unit tests. --- .../model/AbstractAuthenticationIdpModel.java | 2 + .../commons/model/AuthenticationSsoModel.java | 2 + .../commons/model/MailServerPopModel.java | 2 + .../commons/model/MailServerSmtpModel.java | 2 + .../SettingsBrandingColorSchemeModel.java | 2 + .../commons/model/SettingsGeneralModel.java | 2 + .../commons/model/SettingsSecurityModel.java | 2 + .../commons/model/type/SubEntityOf.java | 25 +++ .../service/_AbstractAllServiceImpl.java | 41 +++- .../service/api/AuthenticationService.java | 4 +- .../service/api/MailServerService.java | 4 +- .../bootstrapi/commons/util/FieldNames.java | 210 ++++++++++++++++++ .../commons/util/ServiceResultUtil.java | 15 +- .../service/_AbstractAllServiceImplTest.java | 89 +++++--- .../api/AuthenticationServiceTest.java | 9 +- .../service/api/MailServerServiceTest.java | 9 +- .../commons/util/FieldNamesTest.java | 90 ++++++++ .../commons/util/ServiceResultUtilTest.java | 27 ++- .../rest/Abstract_AllResourceFuncTest.java | 20 +- .../model/SettingsCustomHtmlModel.java | 2 + .../confluence/service/_AllServiceImpl.java | 21 +- .../api/ConfluenceSettingsService.java | 9 +- .../confluence/rest/_AllResourceTest.java | 9 +- .../ConfluenceSettingsServiceTest.java | 13 +- .../service/_AllServiceImplTest.java | 28 +-- .../confluence/rest/_AllResourceFuncTest.java | 14 +- .../model/SettingsBrandingLoginPageModel.java | 2 + .../crowd/service/_AllServiceImpl.java | 26 ++- .../service/api/CrowdSettingsService.java | 6 +- .../crowd/rest/_AllResourceTest.java | 9 +- .../service/CrowdSettingsServiceTest.java | 9 +- .../crowd/service/_AllServiceImplTest.java | 29 +-- .../crowd/rest/_AllResourceFuncTest.java | 16 +- .../jira/model/SettingsBannerModel.java | 2 + .../jira/service/_AllServiceImpl.java | 21 +- .../jira/service/api/JiraSettingsService.java | 6 +- .../jira/rest/_AllResourceTest.java | 9 +- .../jira/service/JiraSettingsServiceTest.java | 11 +- .../jira/service/_AllServiceImplTest.java | 28 +-- .../jira/rest/_AllResourceFuncTest.java | 15 +- 40 files changed, 660 insertions(+), 182 deletions(-) create mode 100644 commons/src/main/java/com/deftdevs/bootstrapi/commons/model/type/SubEntityOf.java create mode 100644 commons/src/main/java/com/deftdevs/bootstrapi/commons/util/FieldNames.java create mode 100644 commons/src/test/java/com/deftdevs/bootstrapi/commons/util/FieldNamesTest.java diff --git a/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/AbstractAuthenticationIdpModel.java b/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/AbstractAuthenticationIdpModel.java index ba5828d8..9de7bf9a 100644 --- a/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/AbstractAuthenticationIdpModel.java +++ b/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/AbstractAuthenticationIdpModel.java @@ -10,6 +10,7 @@ import lombok.experimental.SuperBuilder; import javax.xml.bind.annotation.XmlElement; +import com.deftdevs.bootstrapi.commons.model.type.SubEntityOf; import javax.xml.bind.annotation.XmlRootElement; @@ -17,6 +18,7 @@ @SuperBuilder @NoArgsConstructor @AllArgsConstructor +@SubEntityOf(AuthenticationModel.class) @XmlRootElement @JsonTypeInfo( use = JsonTypeInfo.Id.NAME, diff --git a/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/AuthenticationSsoModel.java b/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/AuthenticationSsoModel.java index 24e8b5b6..b9d96cfa 100644 --- a/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/AuthenticationSsoModel.java +++ b/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/AuthenticationSsoModel.java @@ -7,12 +7,14 @@ import lombok.NoArgsConstructor; import javax.xml.bind.annotation.XmlElement; +import com.deftdevs.bootstrapi.commons.model.type.SubEntityOf; import javax.xml.bind.annotation.XmlRootElement; @Data @Builder @NoArgsConstructor @AllArgsConstructor +@SubEntityOf(AuthenticationModel.class) @XmlRootElement(name = BootstrAPI.AUTHENTICATION + "-" + BootstrAPI.AUTHENTICATION_SSO) public class AuthenticationSsoModel { diff --git a/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/MailServerPopModel.java b/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/MailServerPopModel.java index 82cc46c1..2a1bda69 100644 --- a/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/MailServerPopModel.java +++ b/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/MailServerPopModel.java @@ -6,12 +6,14 @@ import lombok.NoArgsConstructor; import lombok.experimental.SuperBuilder; +import com.deftdevs.bootstrapi.commons.model.type.SubEntityOf; import javax.xml.bind.annotation.XmlRootElement; @Data @SuperBuilder @NoArgsConstructor @EqualsAndHashCode(callSuper = true) +@SubEntityOf(MailServerModel.class) @XmlRootElement(name = BootstrAPI.MAIL_SERVER + "-" + BootstrAPI.MAIL_SERVER_POP) public class MailServerPopModel extends AbstractMailServerProtocolModel { diff --git a/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/MailServerSmtpModel.java b/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/MailServerSmtpModel.java index 0881ba1d..5f7aef5a 100644 --- a/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/MailServerSmtpModel.java +++ b/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/MailServerSmtpModel.java @@ -8,6 +8,7 @@ import lombok.experimental.SuperBuilder; import javax.xml.bind.annotation.XmlElement; +import com.deftdevs.bootstrapi.commons.model.type.SubEntityOf; import javax.xml.bind.annotation.XmlRootElement; @Data @@ -15,6 +16,7 @@ @NoArgsConstructor @AllArgsConstructor @EqualsAndHashCode(callSuper = true) +@SubEntityOf(MailServerModel.class) @XmlRootElement(name = BootstrAPI.MAIL_SERVER + "-" + BootstrAPI.MAIL_SERVER_SMTP) public class MailServerSmtpModel extends AbstractMailServerProtocolModel { diff --git a/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/SettingsBrandingColorSchemeModel.java b/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/SettingsBrandingColorSchemeModel.java index 29fe6548..344719b8 100644 --- a/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/SettingsBrandingColorSchemeModel.java +++ b/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/SettingsBrandingColorSchemeModel.java @@ -8,6 +8,7 @@ import javax.xml.bind.annotation.XmlAccessType; import javax.xml.bind.annotation.XmlAccessorType; import javax.xml.bind.annotation.XmlElement; +import com.deftdevs.bootstrapi.commons.model.type.SubEntityOf; import javax.xml.bind.annotation.XmlRootElement; import static com.deftdevs.bootstrapi.commons.constants.BootstrAPI.*; @@ -16,6 +17,7 @@ @Builder @NoArgsConstructor @AllArgsConstructor +@SubEntityOf(AbstractSettingsModel.class) @XmlRootElement(name = SETTINGS + "-" + SETTINGS_BRANDING + "-" + COLOR_SCHEME) @XmlAccessorType(XmlAccessType.FIELD) public class SettingsBrandingColorSchemeModel { diff --git a/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/SettingsGeneralModel.java b/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/SettingsGeneralModel.java index ca330741..5beecce9 100644 --- a/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/SettingsGeneralModel.java +++ b/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/SettingsGeneralModel.java @@ -9,6 +9,7 @@ import javax.xml.bind.annotation.XmlAccessType; import javax.xml.bind.annotation.XmlAccessorType; import javax.xml.bind.annotation.XmlElement; +import com.deftdevs.bootstrapi.commons.model.type.SubEntityOf; import javax.xml.bind.annotation.XmlRootElement; import java.net.URI; @@ -19,6 +20,7 @@ @Builder @NoArgsConstructor @AllArgsConstructor +@SubEntityOf(AbstractSettingsModel.class) @XmlRootElement(name = SETTINGS + "-" + SETTINGS_GENERAL) @XmlAccessorType(XmlAccessType.FIELD) public class SettingsGeneralModel { diff --git a/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/SettingsSecurityModel.java b/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/SettingsSecurityModel.java index 8e41cf35..bdc08518 100644 --- a/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/SettingsSecurityModel.java +++ b/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/SettingsSecurityModel.java @@ -8,6 +8,7 @@ import javax.xml.bind.annotation.XmlAccessType; import javax.xml.bind.annotation.XmlAccessorType; import javax.xml.bind.annotation.XmlElement; +import com.deftdevs.bootstrapi.commons.model.type.SubEntityOf; import javax.xml.bind.annotation.XmlRootElement; import static com.deftdevs.bootstrapi.commons.constants.BootstrAPI.SETTINGS; @@ -17,6 +18,7 @@ @SuperBuilder @NoArgsConstructor @AllArgsConstructor +@SubEntityOf(AbstractSettingsModel.class) @XmlRootElement(name = SETTINGS + "-" + SETTINGS_SECURITY) @XmlAccessorType(XmlAccessType.FIELD) public class SettingsSecurityModel { diff --git a/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/type/SubEntityOf.java b/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/type/SubEntityOf.java new file mode 100644 index 00000000..7877e9c0 --- /dev/null +++ b/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/type/SubEntityOf.java @@ -0,0 +1,25 @@ +package com.deftdevs.bootstrapi.commons.model.type; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Declares the parent model containing this model as a sub-field, so that + * status-map keys can be derived from a sub-model class alone by walking up + * the model hierarchy (e.g. {@code MailServerPopModel} → field {@code pop} + * of {@code MailServerModel}). The parent must be a commons-visible model + * declaring a field of the annotated type; product models that mirror the + * canonical parent's fields share its keys by construction. + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface SubEntityOf { + + /** The parent model class declaring a field of the annotated type. */ + Class value(); + +} diff --git a/commons/src/main/java/com/deftdevs/bootstrapi/commons/service/_AbstractAllServiceImpl.java b/commons/src/main/java/com/deftdevs/bootstrapi/commons/service/_AbstractAllServiceImpl.java index addd018c..8b88a674 100644 --- a/commons/src/main/java/com/deftdevs/bootstrapi/commons/service/_AbstractAllServiceImpl.java +++ b/commons/src/main/java/com/deftdevs/bootstrapi/commons/service/_AbstractAllServiceImpl.java @@ -4,8 +4,11 @@ import com.deftdevs.bootstrapi.commons.model.type._AllModelAccessor; import com.deftdevs.bootstrapi.commons.model.type._AllModelStatus; import com.deftdevs.bootstrapi.commons.service.api._AllService; +import com.deftdevs.bootstrapi.commons.util.FieldNames; import com.deftdevs.bootstrapi.commons.util.ServiceResultUtil; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; import java.util.LinkedHashMap; import java.util.Map; import java.util.function.Consumer; @@ -13,15 +16,40 @@ /** * Base class for the product _all services. Status-map keys mirror the JSON - * structure of the request: top-level fields are recorded under their field - * name ({@code directories}, {@code permissionsGlobal}), and the sub-field + * structure of the request and are derived from the model classes via + * {@link FieldNames}: top-level fields are recorded under their field name + * ({@code directories}, {@code permissionsGlobal}), and the sub-field * statuses returned by composite services are prefixed with the composite's * field name ({@code settings/general}, {@code mailServer/smtp}). */ public abstract class _AbstractAllServiceImpl<_AllModel extends _AllModelAccessor> implements _AllService<_AllModel> { + private final Class allModelClass = resolveAllModelClass(); + + /** + * Resolves the concrete _AllModel class from the subclass's declaration + * ({@code class _AllServiceImpl extends _AbstractAllServiceImpl<_AllModel>}), + * where the type argument is reified despite erasure. + */ + private Class resolveAllModelClass() { + Class subclass = getClass(); + while (subclass.getSuperclass() != _AbstractAllServiceImpl.class) { + subclass = subclass.getSuperclass(); + } + + final Type superclass = subclass.getGenericSuperclass(); + if (superclass instanceof ParameterizedType) { + final Type typeArgument = ((ParameterizedType) superclass).getActualTypeArguments()[0]; + if (typeArgument instanceof Class) { + return (Class) typeArgument; + } + } + throw new IllegalStateException(getClass().getName() + + " must bind the _AllModel type parameter to a concrete model class"); + } + protected void setEntityWithStatus( - final String field, + final Class fieldType, final I input, final Function> updateFunction, final Consumer resultConsumer, @@ -31,6 +59,7 @@ protected void setEntityWithStatus( return; } + final String field = FieldNames.of(allModelClass, fieldType); try { final ServiceResult serviceResult = updateFunction.apply(input); resultConsumer.accept(serviceResult.getModel()); @@ -46,7 +75,7 @@ protected void setEntityWithStatus( } protected _AllModelStatus setEntity( - final String field, + final Class fieldType, final I input, final Function updateFunction, final Consumer resultConsumer, @@ -56,6 +85,7 @@ protected _AllModelStatus setEntity( return null; } + final String field = FieldNames.of(allModelClass, fieldType); try { final O updatedEntity = updateFunction.apply(input); resultConsumer.accept(updatedEntity); @@ -70,7 +100,7 @@ protected _AllModelStatus setEntity( } protected _AllModelStatus setEntities( - final String field, + final Class entityType, final Map entityMap, final Function, Map> updateFunction, final Consumer> resultConsumer, @@ -80,6 +110,7 @@ protected _AllModelStatus setEntities( return null; } + final String field = FieldNames.of(allModelClass, entityType); try { final Map updatedEntities = updateFunction.apply(entityMap); resultConsumer.accept(new LinkedHashMap<>(updatedEntities)); diff --git a/commons/src/main/java/com/deftdevs/bootstrapi/commons/service/api/AuthenticationService.java b/commons/src/main/java/com/deftdevs/bootstrapi/commons/service/api/AuthenticationService.java index 81df15c6..0bb391cb 100644 --- a/commons/src/main/java/com/deftdevs/bootstrapi/commons/service/api/AuthenticationService.java +++ b/commons/src/main/java/com/deftdevs/bootstrapi/commons/service/api/AuthenticationService.java @@ -22,9 +22,9 @@ default ServiceResult setAuthentication(final Authenticatio final AuthenticationModel result = new AuthenticationModel(); final Map status = new LinkedHashMap<>(); - ServiceResultUtil.setSubEntity(status, "idps", authenticationModel.getIdps(), + ServiceResultUtil.setSubEntity(status, AbstractAuthenticationIdpModel.class, authenticationModel.getIdps(), idps -> new LinkedHashMap<>(setAuthenticationIdps(idps)), result::setIdps); - ServiceResultUtil.setSubEntity(status, "sso", authenticationModel.getSso(), + ServiceResultUtil.setSubEntity(status, AuthenticationSsoModel.class, authenticationModel.getSso(), this::setAuthenticationSso, result::setSso); return new ServiceResult<>(result, status); diff --git a/commons/src/main/java/com/deftdevs/bootstrapi/commons/service/api/MailServerService.java b/commons/src/main/java/com/deftdevs/bootstrapi/commons/service/api/MailServerService.java index 6bee120c..c05e0648 100644 --- a/commons/src/main/java/com/deftdevs/bootstrapi/commons/service/api/MailServerService.java +++ b/commons/src/main/java/com/deftdevs/bootstrapi/commons/service/api/MailServerService.java @@ -20,9 +20,9 @@ default ServiceResult setMailServer(final MailServerModel mailS final MailServerModel result = new MailServerModel(); final Map status = new LinkedHashMap<>(); - ServiceResultUtil.setSubEntity(status, "smtp", mailServerModel.getSmtp(), + ServiceResultUtil.setSubEntity(status, MailServerSmtpModel.class, mailServerModel.getSmtp(), this::setMailServerSmtp, result::setSmtp); - ServiceResultUtil.setSubEntity(status, "pop", mailServerModel.getPop(), + ServiceResultUtil.setSubEntity(status, MailServerPopModel.class, mailServerModel.getPop(), this::setMailServerPop, result::setPop); return new ServiceResult<>(result, status); diff --git a/commons/src/main/java/com/deftdevs/bootstrapi/commons/util/FieldNames.java b/commons/src/main/java/com/deftdevs/bootstrapi/commons/util/FieldNames.java new file mode 100644 index 00000000..a6a20c6d --- /dev/null +++ b/commons/src/main/java/com/deftdevs/bootstrapi/commons/util/FieldNames.java @@ -0,0 +1,210 @@ +package com.deftdevs.bootstrapi.commons.util; + +import com.deftdevs.bootstrapi.commons.model.type.SubEntityOf; + +import javax.xml.bind.annotation.XmlElement; +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.lang.reflect.TypeVariable; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Derives serialized field names from the model classes themselves, so + * hierarchies expressed by the models (a field of a given type, a + * {@link SubEntityOf} declaration) never have to be restated as string + * constants. The status-map keys of the _all endpoint are built from these + * names and therefore always match the JSON structure of the request. + *

+ * A field matches when its (type-variable-resolved) declared type is the + * requested type, or when it is a {@link Map} whose value type (or a + * {@link Collection} whose element type) is the requested type. Inherited + * fields are included. Every lookup fails fast when no field or more than + * one field matches, so any ambiguity introduced by a future model change is + * caught by the unit tests of the code deriving names from it. + */ +public final class FieldNames { + + private static final Map CACHE = new ConcurrentHashMap<>(); + + private FieldNames() { + } + + /** + * Returns the serialized name of the field holding values of + * {@code subEntityClass} within its parent model, which the sub-model + * itself declares via {@link SubEntityOf} + * (e.g. {@code MailServerPopModel} → {@code pop}). + */ + public static String of(final Class subEntityClass) { + final SubEntityOf subEntityOf = subEntityClass.getAnnotation(SubEntityOf.class); + if (subEntityOf == null) { + throw new IllegalArgumentException( + subEntityClass.getName() + " must be annotated with @SubEntityOf"); + } + return of(subEntityOf.value(), subEntityClass); + } + + /** + * Returns the serialized name of the (unique) field of {@code modelClass} + * (including inherited fields) that holds values of {@code fieldType}, + * honoring an explicit {@code @XmlElement(name = ...)} override. + */ + public static String of(final Class modelClass, final Class fieldType) { + return CACHE.computeIfAbsent(modelClass.getName() + "#" + fieldType.getName(), + key -> resolve(modelClass, fieldType)); + } + + /** + * Returns the full slash-separated field path from {@code rootClass} to + * the (unique) field holding values of {@code leafType}, walking the + * model hierarchy (e.g. a product {@code _AllModel} + + * {@code SettingsGeneralModel} → {@code settings/general}, + + * {@code MailServerModel} → {@code mailServer}). + */ + public static String pathOf(final Class rootClass, final Class leafType) { + return CACHE.computeIfAbsent(rootClass.getName() + "->" + leafType.getName(), + key -> resolvePath(rootClass, leafType)); + } + + private static String resolve(final Class modelClass, final Class fieldType) { + Field match = null; + + for (final Field field : declaredAndInheritedFields(modelClass)) { + if (!matches(field, fieldType, typeVariableBindings(modelClass))) { + continue; + } + if (match != null) { + throw new IllegalArgumentException(String.format( + "%s has more than one field holding %s: %s, %s", + modelClass.getName(), fieldType.getName(), match.getName(), field.getName())); + } + match = field; + } + + if (match == null) { + throw new IllegalArgumentException(String.format( + "%s has no field holding %s", modelClass.getName(), fieldType.getName())); + } + return serializedName(match); + } + + private static String resolvePath(final Class rootClass, final Class leafType) { + String match = null; + + for (final Field field : declaredAndInheritedFields(rootClass)) { + final Map, Type> bindings = typeVariableBindings(rootClass); + + String candidate = null; + if (matches(field, leafType, bindings)) { + candidate = serializedName(field); + } else { + final Type resolvedType = resolveType(field.getGenericType(), bindings); + if (resolvedType instanceof Class + && isModel((Class) resolvedType) + && !resolvedType.equals(rootClass)) { + try { + candidate = serializedName(field) + "/" + resolvePath((Class) resolvedType, leafType); + } catch (IllegalArgumentException e) { + // no unique path below this field + } + } + } + + if (candidate == null) { + continue; + } + if (match != null) { + throw new IllegalArgumentException(String.format( + "%s has more than one field path to %s: %s, %s", + rootClass.getName(), leafType.getName(), match, candidate)); + } + match = candidate; + } + + if (match == null) { + throw new IllegalArgumentException(String.format( + "%s has no field path to %s", rootClass.getName(), leafType.getName())); + } + return match; + } + + private static java.util.List declaredAndInheritedFields(final Class modelClass) { + final java.util.List fields = new java.util.ArrayList<>(); + for (Class current = modelClass; current != null && current != Object.class; + current = current.getSuperclass()) { + for (final Field field : current.getDeclaredFields()) { + if (!field.isSynthetic() && !Modifier.isStatic(field.getModifiers())) { + fields.add(field); + } + } + } + return fields; + } + + /** Maps the type variables of superclasses to the concrete types bound by {@code modelClass}. */ + private static Map, Type> typeVariableBindings(final Class modelClass) { + final Map, Type> bindings = new HashMap<>(); + for (Class current = modelClass; current != null && current != Object.class; + current = current.getSuperclass()) { + final Type genericSuperclass = current.getGenericSuperclass(); + if (genericSuperclass instanceof ParameterizedType) { + final ParameterizedType parameterized = (ParameterizedType) genericSuperclass; + final TypeVariable[] variables = current.getSuperclass().getTypeParameters(); + final Type[] arguments = parameterized.getActualTypeArguments(); + for (int i = 0; i < variables.length; i++) { + bindings.put(variables[i], resolveType(arguments[i], bindings)); + } + } + } + return bindings; + } + + private static Type resolveType(final Type type, final Map, Type> bindings) { + if (type instanceof TypeVariable) { + return bindings.getOrDefault(type, type); + } + return type; + } + + private static boolean matches(final Field field, final Class fieldType, + final Map, Type> bindings) { + + final Type resolvedType = resolveType(field.getGenericType(), bindings); + if (resolvedType.equals(fieldType)) { + return true; + } + + if (!(resolvedType instanceof ParameterizedType)) { + return false; + } + + final ParameterizedType parameterized = (ParameterizedType) resolvedType; + final Type[] typeArguments = parameterized.getActualTypeArguments(); + if (Map.class.isAssignableFrom(field.getType()) && typeArguments.length == 2) { + return resolveType(typeArguments[1], bindings).equals(fieldType); + } + if (Collection.class.isAssignableFrom(field.getType()) && typeArguments.length == 1) { + return resolveType(typeArguments[0], bindings).equals(fieldType); + } + return false; + } + + private static String serializedName(final Field field) { + final XmlElement xmlElement = field.getAnnotation(XmlElement.class); + if (xmlElement != null && !"##default".equals(xmlElement.name())) { + return xmlElement.name(); + } + return field.getName(); + } + + private static boolean isModel(final Class type) { + return type.getPackage() != null + && type.getPackage().getName().startsWith("com.deftdevs.bootstrapi"); + } + +} diff --git a/commons/src/main/java/com/deftdevs/bootstrapi/commons/util/ServiceResultUtil.java b/commons/src/main/java/com/deftdevs/bootstrapi/commons/util/ServiceResultUtil.java index ea4de72d..08e477a3 100644 --- a/commons/src/main/java/com/deftdevs/bootstrapi/commons/util/ServiceResultUtil.java +++ b/commons/src/main/java/com/deftdevs/bootstrapi/commons/util/ServiceResultUtil.java @@ -21,15 +21,17 @@ public class ServiceResultUtil { * throws. Does nothing when {@code input} is null, i.e. when the field was * not part of the request. *

- * The key is the sub-field's name within the composite's model, so that - * status keys line up with the JSON structure of the request. Composites - * record their sub-fields under local keys ({@code general}, {@code smtp}); - * the _all endpoint prefixes them with the composite's own field name when - * merging ({@code settings/general}, {@code mailServer/smtp}). + * The status key is derived from the models via + * {@link FieldNames#of(Class)} from the sub-model's {@link com.deftdevs.bootstrapi.commons.model.type.SubEntityOf} declaration: the sub-field's name within the + * composite's model, so that status keys line up with the JSON structure + * of the request. Composites record their sub-fields under local keys + * ({@code general}, {@code smtp}); the _all endpoint prefixes them with + * the composite's own field name when merging ({@code settings/general}, + * {@code mailServer/smtp}). */ public static void setSubEntity( final Map statusMap, - final String key, + final Class subEntityClass, final T input, final UnaryOperator updateFunction, final Consumer resultConsumer) { @@ -38,6 +40,7 @@ public static void setSubEntity( return; } + final String key = FieldNames.of(subEntityClass); try { resultConsumer.accept(updateFunction.apply(input)); statusMap.put(key, _AllModelStatus.success()); diff --git a/commons/src/test/java/com/deftdevs/bootstrapi/commons/service/_AbstractAllServiceImplTest.java b/commons/src/test/java/com/deftdevs/bootstrapi/commons/service/_AbstractAllServiceImplTest.java index ace4db68..24345f8d 100644 --- a/commons/src/test/java/com/deftdevs/bootstrapi/commons/service/_AbstractAllServiceImplTest.java +++ b/commons/src/test/java/com/deftdevs/bootstrapi/commons/service/_AbstractAllServiceImplTest.java @@ -20,9 +20,22 @@ class _AbstractAllServiceImplTest { + private static class TestGroupModel { + } + + private static class TestEntityModel { + } + + private static class TestEntryModel { + } + + @SuppressWarnings("unused") private static class TestAllModel implements _AllModelAccessor { - private final Map status = new HashMap<>(); + private TestGroupModel group; + private TestEntityModel entity; + private Map entries; + private Map status; @Override public Map getStatus() { @@ -46,7 +59,7 @@ public TestAllModel setAll(final TestAllModel allModel) { void setEntitySkipsNullInput() { final Map statusMap = new HashMap<>(); - final _AllModelStatus status = service.setEntity("permissionsGlobal", null, + final _AllModelStatus status = service.setEntity(TestEntityModel.class, null, input -> input, result -> {}, statusMap); assertNull(status); @@ -54,38 +67,39 @@ void setEntitySkipsNullInput() { } @Test - void setEntitySuccessRecordsStatusAndResult() { + void setEntitySuccessRecordsStatusAndResultUnderTheFieldName() { final Map statusMap = new HashMap<>(); - final AtomicReference result = new AtomicReference<>(); + final AtomicReference result = new AtomicReference<>(); + final TestEntityModel updated = new TestEntityModel(); - final _AllModelStatus status = service.setEntity("permissionsGlobal", "input", - input -> input + "-updated", result::set, statusMap); + final _AllModelStatus status = service.setEntity(TestEntityModel.class, new TestEntityModel(), + input -> updated, result::set, statusMap); assertEquals(Response.Status.OK.getStatusCode(), status.getStatus()); - assertEquals("input-updated", result.get()); - assertSame(status, statusMap.get("permissionsGlobal")); + assertEquals(updated, result.get()); + assertSame(status, statusMap.get("entity")); } @Test void setEntityUsesWebApplicationExceptionStatusCode() { final Map statusMap = new HashMap<>(); - final _AllModelStatus status = service.setEntity("permissionsGlobal", "input", + final _AllModelStatus status = service.setEntity(TestEntityModel.class, new TestEntityModel(), input -> { throw new WebApplicationException("conflict", Response.Status.CONFLICT); }, result -> {}, statusMap); assertEquals(Response.Status.CONFLICT.getStatusCode(), status.getStatus()); - assertEquals("Failed to apply permissionsGlobal configuration", status.getMessage()); - assertSame(status, statusMap.get("permissionsGlobal")); + assertEquals("Failed to apply entity configuration", status.getMessage()); + assertSame(status, statusMap.get("entity")); } @Test void setEntityMapsUnexpectedExceptionTo500WithoutDetails() { final Map statusMap = new HashMap<>(); - final _AllModelStatus status = service.setEntity("permissionsGlobal", "input", + final _AllModelStatus status = service.setEntity(TestEntityModel.class, new TestEntityModel(), input -> { throw new IllegalStateException("internal details that must not leak"); }, @@ -101,40 +115,40 @@ void setEntityMapsUnexpectedExceptionTo500WithoutDetails() { void setEntitiesSkipsNullAndEmptyMap() { final Map statusMap = new HashMap<>(); - assertNull(service.setEntities("directories", null, + assertNull(service.setEntities(TestEntryModel.class, null, entities -> entities, result -> {}, statusMap)); - assertNull(service.setEntities("directories", Collections.emptyMap(), + assertNull(service.setEntities(TestEntryModel.class, Collections.emptyMap(), entities -> entities, result -> {}, statusMap)); assertTrue(statusMap.isEmpty()); } @Test - void setEntitiesSuccessRecordsStatusAndResult() { + void setEntitiesSuccessRecordsStatusAndResultUnderTheFieldName() { final Map statusMap = new HashMap<>(); - final AtomicReference> result = new AtomicReference<>(); - final Map entities = Collections.singletonMap("key", "value"); + final AtomicReference> result = new AtomicReference<>(); + final Map entities = Collections.singletonMap("key", new TestEntryModel()); - final _AllModelStatus status = service.setEntities("directories", entities, + final _AllModelStatus status = service.setEntities(TestEntryModel.class, entities, input -> input, result::set, statusMap); assertEquals(Response.Status.OK.getStatusCode(), status.getStatus()); assertEquals(entities, result.get()); - assertSame(status, statusMap.get("directories")); + assertSame(status, statusMap.get("entries")); } @Test void setEntitiesRecordsErrorStatusOnException() { final Map statusMap = new HashMap<>(); - final Map entities = Collections.singletonMap("key", "value"); + final Map entities = Collections.singletonMap("key", new TestEntryModel()); - final _AllModelStatus status = service.setEntities("directories", entities, + final _AllModelStatus status = service.setEntities(TestEntryModel.class, entities, input -> { throw new WebApplicationException(Response.Status.BAD_REQUEST); }, result -> {}, statusMap); assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), status.getStatus()); - assertSame(status, statusMap.get("directories")); + assertSame(status, statusMap.get("entries")); } // setEntityWithStatus @@ -143,7 +157,7 @@ void setEntitiesRecordsErrorStatusOnException() { void setEntityWithStatusSkipsNullInput() { final Map statusMap = new HashMap<>(); - service.setEntityWithStatus("settings", null, + service.setEntityWithStatus(TestGroupModel.class, null, input -> new ServiceResult<>(input, Collections.emptyMap()), result -> {}, statusMap); @@ -151,47 +165,48 @@ void setEntityWithStatusSkipsNullInput() { } @Test - void setEntityWithStatusPrefixesSubFieldStatusesWithField() { + void setEntityWithStatusPrefixesSubFieldStatusesWithTheFieldName() { final Map statusMap = new HashMap<>(); - final AtomicReference result = new AtomicReference<>(); + final AtomicReference result = new AtomicReference<>(); + final TestGroupModel updated = new TestGroupModel(); final Map subStatus = new LinkedHashMap<>(); - subStatus.put("general", _AllModelStatus.success()); - subStatus.put("security", _AllModelStatus.error(Response.Status.BAD_REQUEST, "bad", null)); + subStatus.put("first", _AllModelStatus.success()); + subStatus.put("second", _AllModelStatus.error(Response.Status.BAD_REQUEST, "bad", null)); - service.setEntityWithStatus("settings", "input", - input -> new ServiceResult<>(input + "-updated", subStatus), + service.setEntityWithStatus(TestGroupModel.class, new TestGroupModel(), + input -> new ServiceResult<>(updated, subStatus), result::set, statusMap); - assertEquals("input-updated", result.get()); + assertEquals(updated, result.get()); assertEquals(2, statusMap.size()); - assertEquals(Response.Status.OK.getStatusCode(), statusMap.get("settings/general").getStatus()); - assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), statusMap.get("settings/security").getStatus()); + assertEquals(Response.Status.OK.getStatusCode(), statusMap.get("group/first").getStatus()); + assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), statusMap.get("group/second").getStatus()); } @Test - void setEntityWithStatusFallsBackToFieldKeyOnThrow() { + void setEntityWithStatusFallsBackToTheFieldNameOnThrow() { final Map statusMap = new HashMap<>(); - service.setEntityWithStatus("settings", "input", + service.setEntityWithStatus(TestGroupModel.class, new TestGroupModel(), input -> { throw new IllegalStateException("boom"); }, result -> {}, statusMap); - assertEquals(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode(), statusMap.get("settings").getStatus()); + assertEquals(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode(), statusMap.get("group").getStatus()); } @Test void setEntityWithStatusUsesWebApplicationExceptionStatusCodeOnThrow() { final Map statusMap = new HashMap<>(); - service.setEntityWithStatus("settings", "input", + service.setEntityWithStatus(TestGroupModel.class, new TestGroupModel(), input -> { throw new WebApplicationException(Response.Status.CONFLICT); }, result -> {}, statusMap); - assertEquals(Response.Status.CONFLICT.getStatusCode(), statusMap.get("settings").getStatus()); + assertEquals(Response.Status.CONFLICT.getStatusCode(), statusMap.get("group").getStatus()); } } diff --git a/commons/src/test/java/com/deftdevs/bootstrapi/commons/service/api/AuthenticationServiceTest.java b/commons/src/test/java/com/deftdevs/bootstrapi/commons/service/api/AuthenticationServiceTest.java index 774f0449..0ffcd32b 100644 --- a/commons/src/test/java/com/deftdevs/bootstrapi/commons/service/api/AuthenticationServiceTest.java +++ b/commons/src/test/java/com/deftdevs/bootstrapi/commons/service/api/AuthenticationServiceTest.java @@ -5,6 +5,7 @@ import com.deftdevs.bootstrapi.commons.model.AuthenticationModel; import com.deftdevs.bootstrapi.commons.model.AuthenticationSsoModel; import com.deftdevs.bootstrapi.commons.model.type.ServiceResult; +import com.deftdevs.bootstrapi.commons.util.FieldNames; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -59,8 +60,8 @@ void testSetAuthenticationAppliesIdpsAndSso() { assertEquals(idpModels, result.getModel().getIdps()); assertEquals(ssoModel, result.getModel().getSso()); - assertEquals(Response.Status.OK.getStatusCode(), result.getStatus().get("idps").getStatus()); - assertEquals(Response.Status.OK.getStatusCode(), result.getStatus().get("sso").getStatus()); + assertEquals(Response.Status.OK.getStatusCode(), result.getStatus().get(FieldNames.of(AuthenticationModel.class, AbstractAuthenticationIdpModel.class)).getStatus()); + assertEquals(Response.Status.OK.getStatusCode(), result.getStatus().get(FieldNames.of(AuthenticationModel.class, AuthenticationSsoModel.class)).getStatus()); } @Test @@ -82,7 +83,7 @@ void testSetAuthenticationRecordsPerSubFieldFailure() { assertEquals(idpModels, result.getModel().getIdps()); assertNull(result.getModel().getSso()); - assertEquals(Response.Status.OK.getStatusCode(), result.getStatus().get("idps").getStatus()); - assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), result.getStatus().get("sso").getStatus()); + assertEquals(Response.Status.OK.getStatusCode(), result.getStatus().get(FieldNames.of(AuthenticationModel.class, AbstractAuthenticationIdpModel.class)).getStatus()); + assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), result.getStatus().get(FieldNames.of(AuthenticationModel.class, AuthenticationSsoModel.class)).getStatus()); } } diff --git a/commons/src/test/java/com/deftdevs/bootstrapi/commons/service/api/MailServerServiceTest.java b/commons/src/test/java/com/deftdevs/bootstrapi/commons/service/api/MailServerServiceTest.java index 82ac147f..6df75245 100644 --- a/commons/src/test/java/com/deftdevs/bootstrapi/commons/service/api/MailServerServiceTest.java +++ b/commons/src/test/java/com/deftdevs/bootstrapi/commons/service/api/MailServerServiceTest.java @@ -5,6 +5,7 @@ import com.deftdevs.bootstrapi.commons.model.MailServerPopModel; import com.deftdevs.bootstrapi.commons.model.MailServerSmtpModel; import com.deftdevs.bootstrapi.commons.model.type.ServiceResult; +import com.deftdevs.bootstrapi.commons.util.FieldNames; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -50,8 +51,8 @@ void testSetMailServerAppliesSmtpAndPop() { assertEquals(MailServerSmtpModel.EXAMPLE_1, result.getModel().getSmtp()); assertEquals(MailServerPopModel.EXAMPLE_1, result.getModel().getPop()); - assertEquals(Response.Status.OK.getStatusCode(), result.getStatus().get("smtp").getStatus()); - assertEquals(Response.Status.OK.getStatusCode(), result.getStatus().get("pop").getStatus()); + assertEquals(Response.Status.OK.getStatusCode(), result.getStatus().get(FieldNames.of(MailServerModel.class, MailServerSmtpModel.class)).getStatus()); + assertEquals(Response.Status.OK.getStatusCode(), result.getStatus().get(FieldNames.of(MailServerModel.class, MailServerPopModel.class)).getStatus()); } @Test @@ -74,7 +75,7 @@ void testSetMailServerRecordsPerSubFieldFailure() { assertNull(result.getModel().getSmtp()); assertEquals(MailServerPopModel.EXAMPLE_1, result.getModel().getPop()); - assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), result.getStatus().get("smtp").getStatus()); - assertEquals(Response.Status.OK.getStatusCode(), result.getStatus().get("pop").getStatus()); + assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), result.getStatus().get(FieldNames.of(MailServerModel.class, MailServerSmtpModel.class)).getStatus()); + assertEquals(Response.Status.OK.getStatusCode(), result.getStatus().get(FieldNames.of(MailServerModel.class, MailServerPopModel.class)).getStatus()); } } diff --git a/commons/src/test/java/com/deftdevs/bootstrapi/commons/util/FieldNamesTest.java b/commons/src/test/java/com/deftdevs/bootstrapi/commons/util/FieldNamesTest.java new file mode 100644 index 00000000..47b3953e --- /dev/null +++ b/commons/src/test/java/com/deftdevs/bootstrapi/commons/util/FieldNamesTest.java @@ -0,0 +1,90 @@ +package com.deftdevs.bootstrapi.commons.util; + +import org.junit.jupiter.api.Test; + +import javax.xml.bind.annotation.XmlElement; +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class FieldNamesTest { + + private static class LeafModel { + } + + private static class OtherLeafModel { + } + + private static class GroupModel { + private LeafModel leaf; + private Map others; + } + + private static class ThirdLeafModel { + } + + private static class RootModel { + private GroupModel group; + private Map directLeaves; + private List names; + + @XmlElement(name = "renamed") + private ThirdLeafModel other; + } + + private static class AmbiguousModel { + private LeafModel first; + private LeafModel second; + } + + @Test + void ofResolvesDirectField() { + assertEquals("leaf", FieldNames.of(GroupModel.class, LeafModel.class)); + } + + @Test + void ofResolvesMapValueField() { + assertEquals("others", FieldNames.of(GroupModel.class, OtherLeafModel.class)); + } + + @Test + void ofResolvesCollectionElementField() { + assertEquals("names", FieldNames.of(RootModel.class, String.class)); + } + + @Test + void ofHonorsXmlElementName() { + assertEquals("renamed", FieldNames.of(RootModel.class, ThirdLeafModel.class)); + } + + @Test + void ofRejectsAmbiguousFields() { + assertThrows(IllegalArgumentException.class, + () -> FieldNames.of(AmbiguousModel.class, LeafModel.class)); + } + + @Test + void ofRejectsUnknownFieldType() { + assertThrows(IllegalArgumentException.class, + () -> FieldNames.of(GroupModel.class, RootModel.class)); + } + + @Test + void pathOfResolvesTopLevelField() { + assertEquals("group", FieldNames.pathOf(RootModel.class, GroupModel.class)); + } + + @Test + void pathOfWalksIntoNestedModels() { + assertEquals("group/others", FieldNames.pathOf(RootModel.class, OtherLeafModel.class)); + } + + @Test + void pathOfRejectsAmbiguousPaths() { + // LeafModel is reachable both as root.directLeaves and root.group.leaf + assertThrows(IllegalArgumentException.class, + () -> FieldNames.pathOf(RootModel.class, LeafModel.class)); + } +} diff --git a/commons/src/test/java/com/deftdevs/bootstrapi/commons/util/ServiceResultUtilTest.java b/commons/src/test/java/com/deftdevs/bootstrapi/commons/util/ServiceResultUtilTest.java index 4a013462..0ad3589c 100644 --- a/commons/src/test/java/com/deftdevs/bootstrapi/commons/util/ServiceResultUtilTest.java +++ b/commons/src/test/java/com/deftdevs/bootstrapi/commons/util/ServiceResultUtilTest.java @@ -1,5 +1,6 @@ package com.deftdevs.bootstrapi.commons.util; +import com.deftdevs.bootstrapi.commons.model.type.SubEntityOf; import com.deftdevs.bootstrapi.commons.model.type._AllModelStatus; import org.junit.jupiter.api.Test; @@ -15,34 +16,44 @@ class ServiceResultUtilTest { + @SuppressWarnings("unused") + private static class GroupModel { + private GeneralModel general; + } + + @SubEntityOf(GroupModel.class) + private static class GeneralModel { + } + @Test void setSubEntitySkipsNullInput() { final Map statusMap = new LinkedHashMap<>(); - ServiceResultUtil.setSubEntity(statusMap, "general", null, + ServiceResultUtil.setSubEntity(statusMap, GeneralModel.class, null, input -> input, result -> {}); assertTrue(statusMap.isEmpty()); } @Test - void setSubEntityRecordsSuccessAndResult() { + void setSubEntityRecordsSuccessAndResultUnderTheFieldName() { final Map statusMap = new LinkedHashMap<>(); - final AtomicReference result = new AtomicReference<>(); + final AtomicReference result = new AtomicReference<>(); + final GeneralModel updated = new GeneralModel(); - ServiceResultUtil.setSubEntity(statusMap, "general", "input", - input -> input + "-updated", result::set); + ServiceResultUtil.setSubEntity(statusMap, GeneralModel.class, new GeneralModel(), + input -> updated, result::set); - assertEquals("input-updated", result.get()); + assertEquals(updated, result.get()); assertEquals(Response.Status.OK.getStatusCode(), statusMap.get("general").getStatus()); } @Test void setSubEntityRecordsErrorStatusOnException() { final Map statusMap = new LinkedHashMap<>(); - final AtomicReference result = new AtomicReference<>(); + final AtomicReference result = new AtomicReference<>(); - ServiceResultUtil.setSubEntity(statusMap, "general", "input", + ServiceResultUtil.setSubEntity(statusMap, GeneralModel.class, new GeneralModel(), input -> { throw new WebApplicationException("bad", Response.Status.BAD_REQUEST); }, diff --git a/commons/src/test/java/it/com/deftdevs/bootstrapi/commons/rest/Abstract_AllResourceFuncTest.java b/commons/src/test/java/it/com/deftdevs/bootstrapi/commons/rest/Abstract_AllResourceFuncTest.java index 75abfbfe..8c4abd75 100644 --- a/commons/src/test/java/it/com/deftdevs/bootstrapi/commons/rest/Abstract_AllResourceFuncTest.java +++ b/commons/src/test/java/it/com/deftdevs/bootstrapi/commons/rest/Abstract_AllResourceFuncTest.java @@ -30,24 +30,28 @@ public abstract class Abstract_AllResourceFuncTest { /** The _all endpoint is the REST root itself. */ private static final String ALL_PATH = ""; - protected static final String SETTINGS_GENERAL_KEY = "settings/general"; - private final ObjectMapper objectMapper = new ObjectMapper(); @Test void testSetAllAppliesGeneralSettings() throws Exception { + final String settingsGeneralKey = getSettingsGeneralStatusKey(); + final HttpResponse allResponse = HttpRequestHelper.builder(ALL_PATH) .request(HttpMethod.PUT, getExampleAllModel()); assertEquals(Response.Status.OK.getStatusCode(), allResponse.statusCode()); final JsonNode allNode = objectMapper.readTree(allResponse.body()); - final JsonNode generalStatusNode = allNode.path("status").path(SETTINGS_GENERAL_KEY); + final JsonNode generalStatusNode = allNode.path("status").path(settingsGeneralKey); assertFalse(generalStatusNode.isMissingNode(), - "expected a '" + SETTINGS_GENERAL_KEY + "' entry in the status map, got: " + allNode.path("status")); + "expected a '" + settingsGeneralKey + "' entry in the status map, got: " + allNode.path("status")); assertEquals(Response.Status.OK.getStatusCode(), generalStatusNode.path("status").asInt()); - final JsonNode generalNode = allNode.path("settings").path("general"); + // the status key mirrors the request's field path, so it also locates the model in the body + JsonNode generalNode = allNode; + for (final String segment : settingsGeneralKey.split("/")) { + generalNode = generalNode.path(segment); + } assertEquals(getExampleSettingsGeneralModel().getTitle(), generalNode.path("title").asText()); } @@ -113,6 +117,12 @@ void testSetAllUnauthorized() throws Exception { */ protected abstract Object getExampleAllModel(); + /** + * The status-map key of the general settings, derived from the product's + * models (e.g. {@code FieldNames.pathOf(_AllModel.class, SettingsGeneralModel.class)}). + */ + protected abstract String getSettingsGeneralStatusKey(); + protected SettingsGeneralModel getExampleSettingsGeneralModel() { return SettingsGeneralModel.EXAMPLE_1; } diff --git a/confluence/src/main/java/com/deftdevs/bootstrapi/confluence/model/SettingsCustomHtmlModel.java b/confluence/src/main/java/com/deftdevs/bootstrapi/confluence/model/SettingsCustomHtmlModel.java index 8c9f2f99..cced9a2e 100644 --- a/confluence/src/main/java/com/deftdevs/bootstrapi/confluence/model/SettingsCustomHtmlModel.java +++ b/confluence/src/main/java/com/deftdevs/bootstrapi/confluence/model/SettingsCustomHtmlModel.java @@ -8,6 +8,7 @@ import javax.xml.bind.annotation.XmlAccessType; import javax.xml.bind.annotation.XmlAccessorType; import javax.xml.bind.annotation.XmlElement; +import com.deftdevs.bootstrapi.commons.model.type.SubEntityOf; import javax.xml.bind.annotation.XmlRootElement; import static com.deftdevs.bootstrapi.commons.constants.BootstrAPI.*; @@ -16,6 +17,7 @@ @Builder @NoArgsConstructor @AllArgsConstructor +@SubEntityOf(SettingsModel.class) @XmlRootElement(name = SETTINGS + "-" + SETTINGS_CUSTOM_HTML) @XmlAccessorType(XmlAccessType.FIELD) public class SettingsCustomHtmlModel { diff --git a/confluence/src/main/java/com/deftdevs/bootstrapi/confluence/service/_AllServiceImpl.java b/confluence/src/main/java/com/deftdevs/bootstrapi/confluence/service/_AllServiceImpl.java index e2c56658..8b677030 100644 --- a/confluence/src/main/java/com/deftdevs/bootstrapi/confluence/service/_AllServiceImpl.java +++ b/confluence/src/main/java/com/deftdevs/bootstrapi/confluence/service/_AllServiceImpl.java @@ -1,5 +1,11 @@ package com.deftdevs.bootstrapi.confluence.service; +import com.deftdevs.bootstrapi.commons.model.AbstractDirectoryModel; +import com.deftdevs.bootstrapi.commons.model.ApplicationLinkModel; +import com.deftdevs.bootstrapi.commons.model.AuthenticationModel; +import com.deftdevs.bootstrapi.commons.model.LicenseModel; +import com.deftdevs.bootstrapi.commons.model.MailServerModel; +import com.deftdevs.bootstrapi.commons.model.PermissionsGlobalModel; import com.deftdevs.bootstrapi.commons.model.type._AllModelStatus; import com.deftdevs.bootstrapi.commons.service._AbstractAllServiceImpl; import com.deftdevs.bootstrapi.commons.service.api.ApplicationLinksService; @@ -7,6 +13,7 @@ import com.deftdevs.bootstrapi.commons.service.api.LicensesService; import com.deftdevs.bootstrapi.commons.service.api.MailServerService; import com.deftdevs.bootstrapi.commons.service.api.PermissionsService; +import com.deftdevs.bootstrapi.confluence.model.SettingsModel; import com.deftdevs.bootstrapi.confluence.model._AllModel; import com.deftdevs.bootstrapi.confluence.service.api.ConfluenceAuthenticationService; import com.deftdevs.bootstrapi.confluence.service.api.ConfluenceSettingsService; @@ -50,25 +57,25 @@ public _AllModel setAll( final _AllModel result = new _AllModel(); final Map statusMap = new HashMap<>(); - setEntityWithStatus("settings", allModel.getSettings(), + setEntityWithStatus(SettingsModel.class, allModel.getSettings(), settingsService::setSettings, result::setSettings, statusMap); - setEntities("directories", allModel.getDirectories(), + setEntities(AbstractDirectoryModel.class, allModel.getDirectories(), directoriesService::setDirectories, result::setDirectories, statusMap); - setEntities("applicationLinks", allModel.getApplicationLinks(), + setEntities(ApplicationLinkModel.class, allModel.getApplicationLinks(), applicationLinksService::setApplicationLinks, result::setApplicationLinks, statusMap); - setEntityWithStatus("authentication", allModel.getAuthentication(), + setEntityWithStatus(AuthenticationModel.class, allModel.getAuthentication(), authenticationService::setAuthentication, result::setAuthentication, statusMap); - setEntities("licenses", allModel.getLicenses(), + setEntities(LicenseModel.class, allModel.getLicenses(), licensesService::setLicenses, result::setLicenses, statusMap); - setEntityWithStatus("mailServer", allModel.getMailServer(), + setEntityWithStatus(MailServerModel.class, allModel.getMailServer(), mailServerService::setMailServer, result::setMailServer, statusMap); - setEntity("permissionsGlobal", allModel.getPermissionsGlobal(), + setEntity(PermissionsGlobalModel.class, allModel.getPermissionsGlobal(), permissionsService::setPermissionsGlobal, result::setPermissionsGlobal, statusMap); result.setStatus(statusMap); diff --git a/confluence/src/main/java/com/deftdevs/bootstrapi/confluence/service/api/ConfluenceSettingsService.java b/confluence/src/main/java/com/deftdevs/bootstrapi/confluence/service/api/ConfluenceSettingsService.java index 8a64e20a..dccb109c 100644 --- a/confluence/src/main/java/com/deftdevs/bootstrapi/confluence/service/api/ConfluenceSettingsService.java +++ b/confluence/src/main/java/com/deftdevs/bootstrapi/confluence/service/api/ConfluenceSettingsService.java @@ -1,5 +1,6 @@ package com.deftdevs.bootstrapi.confluence.service.api; +import com.deftdevs.bootstrapi.commons.model.SettingsBrandingColorSchemeModel; import com.deftdevs.bootstrapi.commons.model.SettingsGeneralModel; import com.deftdevs.bootstrapi.commons.model.SettingsSecurityModel; import com.deftdevs.bootstrapi.commons.model.type.ServiceResult; @@ -37,13 +38,13 @@ default ServiceResult setSettings(final SettingsModel settingsMod final SettingsModel result = new SettingsModel(); final Map status = new LinkedHashMap<>(); - ServiceResultUtil.setSubEntity(status, "general", settingsModel.getGeneral(), + ServiceResultUtil.setSubEntity(status, SettingsGeneralModel.class, settingsModel.getGeneral(), this::setSettingsGeneral, result::setGeneral); - ServiceResultUtil.setSubEntity(status, "security", settingsModel.getSecurity(), + ServiceResultUtil.setSubEntity(status, SettingsSecurityModel.class, settingsModel.getSecurity(), this::setSettingsSecurity, result::setSecurity); - ServiceResultUtil.setSubEntity(status, "branding", settingsModel.getBranding(), + ServiceResultUtil.setSubEntity(status, SettingsBrandingColorSchemeModel.class, settingsModel.getBranding(), this::setColourScheme, result::setBranding); - ServiceResultUtil.setSubEntity(status, "customHtml", settingsModel.getCustomHtml(), + ServiceResultUtil.setSubEntity(status, SettingsCustomHtmlModel.class, settingsModel.getCustomHtml(), this::setCustomHtml, result::setCustomHtml); return new ServiceResult<>(result, status); diff --git a/confluence/src/test/java/com/deftdevs/bootstrapi/confluence/rest/_AllResourceTest.java b/confluence/src/test/java/com/deftdevs/bootstrapi/confluence/rest/_AllResourceTest.java index 95d792de..10077d8b 100644 --- a/confluence/src/test/java/com/deftdevs/bootstrapi/confluence/rest/_AllResourceTest.java +++ b/confluence/src/test/java/com/deftdevs/bootstrapi/confluence/rest/_AllResourceTest.java @@ -1,7 +1,10 @@ package com.deftdevs.bootstrapi.confluence.rest; import com.deftdevs.bootstrapi.commons.model.SettingsGeneralModel; +import com.deftdevs.bootstrapi.commons.model.AbstractDirectoryModel; +import com.deftdevs.bootstrapi.commons.model.ApplicationLinkModel; import com.deftdevs.bootstrapi.commons.model.type._AllModelStatus; +import com.deftdevs.bootstrapi.commons.util.FieldNames; import com.deftdevs.bootstrapi.commons.service.api._AllService; import com.deftdevs.bootstrapi.confluence.model.SettingsModel; import com.deftdevs.bootstrapi.confluence.model._AllModel; @@ -39,7 +42,7 @@ public void setup() { allModel.setSettings(settings); final Map status = new HashMap<>(); - status.put("settings/general", _AllModelStatus.success()); + status.put(FieldNames.pathOf(_AllModel.class, SettingsGeneralModel.class), _AllModelStatus.success()); status.put(DIRECTORIES, _AllModelStatus.success()); status.put(APPLICATION_LINKS, _AllModelStatus.success()); allModel.setStatus(status); @@ -62,7 +65,7 @@ public void testSetAll() { public void testSetAllReturns207OnMixedSuccessAndServerError() { final _AllModel result = new _AllModel(); final Map status = new HashMap<>(); - status.put("settings/general", _AllModelStatus.success()); + status.put(FieldNames.pathOf(_AllModel.class, SettingsGeneralModel.class), _AllModelStatus.success()); status.put(DIRECTORIES, _AllModelStatus.error(Response.Status.INTERNAL_SERVER_ERROR, "boom", null)); result.setStatus(status); @@ -76,7 +79,7 @@ public void testSetAllReturns207OnMixedSuccessAndServerError() { public void testSetAllReturns207OnMixedSuccessAndClientError() { final _AllModel result = new _AllModel(); final Map status = new HashMap<>(); - status.put("settings/general", _AllModelStatus.success()); + status.put(FieldNames.pathOf(_AllModel.class, SettingsGeneralModel.class), _AllModelStatus.success()); status.put(DIRECTORIES, _AllModelStatus.error(Response.Status.BAD_REQUEST, "bad", null)); result.setStatus(status); diff --git a/confluence/src/test/java/com/deftdevs/bootstrapi/confluence/service/ConfluenceSettingsServiceTest.java b/confluence/src/test/java/com/deftdevs/bootstrapi/confluence/service/ConfluenceSettingsServiceTest.java index c7cfd1bb..abd24f3d 100644 --- a/confluence/src/test/java/com/deftdevs/bootstrapi/confluence/service/ConfluenceSettingsServiceTest.java +++ b/confluence/src/test/java/com/deftdevs/bootstrapi/confluence/service/ConfluenceSettingsServiceTest.java @@ -6,6 +6,7 @@ import com.deftdevs.bootstrapi.commons.model.SettingsGeneralModel; import com.deftdevs.bootstrapi.commons.model.SettingsSecurityModel; import com.deftdevs.bootstrapi.commons.model.type.ServiceResult; +import com.deftdevs.bootstrapi.commons.util.FieldNames; import com.deftdevs.bootstrapi.commons.service.api.SettingsBrandingService; import com.deftdevs.bootstrapi.confluence.model.SettingsCustomHtmlModel; import com.deftdevs.bootstrapi.confluence.model.SettingsModel; @@ -76,10 +77,10 @@ void testSetSettingsAppliesAllSubFields() { assertEquals(SettingsBrandingColorSchemeModel.EXAMPLE_1, result.getModel().getBranding()); assertEquals(SettingsCustomHtmlModel.EXAMPLE_1, result.getModel().getCustomHtml()); - assertEquals(Response.Status.OK.getStatusCode(), result.getStatus().get("general").getStatus()); - assertEquals(Response.Status.OK.getStatusCode(), result.getStatus().get("security").getStatus()); - assertEquals(Response.Status.OK.getStatusCode(), result.getStatus().get("branding").getStatus()); - assertEquals(Response.Status.OK.getStatusCode(), result.getStatus().get("customHtml").getStatus()); + assertEquals(Response.Status.OK.getStatusCode(), result.getStatus().get(FieldNames.of(SettingsModel.class, SettingsGeneralModel.class)).getStatus()); + assertEquals(Response.Status.OK.getStatusCode(), result.getStatus().get(FieldNames.of(SettingsModel.class, SettingsSecurityModel.class)).getStatus()); + assertEquals(Response.Status.OK.getStatusCode(), result.getStatus().get(FieldNames.of(SettingsModel.class, SettingsBrandingColorSchemeModel.class)).getStatus()); + assertEquals(Response.Status.OK.getStatusCode(), result.getStatus().get(FieldNames.of(SettingsModel.class, SettingsCustomHtmlModel.class)).getStatus()); } @Test @@ -103,7 +104,7 @@ void testSetSettingsRecordsPerSubFieldFailure() { assertEquals(SettingsGeneralModel.EXAMPLE_1, result.getModel().getGeneral()); assertNull(result.getModel().getBranding()); - assertEquals(Response.Status.OK.getStatusCode(), result.getStatus().get("general").getStatus()); - assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), result.getStatus().get("branding").getStatus()); + assertEquals(Response.Status.OK.getStatusCode(), result.getStatus().get(FieldNames.of(SettingsModel.class, SettingsGeneralModel.class)).getStatus()); + assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), result.getStatus().get(FieldNames.of(SettingsModel.class, SettingsBrandingColorSchemeModel.class)).getStatus()); } } diff --git a/confluence/src/test/java/com/deftdevs/bootstrapi/confluence/service/_AllServiceImplTest.java b/confluence/src/test/java/com/deftdevs/bootstrapi/confluence/service/_AllServiceImplTest.java index 6fc93306..0b71b84e 100644 --- a/confluence/src/test/java/com/deftdevs/bootstrapi/confluence/service/_AllServiceImplTest.java +++ b/confluence/src/test/java/com/deftdevs/bootstrapi/confluence/service/_AllServiceImplTest.java @@ -7,8 +7,10 @@ import com.deftdevs.bootstrapi.commons.model.MailServerModel; import com.deftdevs.bootstrapi.commons.model.MailServerSmtpModel; import com.deftdevs.bootstrapi.commons.model.PermissionsGlobalModel; +import com.deftdevs.bootstrapi.commons.model.AuthenticationSsoModel; import com.deftdevs.bootstrapi.commons.model.SettingsGeneralModel; import com.deftdevs.bootstrapi.commons.model.type.ServiceResult; +import com.deftdevs.bootstrapi.commons.util.FieldNames; import com.deftdevs.bootstrapi.commons.model.type._AllModelStatus; import com.deftdevs.bootstrapi.commons.service.api.ApplicationLinksService; import com.deftdevs.bootstrapi.commons.service.api.DirectoriesService; @@ -102,16 +104,16 @@ void testSetAllAppliesAllFields() { final PermissionsGlobalModel permissionsGlobal = new PermissionsGlobalModel(); doReturn(new ServiceResult<>(settings, - Collections.singletonMap("general", _AllModelStatus.success()))) + Collections.singletonMap(FieldNames.of(SettingsModel.class, SettingsGeneralModel.class), _AllModelStatus.success()))) .when(settingsService).setSettings(settings); doReturn(directories).when(directoriesService).setDirectories(directories); doReturn(applicationLinks).when(applicationLinksService).setApplicationLinks(applicationLinks); doReturn(new ServiceResult<>(authentication, - Collections.singletonMap("sso", _AllModelStatus.success()))) + Collections.singletonMap(FieldNames.of(AuthenticationModel.class, AuthenticationSsoModel.class), _AllModelStatus.success()))) .when(authenticationService).setAuthentication(authentication); doReturn(redactedLicenses).when(licensesService).setLicenses(licenses); doReturn(new ServiceResult<>(mailServer, - Collections.singletonMap("smtp", _AllModelStatus.success()))) + Collections.singletonMap(FieldNames.of(MailServerModel.class, MailServerSmtpModel.class), _AllModelStatus.success()))) .when(mailServerService).setMailServer(mailServer); doReturn(permissionsGlobal).when(permissionsService).setPermissionsGlobal(permissionsGlobal); @@ -136,13 +138,13 @@ void testSetAllAppliesAllFields() { final Map status = result.getStatus(); assertEquals(7, status.size()); - assertEquals(Response.Status.OK.getStatusCode(), status.get("settings/general").getStatus()); - assertEquals(Response.Status.OK.getStatusCode(), status.get("directories").getStatus()); - assertEquals(Response.Status.OK.getStatusCode(), status.get("applicationLinks").getStatus()); - assertEquals(Response.Status.OK.getStatusCode(), status.get("authentication/sso").getStatus()); - assertEquals(Response.Status.OK.getStatusCode(), status.get("licenses").getStatus()); - assertEquals(Response.Status.OK.getStatusCode(), status.get("mailServer/smtp").getStatus()); - assertEquals(Response.Status.OK.getStatusCode(), status.get("permissionsGlobal").getStatus()); + assertEquals(Response.Status.OK.getStatusCode(), status.get(FieldNames.pathOf(_AllModel.class, SettingsGeneralModel.class)).getStatus()); + assertEquals(Response.Status.OK.getStatusCode(), status.get(FieldNames.of(_AllModel.class, AbstractDirectoryModel.class)).getStatus()); + assertEquals(Response.Status.OK.getStatusCode(), status.get(FieldNames.of(_AllModel.class, ApplicationLinkModel.class)).getStatus()); + assertEquals(Response.Status.OK.getStatusCode(), status.get(FieldNames.pathOf(_AllModel.class, AuthenticationSsoModel.class)).getStatus()); + assertEquals(Response.Status.OK.getStatusCode(), status.get(FieldNames.of(_AllModel.class, LicenseModel.class)).getStatus()); + assertEquals(Response.Status.OK.getStatusCode(), status.get(FieldNames.pathOf(_AllModel.class, MailServerSmtpModel.class)).getStatus()); + assertEquals(Response.Status.OK.getStatusCode(), status.get(FieldNames.of(_AllModel.class, PermissionsGlobalModel.class)).getStatus()); } @Test @@ -153,7 +155,7 @@ void testSetAllRecordsFailureAndContinuesWithOtherFields() { Collections.singletonMap("licenseKey", LicenseModel.EXAMPLE_1); doReturn(new ServiceResult<>(settings, - Collections.singletonMap("general", _AllModelStatus.success()))) + Collections.singletonMap(FieldNames.of(SettingsModel.class, SettingsGeneralModel.class), _AllModelStatus.success()))) .when(settingsService).setSettings(settings); doThrow(new WebApplicationException(Response.Status.CONFLICT)) .when(licensesService).setLicenses(licenses); @@ -166,7 +168,7 @@ void testSetAllRecordsFailureAndContinuesWithOtherFields() { assertEquals(settings, result.getSettings()); assertNull(result.getLicenses()); - assertEquals(Response.Status.OK.getStatusCode(), result.getStatus().get("settings/general").getStatus()); - assertEquals(Response.Status.CONFLICT.getStatusCode(), result.getStatus().get("licenses").getStatus()); + assertEquals(Response.Status.OK.getStatusCode(), result.getStatus().get(FieldNames.pathOf(_AllModel.class, SettingsGeneralModel.class)).getStatus()); + assertEquals(Response.Status.CONFLICT.getStatusCode(), result.getStatus().get(FieldNames.of(_AllModel.class, LicenseModel.class)).getStatus()); } } diff --git a/confluence/src/test/java/it/com/deftdevs/bootstrapi/confluence/rest/_AllResourceFuncTest.java b/confluence/src/test/java/it/com/deftdevs/bootstrapi/confluence/rest/_AllResourceFuncTest.java index df2128cf..6ac0ad6c 100644 --- a/confluence/src/test/java/it/com/deftdevs/bootstrapi/confluence/rest/_AllResourceFuncTest.java +++ b/confluence/src/test/java/it/com/deftdevs/bootstrapi/confluence/rest/_AllResourceFuncTest.java @@ -4,6 +4,7 @@ import com.deftdevs.bootstrapi.commons.model.MailServerPopModel; import com.deftdevs.bootstrapi.commons.model.MailServerSmtpModel; import com.deftdevs.bootstrapi.commons.model.SettingsGeneralModel; +import com.deftdevs.bootstrapi.commons.util.FieldNames; import com.deftdevs.bootstrapi.confluence.model.SettingsCustomHtmlModel; import com.deftdevs.bootstrapi.confluence.model.SettingsModel; import com.deftdevs.bootstrapi.confluence.model._AllModel; @@ -23,6 +24,11 @@ protected Object getExampleAllModel() { .build(); } + @Override + protected String getSettingsGeneralStatusKey() { + return FieldNames.pathOf(_AllModel.class, SettingsGeneralModel.class); + } + @Override protected SettingsGeneralModel getExampleSettingsGeneralModel() { return SettingsGeneralModel.EXAMPLE_1_NO_MODE; @@ -42,9 +48,9 @@ void testSetAllAppliesMultipleEntities() throws Exception { .build(); assertSetAllApplied(allModel, Arrays.asList( - "settings/general", - "settings/customHtml", - "mailServer/smtp", - "mailServer/pop")); + FieldNames.pathOf(_AllModel.class, SettingsGeneralModel.class), + FieldNames.pathOf(_AllModel.class, SettingsCustomHtmlModel.class), + FieldNames.pathOf(_AllModel.class, MailServerSmtpModel.class), + FieldNames.pathOf(_AllModel.class, MailServerPopModel.class))); } } diff --git a/crowd/src/main/java/com/deftdevs/bootstrapi/crowd/model/SettingsBrandingLoginPageModel.java b/crowd/src/main/java/com/deftdevs/bootstrapi/crowd/model/SettingsBrandingLoginPageModel.java index d1a327a2..cdbedbf6 100644 --- a/crowd/src/main/java/com/deftdevs/bootstrapi/crowd/model/SettingsBrandingLoginPageModel.java +++ b/crowd/src/main/java/com/deftdevs/bootstrapi/crowd/model/SettingsBrandingLoginPageModel.java @@ -6,6 +6,7 @@ import lombok.NoArgsConstructor; import javax.xml.bind.annotation.XmlElement; +import com.deftdevs.bootstrapi.commons.model.type.SubEntityOf; import javax.xml.bind.annotation.XmlRootElement; import static com.deftdevs.bootstrapi.commons.constants.BootstrAPI.SETTINGS; @@ -15,6 +16,7 @@ @Builder @NoArgsConstructor @AllArgsConstructor +@SubEntityOf(SettingsModel.class) @XmlRootElement(name = SETTINGS + "-" + SETTINGS_BRANDING) public class SettingsBrandingLoginPageModel { diff --git a/crowd/src/main/java/com/deftdevs/bootstrapi/crowd/service/_AllServiceImpl.java b/crowd/src/main/java/com/deftdevs/bootstrapi/crowd/service/_AllServiceImpl.java index 30af0e15..4da73dcc 100644 --- a/crowd/src/main/java/com/deftdevs/bootstrapi/crowd/service/_AllServiceImpl.java +++ b/crowd/src/main/java/com/deftdevs/bootstrapi/crowd/service/_AllServiceImpl.java @@ -1,11 +1,19 @@ package com.deftdevs.bootstrapi.crowd.service; +import com.deftdevs.bootstrapi.commons.model.AbstractDirectoryModel; +import com.deftdevs.bootstrapi.commons.model.ApplicationLinkModel; +import com.deftdevs.bootstrapi.commons.model.LicenseModel; +import com.deftdevs.bootstrapi.commons.model.MailServerModel; import com.deftdevs.bootstrapi.commons.model.type._AllModelStatus; import com.deftdevs.bootstrapi.commons.service._AbstractAllServiceImpl; import com.deftdevs.bootstrapi.commons.service.api.ApplicationLinksService; import com.deftdevs.bootstrapi.commons.service.api.DirectoriesService; import com.deftdevs.bootstrapi.commons.service.api.LicensesService; import com.deftdevs.bootstrapi.commons.service.api.MailServerService; +import com.deftdevs.bootstrapi.crowd.model.ApplicationModel; +import com.deftdevs.bootstrapi.crowd.model.MailTemplatesModel; +import com.deftdevs.bootstrapi.crowd.model.SessionConfigModel; +import com.deftdevs.bootstrapi.crowd.model.SettingsModel; import com.deftdevs.bootstrapi.crowd.model._AllModel; import com.deftdevs.bootstrapi.crowd.service.api.ApplicationsService; import com.deftdevs.bootstrapi.crowd.service.api.CrowdSettingsService; @@ -58,31 +66,31 @@ public _AllModel setAll( final _AllModel result = new _AllModel(); final Map statusMap = new HashMap<>(); - setEntityWithStatus("settings", allModel.getSettings(), + setEntityWithStatus(SettingsModel.class, allModel.getSettings(), settingsService::setSettings, result::setSettings, statusMap); - setEntities("directories", allModel.getDirectories(), + setEntities(AbstractDirectoryModel.class, allModel.getDirectories(), directoriesService::setDirectories, result::setDirectories, statusMap); - setEntities("applications", allModel.getApplications(), + setEntities(ApplicationModel.class, allModel.getApplications(), applicationsService::setApplications, result::setApplications, statusMap); - setEntities("applicationLinks", allModel.getApplicationLinks(), + setEntities(ApplicationLinkModel.class, allModel.getApplicationLinks(), applicationLinksService::setApplicationLinks, result::setApplicationLinks, statusMap); - setEntities("licenses", allModel.getLicenses(), + setEntities(LicenseModel.class, allModel.getLicenses(), licensesService::setLicenses, result::setLicenses, statusMap); - setEntityWithStatus("mailServer", allModel.getMailServer(), + setEntityWithStatus(MailServerModel.class, allModel.getMailServer(), mailServerService::setMailServer, result::setMailServer, statusMap); - setEntity("mailTemplates", allModel.getMailTemplates(), + setEntity(MailTemplatesModel.class, allModel.getMailTemplates(), mailTemplatesService::setMailTemplates, result::setMailTemplates, statusMap); - setEntity("sessionConfig", allModel.getSessionConfig(), + setEntity(SessionConfigModel.class, allModel.getSessionConfig(), sessionConfigService::setSessionConfig, result::setSessionConfig, statusMap); - setEntity("trustedProxies", allModel.getTrustedProxies(), + setEntity(String.class, allModel.getTrustedProxies(), trustedProxiesService::setTrustedProxies, result::setTrustedProxies, statusMap); result.setStatus(statusMap); diff --git a/crowd/src/main/java/com/deftdevs/bootstrapi/crowd/service/api/CrowdSettingsService.java b/crowd/src/main/java/com/deftdevs/bootstrapi/crowd/service/api/CrowdSettingsService.java index 06a62fae..a18601fb 100644 --- a/crowd/src/main/java/com/deftdevs/bootstrapi/crowd/service/api/CrowdSettingsService.java +++ b/crowd/src/main/java/com/deftdevs/bootstrapi/crowd/service/api/CrowdSettingsService.java @@ -3,6 +3,8 @@ import com.deftdevs.bootstrapi.commons.model.type.ServiceResult; import com.deftdevs.bootstrapi.commons.model.type._AllModelStatus; import com.deftdevs.bootstrapi.commons.util.ServiceResultUtil; +import com.deftdevs.bootstrapi.commons.model.SettingsGeneralModel; +import com.deftdevs.bootstrapi.crowd.model.SettingsBrandingLoginPageModel; import com.deftdevs.bootstrapi.crowd.model.SettingsModel; import java.util.LinkedHashMap; @@ -20,9 +22,9 @@ default ServiceResult setSettings(final SettingsModel settingsMod final SettingsModel result = new SettingsModel(); final Map status = new LinkedHashMap<>(); - ServiceResultUtil.setSubEntity(status, "general", settingsModel.getGeneral(), + ServiceResultUtil.setSubEntity(status, SettingsGeneralModel.class, settingsModel.getGeneral(), this::setSettingsGeneral, result::setGeneral); - ServiceResultUtil.setSubEntity(status, "branding", settingsModel.getBranding(), + ServiceResultUtil.setSubEntity(status, SettingsBrandingLoginPageModel.class, settingsModel.getBranding(), this::setLoginPage, result::setBranding); return new ServiceResult<>(result, status); diff --git a/crowd/src/test/java/com/deftdevs/bootstrapi/crowd/rest/_AllResourceTest.java b/crowd/src/test/java/com/deftdevs/bootstrapi/crowd/rest/_AllResourceTest.java index 6b781de3..2fa44e93 100644 --- a/crowd/src/test/java/com/deftdevs/bootstrapi/crowd/rest/_AllResourceTest.java +++ b/crowd/src/test/java/com/deftdevs/bootstrapi/crowd/rest/_AllResourceTest.java @@ -4,7 +4,10 @@ import com.deftdevs.bootstrapi.crowd.model.ApplicationModel; import com.deftdevs.bootstrapi.crowd.model.SettingsModel; import com.deftdevs.bootstrapi.crowd.model._AllModel; +import com.deftdevs.bootstrapi.commons.model.AbstractDirectoryModel; +import com.deftdevs.bootstrapi.commons.model.ApplicationLinkModel; import com.deftdevs.bootstrapi.commons.model.type._AllModelStatus; +import com.deftdevs.bootstrapi.commons.util.FieldNames; import com.deftdevs.bootstrapi.commons.service.api._AllService; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -44,7 +47,7 @@ public void setup() { allModel.setApplications(applications); final Map status = new HashMap<>(); - status.put("settings/general", _AllModelStatus.success()); + status.put(FieldNames.pathOf(_AllModel.class, SettingsGeneralModel.class), _AllModelStatus.success()); status.put(DIRECTORIES, _AllModelStatus.success()); status.put(APPLICATIONS, _AllModelStatus.success()); allModel.setStatus(status); @@ -67,7 +70,7 @@ public void testSetAll() { public void testSetAllReturns207OnMixedSuccessAndServerError() { final _AllModel result = new _AllModel(); final Map status = new HashMap<>(); - status.put("settings/general", _AllModelStatus.success()); + status.put(FieldNames.pathOf(_AllModel.class, SettingsGeneralModel.class), _AllModelStatus.success()); status.put(DIRECTORIES, _AllModelStatus.error(Response.Status.INTERNAL_SERVER_ERROR, "boom", null)); result.setStatus(status); @@ -81,7 +84,7 @@ public void testSetAllReturns207OnMixedSuccessAndServerError() { public void testSetAllReturns207OnMixedSuccessAndClientError() { final _AllModel result = new _AllModel(); final Map status = new HashMap<>(); - status.put("settings/general", _AllModelStatus.success()); + status.put(FieldNames.pathOf(_AllModel.class, SettingsGeneralModel.class), _AllModelStatus.success()); status.put(DIRECTORIES, _AllModelStatus.error(Response.Status.BAD_REQUEST, "bad", null)); result.setStatus(status); diff --git a/crowd/src/test/java/com/deftdevs/bootstrapi/crowd/service/CrowdSettingsServiceTest.java b/crowd/src/test/java/com/deftdevs/bootstrapi/crowd/service/CrowdSettingsServiceTest.java index 36a40f4c..fc173780 100644 --- a/crowd/src/test/java/com/deftdevs/bootstrapi/crowd/service/CrowdSettingsServiceTest.java +++ b/crowd/src/test/java/com/deftdevs/bootstrapi/crowd/service/CrowdSettingsServiceTest.java @@ -4,6 +4,7 @@ import com.deftdevs.bootstrapi.commons.exception.web.BadRequestException; import com.deftdevs.bootstrapi.commons.model.SettingsGeneralModel; import com.deftdevs.bootstrapi.commons.model.type.ServiceResult; +import com.deftdevs.bootstrapi.commons.util.FieldNames; import com.deftdevs.bootstrapi.crowd.model.SettingsBrandingLoginPageModel; import com.deftdevs.bootstrapi.crowd.model.SettingsModel; import com.deftdevs.bootstrapi.crowd.service.api.CrowdSettingsBrandingService; @@ -62,8 +63,8 @@ void testSetSettingsAppliesAllSubFields() { assertEquals(SettingsGeneralModel.EXAMPLE_1, result.getModel().getGeneral()); assertEquals(SettingsBrandingLoginPageModel.EXAMPLE_1, result.getModel().getBranding()); - assertEquals(Response.Status.OK.getStatusCode(), result.getStatus().get("general").getStatus()); - assertEquals(Response.Status.OK.getStatusCode(), result.getStatus().get("branding").getStatus()); + assertEquals(Response.Status.OK.getStatusCode(), result.getStatus().get(FieldNames.of(SettingsModel.class, SettingsGeneralModel.class)).getStatus()); + assertEquals(Response.Status.OK.getStatusCode(), result.getStatus().get(FieldNames.of(SettingsModel.class, SettingsBrandingLoginPageModel.class)).getStatus()); } @Test @@ -87,7 +88,7 @@ void testSetSettingsRecordsPerSubFieldFailure() { assertEquals(SettingsGeneralModel.EXAMPLE_1, result.getModel().getGeneral()); assertNull(result.getModel().getBranding()); - assertEquals(Response.Status.OK.getStatusCode(), result.getStatus().get("general").getStatus()); - assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), result.getStatus().get("branding").getStatus()); + assertEquals(Response.Status.OK.getStatusCode(), result.getStatus().get(FieldNames.of(SettingsModel.class, SettingsGeneralModel.class)).getStatus()); + assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), result.getStatus().get(FieldNames.of(SettingsModel.class, SettingsBrandingLoginPageModel.class)).getStatus()); } } diff --git a/crowd/src/test/java/com/deftdevs/bootstrapi/crowd/service/_AllServiceImplTest.java b/crowd/src/test/java/com/deftdevs/bootstrapi/crowd/service/_AllServiceImplTest.java index 3dd4edf1..7fe5acf5 100644 --- a/crowd/src/test/java/com/deftdevs/bootstrapi/crowd/service/_AllServiceImplTest.java +++ b/crowd/src/test/java/com/deftdevs/bootstrapi/crowd/service/_AllServiceImplTest.java @@ -7,6 +7,7 @@ import com.deftdevs.bootstrapi.commons.model.MailServerSmtpModel; import com.deftdevs.bootstrapi.commons.model.SettingsGeneralModel; import com.deftdevs.bootstrapi.commons.model.type.ServiceResult; +import com.deftdevs.bootstrapi.commons.util.FieldNames; import com.deftdevs.bootstrapi.commons.model.type._AllModelStatus; import com.deftdevs.bootstrapi.commons.service.api.ApplicationLinksService; import com.deftdevs.bootstrapi.commons.service.api.DirectoriesService; @@ -118,14 +119,14 @@ void testSetAllAppliesAllFields() { final List trustedProxies = Collections.singletonList("192.168.0.1"); doReturn(new ServiceResult<>(settings, - Collections.singletonMap("general", _AllModelStatus.success()))) + Collections.singletonMap(FieldNames.of(SettingsModel.class, SettingsGeneralModel.class), _AllModelStatus.success()))) .when(settingsService).setSettings(settings); doReturn(directories).when(directoriesService).setDirectories(directories); doReturn(applications).when(applicationsService).setApplications(applications); doReturn(applicationLinks).when(applicationLinksService).setApplicationLinks(applicationLinks); doReturn(redactedLicenses).when(licensesService).setLicenses(licenses); doReturn(new ServiceResult<>(mailServer, - Collections.singletonMap("smtp", _AllModelStatus.success()))) + Collections.singletonMap(FieldNames.of(MailServerModel.class, MailServerSmtpModel.class), _AllModelStatus.success()))) .when(mailServerService).setMailServer(mailServer); doReturn(mailTemplates).when(mailTemplatesService).setMailTemplates(mailTemplates); doReturn(sessionConfig).when(sessionConfigService).setSessionConfig(sessionConfig); @@ -156,15 +157,15 @@ void testSetAllAppliesAllFields() { final Map status = result.getStatus(); assertEquals(9, status.size()); - assertEquals(Response.Status.OK.getStatusCode(), status.get("settings/general").getStatus()); - assertEquals(Response.Status.OK.getStatusCode(), status.get("directories").getStatus()); - assertEquals(Response.Status.OK.getStatusCode(), status.get("applications").getStatus()); - assertEquals(Response.Status.OK.getStatusCode(), status.get("applicationLinks").getStatus()); - assertEquals(Response.Status.OK.getStatusCode(), status.get("licenses").getStatus()); - assertEquals(Response.Status.OK.getStatusCode(), status.get("mailServer/smtp").getStatus()); - assertEquals(Response.Status.OK.getStatusCode(), status.get("mailTemplates").getStatus()); - assertEquals(Response.Status.OK.getStatusCode(), status.get("sessionConfig").getStatus()); - assertEquals(Response.Status.OK.getStatusCode(), status.get("trustedProxies").getStatus()); + assertEquals(Response.Status.OK.getStatusCode(), status.get(FieldNames.pathOf(_AllModel.class, SettingsGeneralModel.class)).getStatus()); + assertEquals(Response.Status.OK.getStatusCode(), status.get(FieldNames.of(_AllModel.class, AbstractDirectoryModel.class)).getStatus()); + assertEquals(Response.Status.OK.getStatusCode(), status.get(FieldNames.of(_AllModel.class, ApplicationModel.class)).getStatus()); + assertEquals(Response.Status.OK.getStatusCode(), status.get(FieldNames.of(_AllModel.class, ApplicationLinkModel.class)).getStatus()); + assertEquals(Response.Status.OK.getStatusCode(), status.get(FieldNames.of(_AllModel.class, LicenseModel.class)).getStatus()); + assertEquals(Response.Status.OK.getStatusCode(), status.get(FieldNames.pathOf(_AllModel.class, MailServerSmtpModel.class)).getStatus()); + assertEquals(Response.Status.OK.getStatusCode(), status.get(FieldNames.of(_AllModel.class, MailTemplatesModel.class)).getStatus()); + assertEquals(Response.Status.OK.getStatusCode(), status.get(FieldNames.of(_AllModel.class, SessionConfigModel.class)).getStatus()); + assertEquals(Response.Status.OK.getStatusCode(), status.get(FieldNames.of(_AllModel.class, String.class)).getStatus()); } @Test @@ -175,7 +176,7 @@ void testSetAllRecordsFailureAndContinuesWithOtherFields() { Collections.singletonMap("licenseKey", LicenseModel.EXAMPLE_1); doReturn(new ServiceResult<>(settings, - Collections.singletonMap("general", _AllModelStatus.success()))) + Collections.singletonMap(FieldNames.of(SettingsModel.class, SettingsGeneralModel.class), _AllModelStatus.success()))) .when(settingsService).setSettings(settings); doThrow(new WebApplicationException(Response.Status.CONFLICT)) .when(licensesService).setLicenses(licenses); @@ -188,7 +189,7 @@ void testSetAllRecordsFailureAndContinuesWithOtherFields() { assertEquals(settings, result.getSettings()); assertNull(result.getLicenses()); - assertEquals(Response.Status.OK.getStatusCode(), result.getStatus().get("settings/general").getStatus()); - assertEquals(Response.Status.CONFLICT.getStatusCode(), result.getStatus().get("licenses").getStatus()); + assertEquals(Response.Status.OK.getStatusCode(), result.getStatus().get(FieldNames.pathOf(_AllModel.class, SettingsGeneralModel.class)).getStatus()); + assertEquals(Response.Status.CONFLICT.getStatusCode(), result.getStatus().get(FieldNames.of(_AllModel.class, LicenseModel.class)).getStatus()); } } diff --git a/crowd/src/test/java/it/com/deftdevs/bootstrapi/crowd/rest/_AllResourceFuncTest.java b/crowd/src/test/java/it/com/deftdevs/bootstrapi/crowd/rest/_AllResourceFuncTest.java index 90e8b640..45e74ebe 100644 --- a/crowd/src/test/java/it/com/deftdevs/bootstrapi/crowd/rest/_AllResourceFuncTest.java +++ b/crowd/src/test/java/it/com/deftdevs/bootstrapi/crowd/rest/_AllResourceFuncTest.java @@ -3,6 +3,7 @@ import com.deftdevs.bootstrapi.commons.model.MailServerModel; import com.deftdevs.bootstrapi.commons.model.MailServerSmtpModel; import com.deftdevs.bootstrapi.commons.model.SettingsGeneralModel; +import com.deftdevs.bootstrapi.commons.util.FieldNames; import com.deftdevs.bootstrapi.crowd.model.MailTemplatesModel; import com.deftdevs.bootstrapi.crowd.model.SessionConfigModel; import com.deftdevs.bootstrapi.crowd.model.SettingsModel; @@ -23,6 +24,11 @@ protected Object getExampleAllModel() { .build(); } + @Override + protected String getSettingsGeneralStatusKey() { + return FieldNames.pathOf(_AllModel.class, SettingsGeneralModel.class); + } + @Override protected SettingsGeneralModel getExampleSettingsGeneralModel() { return SettingsGeneralModel.EXAMPLE_1_MINIMAL; @@ -44,10 +50,10 @@ void testSetAllAppliesMultipleEntities() throws Exception { .build(); assertSetAllApplied(allModel, Arrays.asList( - "settings/general", - "mailServer/smtp", - "mailTemplates", - "sessionConfig", - "trustedProxies")); + FieldNames.pathOf(_AllModel.class, SettingsGeneralModel.class), + FieldNames.pathOf(_AllModel.class, MailServerSmtpModel.class), + FieldNames.of(_AllModel.class, MailTemplatesModel.class), + FieldNames.of(_AllModel.class, SessionConfigModel.class), + FieldNames.of(_AllModel.class, String.class))); } } diff --git a/jira/src/main/java/com/deftdevs/bootstrapi/jira/model/SettingsBannerModel.java b/jira/src/main/java/com/deftdevs/bootstrapi/jira/model/SettingsBannerModel.java index d739a6b7..5c3efc4c 100644 --- a/jira/src/main/java/com/deftdevs/bootstrapi/jira/model/SettingsBannerModel.java +++ b/jira/src/main/java/com/deftdevs/bootstrapi/jira/model/SettingsBannerModel.java @@ -8,6 +8,7 @@ import javax.xml.bind.annotation.XmlAccessType; import javax.xml.bind.annotation.XmlAccessorType; import javax.xml.bind.annotation.XmlElement; +import com.deftdevs.bootstrapi.commons.model.type.SubEntityOf; import javax.xml.bind.annotation.XmlRootElement; import static com.deftdevs.bootstrapi.commons.constants.BootstrAPI.SETTINGS; @@ -17,6 +18,7 @@ @Builder @NoArgsConstructor @AllArgsConstructor +@SubEntityOf(SettingsModel.class) @XmlRootElement(name = SETTINGS + "-" + SETTINGS_BANNER) @XmlAccessorType(XmlAccessType.FIELD) public class SettingsBannerModel { diff --git a/jira/src/main/java/com/deftdevs/bootstrapi/jira/service/_AllServiceImpl.java b/jira/src/main/java/com/deftdevs/bootstrapi/jira/service/_AllServiceImpl.java index b7c62262..dab5d507 100644 --- a/jira/src/main/java/com/deftdevs/bootstrapi/jira/service/_AllServiceImpl.java +++ b/jira/src/main/java/com/deftdevs/bootstrapi/jira/service/_AllServiceImpl.java @@ -1,5 +1,11 @@ package com.deftdevs.bootstrapi.jira.service; +import com.deftdevs.bootstrapi.commons.model.AbstractDirectoryModel; +import com.deftdevs.bootstrapi.commons.model.ApplicationLinkModel; +import com.deftdevs.bootstrapi.commons.model.AuthenticationModel; +import com.deftdevs.bootstrapi.commons.model.LicenseModel; +import com.deftdevs.bootstrapi.commons.model.MailServerModel; +import com.deftdevs.bootstrapi.commons.model.PermissionsGlobalModel; import com.deftdevs.bootstrapi.commons.model.type._AllModelStatus; import com.deftdevs.bootstrapi.commons.service._AbstractAllServiceImpl; import com.deftdevs.bootstrapi.commons.service.api.ApplicationLinksService; @@ -7,6 +13,7 @@ import com.deftdevs.bootstrapi.commons.service.api.LicensesService; import com.deftdevs.bootstrapi.commons.service.api.MailServerService; import com.deftdevs.bootstrapi.commons.service.api.PermissionsService; +import com.deftdevs.bootstrapi.jira.model.SettingsModel; import com.deftdevs.bootstrapi.jira.model._AllModel; import com.deftdevs.bootstrapi.jira.service.api.JiraAuthenticationService; import com.deftdevs.bootstrapi.jira.service.api.JiraSettingsService; @@ -50,25 +57,25 @@ public _AllModel setAll( final _AllModel result = new _AllModel(); final Map statusMap = new HashMap<>(); - setEntityWithStatus("settings", allModel.getSettings(), + setEntityWithStatus(SettingsModel.class, allModel.getSettings(), settingsService::setSettings, result::setSettings, statusMap); - setEntities("directories", allModel.getDirectories(), + setEntities(AbstractDirectoryModel.class, allModel.getDirectories(), directoriesService::setDirectories, result::setDirectories, statusMap); - setEntities("applicationLinks", allModel.getApplicationLinks(), + setEntities(ApplicationLinkModel.class, allModel.getApplicationLinks(), applicationLinksService::setApplicationLinks, result::setApplicationLinks, statusMap); - setEntityWithStatus("authentication", allModel.getAuthentication(), + setEntityWithStatus(AuthenticationModel.class, allModel.getAuthentication(), authenticationService::setAuthentication, result::setAuthentication, statusMap); - setEntities("licenses", allModel.getLicenses(), + setEntities(LicenseModel.class, allModel.getLicenses(), licensesService::setLicenses, result::setLicenses, statusMap); - setEntityWithStatus("mailServer", allModel.getMailServer(), + setEntityWithStatus(MailServerModel.class, allModel.getMailServer(), mailServerService::setMailServer, result::setMailServer, statusMap); - setEntity("permissionsGlobal", allModel.getPermissionsGlobal(), + setEntity(PermissionsGlobalModel.class, allModel.getPermissionsGlobal(), permissionsService::setPermissionsGlobal, result::setPermissionsGlobal, statusMap); result.setStatus(statusMap); diff --git a/jira/src/main/java/com/deftdevs/bootstrapi/jira/service/api/JiraSettingsService.java b/jira/src/main/java/com/deftdevs/bootstrapi/jira/service/api/JiraSettingsService.java index ce6ec671..bef053c6 100644 --- a/jira/src/main/java/com/deftdevs/bootstrapi/jira/service/api/JiraSettingsService.java +++ b/jira/src/main/java/com/deftdevs/bootstrapi/jira/service/api/JiraSettingsService.java @@ -30,11 +30,11 @@ default ServiceResult setSettings(final SettingsModel settingsMod final SettingsModel result = new SettingsModel(); final Map status = new LinkedHashMap<>(); - ServiceResultUtil.setSubEntity(status, "general", settingsModel.getGeneral(), + ServiceResultUtil.setSubEntity(status, SettingsGeneralModel.class, settingsModel.getGeneral(), this::setSettingsGeneral, result::setGeneral); - ServiceResultUtil.setSubEntity(status, "security", settingsModel.getSecurity(), + ServiceResultUtil.setSubEntity(status, SettingsSecurityModel.class, settingsModel.getSecurity(), this::setSettingsSecurity, result::setSecurity); - ServiceResultUtil.setSubEntity(status, "banner", settingsModel.getBanner(), + ServiceResultUtil.setSubEntity(status, SettingsBannerModel.class, settingsModel.getBanner(), this::setSettingsBanner, result::setBanner); return new ServiceResult<>(result, status); diff --git a/jira/src/test/java/com/deftdevs/bootstrapi/jira/rest/_AllResourceTest.java b/jira/src/test/java/com/deftdevs/bootstrapi/jira/rest/_AllResourceTest.java index bba239e0..00648efb 100644 --- a/jira/src/test/java/com/deftdevs/bootstrapi/jira/rest/_AllResourceTest.java +++ b/jira/src/test/java/com/deftdevs/bootstrapi/jira/rest/_AllResourceTest.java @@ -1,7 +1,10 @@ package com.deftdevs.bootstrapi.jira.rest; import com.deftdevs.bootstrapi.commons.model.SettingsGeneralModel; +import com.deftdevs.bootstrapi.commons.model.AbstractDirectoryModel; +import com.deftdevs.bootstrapi.commons.model.ApplicationLinkModel; import com.deftdevs.bootstrapi.commons.model.type._AllModelStatus; +import com.deftdevs.bootstrapi.commons.util.FieldNames; import com.deftdevs.bootstrapi.commons.service.api._AllService; import com.deftdevs.bootstrapi.jira.model.SettingsModel; import com.deftdevs.bootstrapi.jira.model._AllModel; @@ -39,7 +42,7 @@ public void setup() { allModel.setSettings(settings); final Map status = new HashMap<>(); - status.put("settings/general", _AllModelStatus.success()); + status.put(FieldNames.pathOf(_AllModel.class, SettingsGeneralModel.class), _AllModelStatus.success()); status.put(DIRECTORIES, _AllModelStatus.success()); status.put(APPLICATION_LINKS, _AllModelStatus.success()); allModel.setStatus(status); @@ -62,7 +65,7 @@ public void testSetAll() { public void testSetAllReturns207OnMixedSuccessAndServerError() { final _AllModel result = new _AllModel(); final Map status = new HashMap<>(); - status.put("settings/general", _AllModelStatus.success()); + status.put(FieldNames.pathOf(_AllModel.class, SettingsGeneralModel.class), _AllModelStatus.success()); status.put(DIRECTORIES, _AllModelStatus.error(Response.Status.INTERNAL_SERVER_ERROR, "boom", null)); result.setStatus(status); @@ -76,7 +79,7 @@ public void testSetAllReturns207OnMixedSuccessAndServerError() { public void testSetAllReturns207OnMixedSuccessAndClientError() { final _AllModel result = new _AllModel(); final Map status = new HashMap<>(); - status.put("settings/general", _AllModelStatus.success()); + status.put(FieldNames.pathOf(_AllModel.class, SettingsGeneralModel.class), _AllModelStatus.success()); status.put(DIRECTORIES, _AllModelStatus.error(Response.Status.BAD_REQUEST, "bad", null)); result.setStatus(status); diff --git a/jira/src/test/java/com/deftdevs/bootstrapi/jira/service/JiraSettingsServiceTest.java b/jira/src/test/java/com/deftdevs/bootstrapi/jira/service/JiraSettingsServiceTest.java index 88bf6a36..d05d4652 100644 --- a/jira/src/test/java/com/deftdevs/bootstrapi/jira/service/JiraSettingsServiceTest.java +++ b/jira/src/test/java/com/deftdevs/bootstrapi/jira/service/JiraSettingsServiceTest.java @@ -5,6 +5,7 @@ import com.deftdevs.bootstrapi.commons.model.SettingsGeneralModel; import com.deftdevs.bootstrapi.commons.model.SettingsSecurityModel; import com.deftdevs.bootstrapi.commons.model.type.ServiceResult; +import com.deftdevs.bootstrapi.commons.util.FieldNames; import com.deftdevs.bootstrapi.jira.model.SettingsBannerModel; import com.deftdevs.bootstrapi.jira.model.SettingsModel; import org.junit.jupiter.api.BeforeEach; @@ -139,9 +140,9 @@ void testSetSettingsAppliesAllSubFields() { assertEquals(SettingsGeneralModel.EXAMPLE_1, result.getModel().getGeneral()); assertEquals(SettingsSecurityModel.EXAMPLE_1, result.getModel().getSecurity()); assertEquals(SettingsBannerModel.EXAMPLE_1, result.getModel().getBanner()); - assertEquals(Response.Status.OK.getStatusCode(), result.getStatus().get("general").getStatus()); - assertEquals(Response.Status.OK.getStatusCode(), result.getStatus().get("security").getStatus()); - assertEquals(Response.Status.OK.getStatusCode(), result.getStatus().get("banner").getStatus()); + assertEquals(Response.Status.OK.getStatusCode(), result.getStatus().get(FieldNames.of(SettingsModel.class, SettingsGeneralModel.class)).getStatus()); + assertEquals(Response.Status.OK.getStatusCode(), result.getStatus().get(FieldNames.of(SettingsModel.class, SettingsSecurityModel.class)).getStatus()); + assertEquals(Response.Status.OK.getStatusCode(), result.getStatus().get(FieldNames.of(SettingsModel.class, SettingsBannerModel.class)).getStatus()); } @Test @@ -167,8 +168,8 @@ void testSetSettingsRecordsPerSubFieldFailure() { assertEquals(SettingsGeneralModel.EXAMPLE_1, result.getModel().getGeneral()); assertNull(result.getModel().getBanner()); - assertEquals(Response.Status.OK.getStatusCode(), result.getStatus().get("general").getStatus()); - assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), result.getStatus().get("banner").getStatus()); + assertEquals(Response.Status.OK.getStatusCode(), result.getStatus().get(FieldNames.of(SettingsModel.class, SettingsGeneralModel.class)).getStatus()); + assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), result.getStatus().get(FieldNames.of(SettingsModel.class, SettingsBannerModel.class)).getStatus()); } } diff --git a/jira/src/test/java/com/deftdevs/bootstrapi/jira/service/_AllServiceImplTest.java b/jira/src/test/java/com/deftdevs/bootstrapi/jira/service/_AllServiceImplTest.java index 3dd4b66d..d0e09ea7 100644 --- a/jira/src/test/java/com/deftdevs/bootstrapi/jira/service/_AllServiceImplTest.java +++ b/jira/src/test/java/com/deftdevs/bootstrapi/jira/service/_AllServiceImplTest.java @@ -7,8 +7,10 @@ import com.deftdevs.bootstrapi.commons.model.MailServerModel; import com.deftdevs.bootstrapi.commons.model.MailServerSmtpModel; import com.deftdevs.bootstrapi.commons.model.PermissionsGlobalModel; +import com.deftdevs.bootstrapi.commons.model.AuthenticationSsoModel; import com.deftdevs.bootstrapi.commons.model.SettingsGeneralModel; import com.deftdevs.bootstrapi.commons.model.type.ServiceResult; +import com.deftdevs.bootstrapi.commons.util.FieldNames; import com.deftdevs.bootstrapi.commons.model.type._AllModelStatus; import com.deftdevs.bootstrapi.commons.service.api.ApplicationLinksService; import com.deftdevs.bootstrapi.commons.service.api.DirectoriesService; @@ -102,16 +104,16 @@ void testSetAllAppliesAllFields() { final PermissionsGlobalModel permissionsGlobal = new PermissionsGlobalModel(); doReturn(new ServiceResult<>(settings, - Collections.singletonMap("general", _AllModelStatus.success()))) + Collections.singletonMap(FieldNames.of(SettingsModel.class, SettingsGeneralModel.class), _AllModelStatus.success()))) .when(settingsService).setSettings(settings); doReturn(directories).when(directoriesService).setDirectories(directories); doReturn(applicationLinks).when(applicationLinksService).setApplicationLinks(applicationLinks); doReturn(new ServiceResult<>(authentication, - Collections.singletonMap("sso", _AllModelStatus.success()))) + Collections.singletonMap(FieldNames.of(AuthenticationModel.class, AuthenticationSsoModel.class), _AllModelStatus.success()))) .when(authenticationService).setAuthentication(authentication); doReturn(redactedLicenses).when(licensesService).setLicenses(licenses); doReturn(new ServiceResult<>(mailServer, - Collections.singletonMap("smtp", _AllModelStatus.success()))) + Collections.singletonMap(FieldNames.of(MailServerModel.class, MailServerSmtpModel.class), _AllModelStatus.success()))) .when(mailServerService).setMailServer(mailServer); doReturn(permissionsGlobal).when(permissionsService).setPermissionsGlobal(permissionsGlobal); @@ -136,13 +138,13 @@ void testSetAllAppliesAllFields() { final Map status = result.getStatus(); assertEquals(7, status.size()); - assertEquals(Response.Status.OK.getStatusCode(), status.get("settings/general").getStatus()); - assertEquals(Response.Status.OK.getStatusCode(), status.get("directories").getStatus()); - assertEquals(Response.Status.OK.getStatusCode(), status.get("applicationLinks").getStatus()); - assertEquals(Response.Status.OK.getStatusCode(), status.get("authentication/sso").getStatus()); - assertEquals(Response.Status.OK.getStatusCode(), status.get("licenses").getStatus()); - assertEquals(Response.Status.OK.getStatusCode(), status.get("mailServer/smtp").getStatus()); - assertEquals(Response.Status.OK.getStatusCode(), status.get("permissionsGlobal").getStatus()); + assertEquals(Response.Status.OK.getStatusCode(), status.get(FieldNames.pathOf(_AllModel.class, SettingsGeneralModel.class)).getStatus()); + assertEquals(Response.Status.OK.getStatusCode(), status.get(FieldNames.of(_AllModel.class, AbstractDirectoryModel.class)).getStatus()); + assertEquals(Response.Status.OK.getStatusCode(), status.get(FieldNames.of(_AllModel.class, ApplicationLinkModel.class)).getStatus()); + assertEquals(Response.Status.OK.getStatusCode(), status.get(FieldNames.pathOf(_AllModel.class, AuthenticationSsoModel.class)).getStatus()); + assertEquals(Response.Status.OK.getStatusCode(), status.get(FieldNames.of(_AllModel.class, LicenseModel.class)).getStatus()); + assertEquals(Response.Status.OK.getStatusCode(), status.get(FieldNames.pathOf(_AllModel.class, MailServerSmtpModel.class)).getStatus()); + assertEquals(Response.Status.OK.getStatusCode(), status.get(FieldNames.of(_AllModel.class, PermissionsGlobalModel.class)).getStatus()); } @Test @@ -153,7 +155,7 @@ void testSetAllRecordsFailureAndContinuesWithOtherFields() { Collections.singletonMap("licenseKey", LicenseModel.EXAMPLE_1); doReturn(new ServiceResult<>(settings, - Collections.singletonMap("general", _AllModelStatus.success()))) + Collections.singletonMap(FieldNames.of(SettingsModel.class, SettingsGeneralModel.class), _AllModelStatus.success()))) .when(settingsService).setSettings(settings); doThrow(new WebApplicationException(Response.Status.CONFLICT)) .when(licensesService).setLicenses(licenses); @@ -166,7 +168,7 @@ void testSetAllRecordsFailureAndContinuesWithOtherFields() { assertEquals(settings, result.getSettings()); assertNull(result.getLicenses()); - assertEquals(Response.Status.OK.getStatusCode(), result.getStatus().get("settings/general").getStatus()); - assertEquals(Response.Status.CONFLICT.getStatusCode(), result.getStatus().get("licenses").getStatus()); + assertEquals(Response.Status.OK.getStatusCode(), result.getStatus().get(FieldNames.pathOf(_AllModel.class, SettingsGeneralModel.class)).getStatus()); + assertEquals(Response.Status.CONFLICT.getStatusCode(), result.getStatus().get(FieldNames.of(_AllModel.class, LicenseModel.class)).getStatus()); } } diff --git a/jira/src/test/java/it/com/deftdevs/bootstrapi/jira/rest/_AllResourceFuncTest.java b/jira/src/test/java/it/com/deftdevs/bootstrapi/jira/rest/_AllResourceFuncTest.java index 48c964a8..e790210a 100644 --- a/jira/src/test/java/it/com/deftdevs/bootstrapi/jira/rest/_AllResourceFuncTest.java +++ b/jira/src/test/java/it/com/deftdevs/bootstrapi/jira/rest/_AllResourceFuncTest.java @@ -3,6 +3,8 @@ import com.deftdevs.bootstrapi.commons.model.MailServerModel; import com.deftdevs.bootstrapi.commons.model.MailServerPopModel; import com.deftdevs.bootstrapi.commons.model.MailServerSmtpModel; +import com.deftdevs.bootstrapi.commons.model.SettingsGeneralModel; +import com.deftdevs.bootstrapi.commons.util.FieldNames; import com.deftdevs.bootstrapi.jira.model.SettingsBannerModel; import com.deftdevs.bootstrapi.jira.model.SettingsModel; import com.deftdevs.bootstrapi.jira.model._AllModel; @@ -13,6 +15,11 @@ public class _AllResourceFuncTest extends Abstract_AllResourceFuncTest { + @Override + protected String getSettingsGeneralStatusKey() { + return FieldNames.pathOf(_AllModel.class, SettingsGeneralModel.class); + } + @Override protected Object getExampleAllModel() { return _AllModel.builder() @@ -36,9 +43,9 @@ void testSetAllAppliesMultipleEntities() throws Exception { .build(); assertSetAllApplied(allModel, Arrays.asList( - "settings/general", - "settings/banner", - "mailServer/smtp", - "mailServer/pop")); + FieldNames.pathOf(_AllModel.class, SettingsGeneralModel.class), + FieldNames.pathOf(_AllModel.class, SettingsBannerModel.class), + FieldNames.pathOf(_AllModel.class, MailServerSmtpModel.class), + FieldNames.pathOf(_AllModel.class, MailServerPopModel.class))); } } From a9af1b479b17ca219c2f3c8e61ddf4eb586a6729 Mon Sep 17 00:00:00 2001 From: Patrick Hobusch Date: Mon, 6 Jul 2026 10:16:49 +0200 Subject: [PATCH 11/18] Persist Confluence security settings and keep status order setSettingsSecurity mutated the Settings copy returned by getGlobalSettings() but never committed it, so PUT /settings/security (and the security sub-field of the _all endpoint) reported success without persisting anything. The _all services also collected statuses in a HashMap, discarding the request-mirroring order the composites produce; LinkedHashMap keeps the response status map in request order. --- .../bootstrapi/confluence/service/SettingsServiceImpl.java | 2 ++ .../bootstrapi/confluence/service/_AllServiceImpl.java | 4 ++-- .../bootstrapi/confluence/service/SettingsServiceTest.java | 1 + .../deftdevs/bootstrapi/crowd/service/_AllServiceImpl.java | 4 ++-- .../com/deftdevs/bootstrapi/jira/service/_AllServiceImpl.java | 4 ++-- 5 files changed, 9 insertions(+), 6 deletions(-) diff --git a/confluence/src/main/java/com/deftdevs/bootstrapi/confluence/service/SettingsServiceImpl.java b/confluence/src/main/java/com/deftdevs/bootstrapi/confluence/service/SettingsServiceImpl.java index a53d3b0a..63b6ffc5 100644 --- a/confluence/src/main/java/com/deftdevs/bootstrapi/confluence/service/SettingsServiceImpl.java +++ b/confluence/src/main/java/com/deftdevs/bootstrapi/confluence/service/SettingsServiceImpl.java @@ -122,6 +122,8 @@ public SettingsSecurityModel setSettingsSecurity( settings.setWebSudoTimeout(settingsSecurityModel.getWebSudoTimeout()); } + globalSettingsManager.updateGlobalSettings(settings); + return getSettingsSecurity(); } diff --git a/confluence/src/main/java/com/deftdevs/bootstrapi/confluence/service/_AllServiceImpl.java b/confluence/src/main/java/com/deftdevs/bootstrapi/confluence/service/_AllServiceImpl.java index 8b677030..3e13e66b 100644 --- a/confluence/src/main/java/com/deftdevs/bootstrapi/confluence/service/_AllServiceImpl.java +++ b/confluence/src/main/java/com/deftdevs/bootstrapi/confluence/service/_AllServiceImpl.java @@ -18,7 +18,7 @@ import com.deftdevs.bootstrapi.confluence.service.api.ConfluenceAuthenticationService; import com.deftdevs.bootstrapi.confluence.service.api.ConfluenceSettingsService; -import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.Map; @@ -55,7 +55,7 @@ public _AllModel setAll( final _AllModel allModel) { final _AllModel result = new _AllModel(); - final Map statusMap = new HashMap<>(); + final Map statusMap = new LinkedHashMap<>(); setEntityWithStatus(SettingsModel.class, allModel.getSettings(), settingsService::setSettings, result::setSettings, statusMap); diff --git a/confluence/src/test/java/com/deftdevs/bootstrapi/confluence/service/SettingsServiceTest.java b/confluence/src/test/java/com/deftdevs/bootstrapi/confluence/service/SettingsServiceTest.java index 45fa08c0..6dcb7a52 100644 --- a/confluence/src/test/java/com/deftdevs/bootstrapi/confluence/service/SettingsServiceTest.java +++ b/confluence/src/test/java/com/deftdevs/bootstrapi/confluence/service/SettingsServiceTest.java @@ -161,6 +161,7 @@ void testSetSettingsSecurity() { spy.setSettingsSecurity(settingsSecurityModel); verify(settings).setWebSudoEnabled(settingsSecurityModel.getWebSudoEnabled()); verify(settings).setWebSudoTimeout(settingsSecurityModel.getWebSudoTimeout()); + verify(globalSettingsManager).updateGlobalSettings(settings); } } diff --git a/crowd/src/main/java/com/deftdevs/bootstrapi/crowd/service/_AllServiceImpl.java b/crowd/src/main/java/com/deftdevs/bootstrapi/crowd/service/_AllServiceImpl.java index 4da73dcc..28613657 100644 --- a/crowd/src/main/java/com/deftdevs/bootstrapi/crowd/service/_AllServiceImpl.java +++ b/crowd/src/main/java/com/deftdevs/bootstrapi/crowd/service/_AllServiceImpl.java @@ -21,7 +21,7 @@ import com.deftdevs.bootstrapi.crowd.service.api.SessionConfigService; import com.deftdevs.bootstrapi.crowd.service.api.TrustedProxiesService; -import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.Map; @@ -64,7 +64,7 @@ public _AllModel setAll( final _AllModel allModel) { final _AllModel result = new _AllModel(); - final Map statusMap = new HashMap<>(); + final Map statusMap = new LinkedHashMap<>(); setEntityWithStatus(SettingsModel.class, allModel.getSettings(), settingsService::setSettings, result::setSettings, statusMap); diff --git a/jira/src/main/java/com/deftdevs/bootstrapi/jira/service/_AllServiceImpl.java b/jira/src/main/java/com/deftdevs/bootstrapi/jira/service/_AllServiceImpl.java index dab5d507..7e951bd1 100644 --- a/jira/src/main/java/com/deftdevs/bootstrapi/jira/service/_AllServiceImpl.java +++ b/jira/src/main/java/com/deftdevs/bootstrapi/jira/service/_AllServiceImpl.java @@ -18,7 +18,7 @@ import com.deftdevs.bootstrapi.jira.service.api.JiraAuthenticationService; import com.deftdevs.bootstrapi.jira.service.api.JiraSettingsService; -import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.Map; @@ -55,7 +55,7 @@ public _AllModel setAll( final _AllModel allModel) { final _AllModel result = new _AllModel(); - final Map statusMap = new HashMap<>(); + final Map statusMap = new LinkedHashMap<>(); setEntityWithStatus(SettingsModel.class, allModel.getSettings(), settingsService::setSettings, result::setSettings, statusMap); From 432a41cf862024fb5b57458d3d4f42614842cc68 Mon Sep 17 00:00:00 2001 From: Patrick Hobusch Date: Mon, 6 Jul 2026 11:44:25 +0200 Subject: [PATCH 12/18] Rename Abstract_AllResourceFuncTest to _AbstractAllResourceFuncTest The underscore prefix leads for visual separation of the _all family, matching _AbstractAllModel, _AbstractAllServiceImpl and _AbstractAllResourceImpl. --- ...esourceFuncTest.java => _AbstractAllResourceFuncTest.java} | 2 +- .../bootstrapi/confluence/rest/_AllResourceFuncTest.java | 4 ++-- .../deftdevs/bootstrapi/crowd/rest/_AllResourceFuncTest.java | 4 ++-- .../deftdevs/bootstrapi/jira/rest/_AllResourceFuncTest.java | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) rename commons/src/test/java/it/com/deftdevs/bootstrapi/commons/rest/{Abstract_AllResourceFuncTest.java => _AbstractAllResourceFuncTest.java} (99%) diff --git a/commons/src/test/java/it/com/deftdevs/bootstrapi/commons/rest/Abstract_AllResourceFuncTest.java b/commons/src/test/java/it/com/deftdevs/bootstrapi/commons/rest/_AbstractAllResourceFuncTest.java similarity index 99% rename from commons/src/test/java/it/com/deftdevs/bootstrapi/commons/rest/Abstract_AllResourceFuncTest.java rename to commons/src/test/java/it/com/deftdevs/bootstrapi/commons/rest/_AbstractAllResourceFuncTest.java index 8c4abd75..68c815fa 100644 --- a/commons/src/test/java/it/com/deftdevs/bootstrapi/commons/rest/Abstract_AllResourceFuncTest.java +++ b/commons/src/test/java/it/com/deftdevs/bootstrapi/commons/rest/_AbstractAllResourceFuncTest.java @@ -25,7 +25,7 @@ * on all product test instances. The per-sub-field status aggregation and * partial-failure behavior are covered by unit tests. */ -public abstract class Abstract_AllResourceFuncTest { +public abstract class _AbstractAllResourceFuncTest { /** The _all endpoint is the REST root itself. */ private static final String ALL_PATH = ""; diff --git a/confluence/src/test/java/it/com/deftdevs/bootstrapi/confluence/rest/_AllResourceFuncTest.java b/confluence/src/test/java/it/com/deftdevs/bootstrapi/confluence/rest/_AllResourceFuncTest.java index 6ac0ad6c..85059036 100644 --- a/confluence/src/test/java/it/com/deftdevs/bootstrapi/confluence/rest/_AllResourceFuncTest.java +++ b/confluence/src/test/java/it/com/deftdevs/bootstrapi/confluence/rest/_AllResourceFuncTest.java @@ -8,12 +8,12 @@ import com.deftdevs.bootstrapi.confluence.model.SettingsCustomHtmlModel; import com.deftdevs.bootstrapi.confluence.model.SettingsModel; import com.deftdevs.bootstrapi.confluence.model._AllModel; -import it.com.deftdevs.bootstrapi.commons.rest.Abstract_AllResourceFuncTest; +import it.com.deftdevs.bootstrapi.commons.rest._AbstractAllResourceFuncTest; import org.junit.jupiter.api.Test; import java.util.Arrays; -public class _AllResourceFuncTest extends Abstract_AllResourceFuncTest { +public class _AllResourceFuncTest extends _AbstractAllResourceFuncTest { @Override protected Object getExampleAllModel() { diff --git a/crowd/src/test/java/it/com/deftdevs/bootstrapi/crowd/rest/_AllResourceFuncTest.java b/crowd/src/test/java/it/com/deftdevs/bootstrapi/crowd/rest/_AllResourceFuncTest.java index 45e74ebe..07436483 100644 --- a/crowd/src/test/java/it/com/deftdevs/bootstrapi/crowd/rest/_AllResourceFuncTest.java +++ b/crowd/src/test/java/it/com/deftdevs/bootstrapi/crowd/rest/_AllResourceFuncTest.java @@ -8,12 +8,12 @@ import com.deftdevs.bootstrapi.crowd.model.SessionConfigModel; import com.deftdevs.bootstrapi.crowd.model.SettingsModel; import com.deftdevs.bootstrapi.crowd.model._AllModel; -import it.com.deftdevs.bootstrapi.commons.rest.Abstract_AllResourceFuncTest; +import it.com.deftdevs.bootstrapi.commons.rest._AbstractAllResourceFuncTest; import org.junit.jupiter.api.Test; import java.util.Arrays; -public class _AllResourceFuncTest extends Abstract_AllResourceFuncTest { +public class _AllResourceFuncTest extends _AbstractAllResourceFuncTest { @Override protected Object getExampleAllModel() { diff --git a/jira/src/test/java/it/com/deftdevs/bootstrapi/jira/rest/_AllResourceFuncTest.java b/jira/src/test/java/it/com/deftdevs/bootstrapi/jira/rest/_AllResourceFuncTest.java index e790210a..6b80aac7 100644 --- a/jira/src/test/java/it/com/deftdevs/bootstrapi/jira/rest/_AllResourceFuncTest.java +++ b/jira/src/test/java/it/com/deftdevs/bootstrapi/jira/rest/_AllResourceFuncTest.java @@ -8,12 +8,12 @@ import com.deftdevs.bootstrapi.jira.model.SettingsBannerModel; import com.deftdevs.bootstrapi.jira.model.SettingsModel; import com.deftdevs.bootstrapi.jira.model._AllModel; -import it.com.deftdevs.bootstrapi.commons.rest.Abstract_AllResourceFuncTest; +import it.com.deftdevs.bootstrapi.commons.rest._AbstractAllResourceFuncTest; import org.junit.jupiter.api.Test; import java.util.Arrays; -public class _AllResourceFuncTest extends Abstract_AllResourceFuncTest { +public class _AllResourceFuncTest extends _AbstractAllResourceFuncTest { @Override protected String getSettingsGeneralStatusKey() { From 48654ef07c676e7b37e020606f4d44e667ba14cc Mon Sep 17 00:00:00 2001 From: Patrick Hobusch Date: Mon, 6 Jul 2026 11:11:17 +0200 Subject: [PATCH 13/18] Harden the _all request handling and reflective path derivation A missing or null request body now yields 400 instead of an NPE-driven 500. FieldNames.pathOf collects all candidate paths before deciding, so nested ambiguity is reported instead of silently treated as absence, a visited set guards against model cycles, and enum-typed fields are not descended into. getAuthenticationIdps uses the same curated duplicate-name error as findIdpConfigByName instead of an uncontrolled Collectors.toMap exception. --- .../rest/_AbstractAllResourceImpl.java | 5 ++ .../AbstractAuthenticationService.java | 4 +- .../bootstrapi/commons/util/FieldNames.java | 81 ++++++++++--------- .../rest/_AbstractAllResourceFuncTest.java | 7 ++ 4 files changed, 57 insertions(+), 40 deletions(-) diff --git a/commons/src/main/java/com/deftdevs/bootstrapi/commons/rest/_AbstractAllResourceImpl.java b/commons/src/main/java/com/deftdevs/bootstrapi/commons/rest/_AbstractAllResourceImpl.java index e81e1926..e117d1b9 100644 --- a/commons/src/main/java/com/deftdevs/bootstrapi/commons/rest/_AbstractAllResourceImpl.java +++ b/commons/src/main/java/com/deftdevs/bootstrapi/commons/rest/_AbstractAllResourceImpl.java @@ -1,5 +1,6 @@ package com.deftdevs.bootstrapi.commons.rest; +import com.deftdevs.bootstrapi.commons.exception.web.BadRequestException; import com.deftdevs.bootstrapi.commons.model.type._AllModelAccessor; import com.deftdevs.bootstrapi.commons.model.type._AllModelStatus; import com.deftdevs.bootstrapi.commons.rest.api._AllResource; @@ -25,6 +26,10 @@ public _AbstractAllResourceImpl( public Response setAll( final _AllModel allModel) { + if (allModel == null) { + throw new BadRequestException("A configuration must be provided in the request body"); + } + final _AllModel result = allService.setAll(allModel); final int overallStatus = computeOverallStatus(result.getStatus()); return Response.status(overallStatus).entity(result).build(); diff --git a/commons/src/main/java/com/deftdevs/bootstrapi/commons/service/AbstractAuthenticationService.java b/commons/src/main/java/com/deftdevs/bootstrapi/commons/service/AbstractAuthenticationService.java index 66bba00b..e9b0d32d 100644 --- a/commons/src/main/java/com/deftdevs/bootstrapi/commons/service/AbstractAuthenticationService.java +++ b/commons/src/main/java/com/deftdevs/bootstrapi/commons/service/AbstractAuthenticationService.java @@ -31,7 +31,9 @@ protected AbstractAuthenticationService( public Map getAuthenticationIdps() { return idpConfigService.getIdpConfigs().stream() .map(this::toAuthenticationIdpModel) - .collect(Collectors.toMap(AbstractAuthenticationIdpModel::getName, Function.identity())); + .collect(Collectors.toMap(AbstractAuthenticationIdpModel::getName, Function.identity(), (existing, replacement) -> { + throw new IllegalStateException("Duplicate name key found: " + existing.getName()); + })); } @Override diff --git a/commons/src/main/java/com/deftdevs/bootstrapi/commons/util/FieldNames.java b/commons/src/main/java/com/deftdevs/bootstrapi/commons/util/FieldNames.java index a6a20c6d..ce1dba8d 100644 --- a/commons/src/main/java/com/deftdevs/bootstrapi/commons/util/FieldNames.java +++ b/commons/src/main/java/com/deftdevs/bootstrapi/commons/util/FieldNames.java @@ -71,66 +71,69 @@ public static String pathOf(final Class rootClass, final Class leafType) { key -> resolvePath(rootClass, leafType)); } - private static String resolve(final Class modelClass, final Class fieldType) { - Field match = null; - - for (final Field field : declaredAndInheritedFields(modelClass)) { - if (!matches(field, fieldType, typeVariableBindings(modelClass))) { - continue; - } - if (match != null) { - throw new IllegalArgumentException(String.format( - "%s has more than one field holding %s: %s, %s", - modelClass.getName(), fieldType.getName(), match.getName(), field.getName())); - } - match = field; + private static String resolvePath(final Class rootClass, final Class leafType) { + final java.util.List paths = candidatePaths(rootClass, leafType, new java.util.HashSet<>()); + if (paths.isEmpty()) { + throw new IllegalArgumentException(String.format( + "%s has no field path to %s", rootClass.getName(), leafType.getName())); } - - if (match == null) { + if (paths.size() > 1) { throw new IllegalArgumentException(String.format( - "%s has no field holding %s", modelClass.getName(), fieldType.getName())); + "%s has more than one field path to %s: %s", + rootClass.getName(), leafType.getName(), String.join(", ", paths))); } - return serializedName(match); + return paths.get(0); } - private static String resolvePath(final Class rootClass, final Class leafType) { - String match = null; + private static java.util.List candidatePaths(final Class rootClass, final Class leafType, + final java.util.Set> visited) { - for (final Field field : declaredAndInheritedFields(rootClass)) { - final Map, Type> bindings = typeVariableBindings(rootClass); + final java.util.List paths = new java.util.ArrayList<>(); + if (!visited.add(rootClass)) { + return paths; + } - String candidate = null; + final Map, Type> bindings = typeVariableBindings(rootClass); + for (final Field field : declaredAndInheritedFields(rootClass)) { if (matches(field, leafType, bindings)) { - candidate = serializedName(field); - } else { - final Type resolvedType = resolveType(field.getGenericType(), bindings); - if (resolvedType instanceof Class - && isModel((Class) resolvedType) - && !resolvedType.equals(rootClass)) { - try { - candidate = serializedName(field) + "/" + resolvePath((Class) resolvedType, leafType); - } catch (IllegalArgumentException e) { - // no unique path below this field - } + paths.add(serializedName(field)); + continue; + } + + final Type resolvedType = resolveType(field.getGenericType(), bindings); + if (resolvedType instanceof Class + && isModel((Class) resolvedType) + && !((Class) resolvedType).isEnum()) { + for (final String subPath : candidatePaths((Class) resolvedType, leafType, visited)) { + paths.add(serializedName(field) + "/" + subPath); } } + } + + visited.remove(rootClass); + return paths; + } + + private static String resolve(final Class modelClass, final Class fieldType) { + Field match = null; - if (candidate == null) { + for (final Field field : declaredAndInheritedFields(modelClass)) { + if (!matches(field, fieldType, typeVariableBindings(modelClass))) { continue; } if (match != null) { throw new IllegalArgumentException(String.format( - "%s has more than one field path to %s: %s, %s", - rootClass.getName(), leafType.getName(), match, candidate)); + "%s has more than one field holding %s: %s, %s", + modelClass.getName(), fieldType.getName(), match.getName(), field.getName())); } - match = candidate; + match = field; } if (match == null) { throw new IllegalArgumentException(String.format( - "%s has no field path to %s", rootClass.getName(), leafType.getName())); + "%s has no field holding %s", modelClass.getName(), fieldType.getName())); } - return match; + return serializedName(match); } private static java.util.List declaredAndInheritedFields(final Class modelClass) { diff --git a/commons/src/test/java/it/com/deftdevs/bootstrapi/commons/rest/_AbstractAllResourceFuncTest.java b/commons/src/test/java/it/com/deftdevs/bootstrapi/commons/rest/_AbstractAllResourceFuncTest.java index 68c815fa..b854da11 100644 --- a/commons/src/test/java/it/com/deftdevs/bootstrapi/commons/rest/_AbstractAllResourceFuncTest.java +++ b/commons/src/test/java/it/com/deftdevs/bootstrapi/commons/rest/_AbstractAllResourceFuncTest.java @@ -55,6 +55,13 @@ void testSetAllAppliesGeneralSettings() throws Exception { assertEquals(getExampleSettingsGeneralModel().getTitle(), generalNode.path("title").asText()); } + @Test + void testSetAllWithoutBodyIsBadRequest() throws Exception { + final HttpResponse allResponse = HttpRequestHelper.builder(ALL_PATH) + .request(HttpMethod.PUT, null); + assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), allResponse.statusCode()); + } + @Test void testSetAllEmptyModelIsOkWithEmptyStatus() throws Exception { final HttpResponse allResponse = HttpRequestHelper.builder(ALL_PATH) From 1c742882f391a836b34b5d2fea0920c6146c6255 Mon Sep 17 00:00:00 2001 From: Patrick Hobusch Date: Mon, 6 Jul 2026 11:11:17 +0200 Subject: [PATCH 14/18] Trim the shadowed OpenAPI copy, dead constants and stale wording The _AllResource interface no longer carries an @Operation block: OpenAPI only reads the overriding product methods, so the interface copy was shadowed and could only drift. The response descriptions now state that a unanimous sub-field failure returns that status code with the same body, the 207 wording covers mixed failures without successes, and Crowd's operation notes that POP cannot be applied. Removes the unused mail-templates/session-config/trusted-proxies path constants, reverts the vestigial login-page root-element rename, and makes setEntity/setEntities void since no caller used their return value. Regenerates the docs, whose _AllModel field order had drifted after the base-model extraction. --- .../commons/constants/BootstrAPI.java | 8 ++-- .../commons/rest/api/_AllResource.java | 32 +++------------ .../service/_AbstractAllServiceImpl.java | 24 ++++------- .../service/_AbstractAllServiceImplTest.java | 40 ++++++++----------- confluence/Apis/AllApi.md | 2 +- confluence/Models/_AllModel.md | 4 +- crowd/Apis/AllApi.md | 2 +- crowd/Models/_AllModel.md | 4 +- .../model/SettingsBrandingLoginPageModel.java | 5 +-- .../crowd/rest/_AllResourceImpl.java | 3 +- jira/Apis/AllApi.md | 2 +- jira/Models/_AllModel.md | 4 +- 12 files changed, 46 insertions(+), 84 deletions(-) diff --git a/commons/src/main/java/com/deftdevs/bootstrapi/commons/constants/BootstrAPI.java b/commons/src/main/java/com/deftdevs/bootstrapi/commons/constants/BootstrAPI.java index f0dce14e..e4a07202 100644 --- a/commons/src/main/java/com/deftdevs/bootstrapi/commons/constants/BootstrAPI.java +++ b/commons/src/main/java/com/deftdevs/bootstrapi/commons/constants/BootstrAPI.java @@ -40,13 +40,11 @@ public class BootstrAPI { public static final String MAIL_SERVERS = "mail-servers"; public static final String MAIL_SERVER_POP = "pop"; public static final String MAIL_SERVER_SMTP = "smtp"; - public static final String MAIL_TEMPLATES = "mail-templates"; public static final String PERMISSION = "permission"; public static final String PERMISSIONS = "permissions"; public static final String PERMISSION_ANONYMOUS_ACCESS = "anonymous-access"; public static final String PERMISSIONS_GLOBAL = "global"; public static final String PING = "ping"; - public static final String SESSION_CONFIG = "session-config"; public static final String SETTINGS = "settings"; public static final String SETTINGS_BANNER = "banner"; public static final String SETTINGS_BRANDING = "branding"; @@ -57,7 +55,6 @@ public class BootstrAPI { public static final String SETTINGS_SECURITY = "security"; public static final String USER = "user"; public static final String USERS = "users"; - public static final String TRUSTED_PROXIES = "trusted-proxies"; public static final String USER_PASSWORD = "password"; public static final String MEDIA_TYPE_IMAGE = "image/*"; @@ -67,8 +64,9 @@ public class BootstrAPI { public static final String _ALL_PUT_RESPONSE_DESCRIPTION = "Returns the updated configuration. The per-sub-field outcome is reported in the" + " 'status' map, keyed by the request's field paths (e.g. 'settings/general', 'mailServer/smtp';" + " 2xx for success, 4xx/5xx for failure with a human-readable 'message' and optional 'details')." - + " License keys in the response are redacted (e.g. 'AAAB...wxyz#a1b2')."; - public static final String _ALL_PUT_PARTIAL_RESPONSE_DESCRIPTION = "Configuration applied partially. Inspect the per-sub-field 'status' map" + + " License keys in the response are redacted (e.g. 'AAAB...wxyz#a1b2')." + + " If all sub-fields fail with the same status code, that code is returned with the same response body."; + public static final String _ALL_PUT_PARTIAL_RESPONSE_DESCRIPTION = "Sub-field outcomes differ. Inspect the per-sub-field 'status' map" + " in the response body to see which fields succeeded and which failed."; public static final String SETTINGS_GENERAL_GET_SUMMARY = "Get the general settings"; public static final String SETTINGS_GENERAL_GET_RESPONSE_DESCRIPTION = "Returns the general settings"; diff --git a/commons/src/main/java/com/deftdevs/bootstrapi/commons/rest/api/_AllResource.java b/commons/src/main/java/com/deftdevs/bootstrapi/commons/rest/api/_AllResource.java index 62d097a0..cf49f60a 100644 --- a/commons/src/main/java/com/deftdevs/bootstrapi/commons/rest/api/_AllResource.java +++ b/commons/src/main/java/com/deftdevs/bootstrapi/commons/rest/api/_AllResource.java @@ -1,12 +1,6 @@ package com.deftdevs.bootstrapi.commons.rest.api; -import com.deftdevs.bootstrapi.commons.constants.BootstrAPI; -import com.deftdevs.bootstrapi.commons.model.ErrorCollection; import com.deftdevs.bootstrapi.commons.model.type._AllModelAccessor; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.media.Content; -import io.swagger.v3.oas.annotations.media.Schema; -import io.swagger.v3.oas.annotations.responses.ApiResponse; import javax.validation.constraints.NotNull; import javax.ws.rs.PUT; @@ -15,29 +9,13 @@ public interface _AllResource<_AllModel extends _AllModelAccessor> { /** - * Product implementations should override this method and re-declare the - * {@code @Operation} annotation with their concrete {@code _AllModel} as the - * response schema, so the generated OpenAPI documentation shows the product - * model instead of an empty response body. + * Product implementations must override this method and declare the + * {@code @Operation} annotation with their concrete {@code _AllModel} as + * the response schema. The annotation is not declared here because + * OpenAPI only reads the overriding method's annotations, so an + * interface-level copy would be shadowed and silently drift. */ @PUT - @Operation( - summary = BootstrAPI._ALL_PUT_SUMMARY, - responses = { - @ApiResponse( - responseCode = "200", - description = BootstrAPI._ALL_PUT_RESPONSE_DESCRIPTION - ), - @ApiResponse( - responseCode = "207", - description = BootstrAPI._ALL_PUT_PARTIAL_RESPONSE_DESCRIPTION - ), - @ApiResponse( - responseCode = "default", content = @Content(schema = @Schema(implementation = ErrorCollection.class)), - description = BootstrAPI.ERROR_COLLECTION_RESPONSE_DESCRIPTION - ), - } - ) Response setAll( @NotNull final _AllModel bean); diff --git a/commons/src/main/java/com/deftdevs/bootstrapi/commons/service/_AbstractAllServiceImpl.java b/commons/src/main/java/com/deftdevs/bootstrapi/commons/service/_AbstractAllServiceImpl.java index 8b88a674..90467d5b 100644 --- a/commons/src/main/java/com/deftdevs/bootstrapi/commons/service/_AbstractAllServiceImpl.java +++ b/commons/src/main/java/com/deftdevs/bootstrapi/commons/service/_AbstractAllServiceImpl.java @@ -74,7 +74,7 @@ protected void setEntityWithStatus( } } - protected _AllModelStatus setEntity( + protected void setEntity( final Class fieldType, final I input, final Function updateFunction, @@ -82,24 +82,20 @@ protected _AllModelStatus setEntity( final Map statusMap) { if (input == null) { - return null; + return; } final String field = FieldNames.of(allModelClass, fieldType); try { final O updatedEntity = updateFunction.apply(input); resultConsumer.accept(updatedEntity); - final _AllModelStatus status = _AllModelStatus.success(); - statusMap.put(field, status); - return status; + statusMap.put(field, _AllModelStatus.success()); } catch (Exception e) { - final _AllModelStatus status = ServiceResultUtil.toErrorStatus(field, e); - statusMap.put(field, status); - return status; + statusMap.put(field, ServiceResultUtil.toErrorStatus(field, e)); } } - protected _AllModelStatus setEntities( + protected void setEntities( final Class entityType, final Map entityMap, final Function, Map> updateFunction, @@ -107,20 +103,16 @@ protected _AllModelStatus setEntities( final Map statusMap) { if (entityMap == null || entityMap.isEmpty()) { - return null; + return; } final String field = FieldNames.of(allModelClass, entityType); try { final Map updatedEntities = updateFunction.apply(entityMap); resultConsumer.accept(new LinkedHashMap<>(updatedEntities)); - final _AllModelStatus status = _AllModelStatus.success(); - statusMap.put(field, status); - return status; + statusMap.put(field, _AllModelStatus.success()); } catch (Exception e) { - final _AllModelStatus status = ServiceResultUtil.toErrorStatus(field, e); - statusMap.put(field, status); - return status; + statusMap.put(field, ServiceResultUtil.toErrorStatus(field, e)); } } diff --git a/commons/src/test/java/com/deftdevs/bootstrapi/commons/service/_AbstractAllServiceImplTest.java b/commons/src/test/java/com/deftdevs/bootstrapi/commons/service/_AbstractAllServiceImplTest.java index 24345f8d..7c8c6d81 100644 --- a/commons/src/test/java/com/deftdevs/bootstrapi/commons/service/_AbstractAllServiceImplTest.java +++ b/commons/src/test/java/com/deftdevs/bootstrapi/commons/service/_AbstractAllServiceImplTest.java @@ -15,7 +15,6 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.jupiter.api.Assertions.assertTrue; class _AbstractAllServiceImplTest { @@ -59,10 +58,9 @@ public TestAllModel setAll(final TestAllModel allModel) { void setEntitySkipsNullInput() { final Map statusMap = new HashMap<>(); - final _AllModelStatus status = service.setEntity(TestEntityModel.class, null, + service.setEntity(TestEntityModel.class, null, input -> input, result -> {}, statusMap); - assertNull(status); assertTrue(statusMap.isEmpty()); } @@ -72,41 +70,39 @@ void setEntitySuccessRecordsStatusAndResultUnderTheFieldName() { final AtomicReference result = new AtomicReference<>(); final TestEntityModel updated = new TestEntityModel(); - final _AllModelStatus status = service.setEntity(TestEntityModel.class, new TestEntityModel(), + service.setEntity(TestEntityModel.class, new TestEntityModel(), input -> updated, result::set, statusMap); - assertEquals(Response.Status.OK.getStatusCode(), status.getStatus()); + assertEquals(Response.Status.OK.getStatusCode(), statusMap.get("entity").getStatus()); assertEquals(updated, result.get()); - assertSame(status, statusMap.get("entity")); } @Test void setEntityUsesWebApplicationExceptionStatusCode() { final Map statusMap = new HashMap<>(); - final _AllModelStatus status = service.setEntity(TestEntityModel.class, new TestEntityModel(), + service.setEntity(TestEntityModel.class, new TestEntityModel(), input -> { throw new WebApplicationException("conflict", Response.Status.CONFLICT); }, result -> {}, statusMap); - assertEquals(Response.Status.CONFLICT.getStatusCode(), status.getStatus()); - assertEquals("Failed to apply entity configuration", status.getMessage()); - assertSame(status, statusMap.get("entity")); + assertEquals(Response.Status.CONFLICT.getStatusCode(), statusMap.get("entity").getStatus()); + assertEquals("Failed to apply entity configuration", statusMap.get("entity").getMessage()); } @Test void setEntityMapsUnexpectedExceptionTo500WithoutDetails() { final Map statusMap = new HashMap<>(); - final _AllModelStatus status = service.setEntity(TestEntityModel.class, new TestEntityModel(), + service.setEntity(TestEntityModel.class, new TestEntityModel(), input -> { throw new IllegalStateException("internal details that must not leak"); }, result -> {}, statusMap); - assertEquals(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode(), status.getStatus()); - assertNull(status.getDetails()); + assertEquals(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode(), statusMap.get("entity").getStatus()); + assertNull(statusMap.get("entity").getDetails()); } // setEntities @@ -115,10 +111,10 @@ void setEntityMapsUnexpectedExceptionTo500WithoutDetails() { void setEntitiesSkipsNullAndEmptyMap() { final Map statusMap = new HashMap<>(); - assertNull(service.setEntities(TestEntryModel.class, null, - entities -> entities, result -> {}, statusMap)); - assertNull(service.setEntities(TestEntryModel.class, Collections.emptyMap(), - entities -> entities, result -> {}, statusMap)); + service.setEntities(TestEntryModel.class, null, + entities -> entities, result -> {}, statusMap); + service.setEntities(TestEntryModel.class, Collections.emptyMap(), + entities -> entities, result -> {}, statusMap); assertTrue(statusMap.isEmpty()); } @@ -128,12 +124,11 @@ void setEntitiesSuccessRecordsStatusAndResultUnderTheFieldName() { final AtomicReference> result = new AtomicReference<>(); final Map entities = Collections.singletonMap("key", new TestEntryModel()); - final _AllModelStatus status = service.setEntities(TestEntryModel.class, entities, + service.setEntities(TestEntryModel.class, entities, input -> input, result::set, statusMap); - assertEquals(Response.Status.OK.getStatusCode(), status.getStatus()); + assertEquals(Response.Status.OK.getStatusCode(), statusMap.get("entries").getStatus()); assertEquals(entities, result.get()); - assertSame(status, statusMap.get("entries")); } @Test @@ -141,14 +136,13 @@ void setEntitiesRecordsErrorStatusOnException() { final Map statusMap = new HashMap<>(); final Map entities = Collections.singletonMap("key", new TestEntryModel()); - final _AllModelStatus status = service.setEntities(TestEntryModel.class, entities, + service.setEntities(TestEntryModel.class, entities, input -> { throw new WebApplicationException(Response.Status.BAD_REQUEST); }, result -> {}, statusMap); - assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), status.getStatus()); - assertSame(status, statusMap.get("entries")); + assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), statusMap.get("entries").getStatus()); } // setEntityWithStatus diff --git a/confluence/Apis/AllApi.md b/confluence/Apis/AllApi.md index 9619e201..45bb7cb3 100644 --- a/confluence/Apis/AllApi.md +++ b/confluence/Apis/AllApi.md @@ -13,7 +13,7 @@ All URIs are relative to *https://CONFLUENCE_URL/rest/bootstrapi/1* Apply a complete configuration - Returns the updated configuration. The per-sub-field outcome is reported in the 'status' map, keyed by the request's field paths (e.g. 'settings/general', 'mailServer/smtp'; 2xx for success, 4xx/5xx for failure with a human-readable 'message' and optional 'details'). License keys in the response are redacted (e.g. 'AAAB...wxyz#a1b2'). + Returns the updated configuration. The per-sub-field outcome is reported in the 'status' map, keyed by the request's field paths (e.g. 'settings/general', 'mailServer/smtp'; 2xx for success, 4xx/5xx for failure with a human-readable 'message' and optional 'details'). License keys in the response are redacted (e.g. 'AAAB...wxyz#a1b2'). If all sub-fields fail with the same status code, that code is returned with the same response body. ### Parameters diff --git a/confluence/Models/_AllModel.md b/confluence/Models/_AllModel.md index 3a2e4dee..82c41779 100644 --- a/confluence/Models/_AllModel.md +++ b/confluence/Models/_AllModel.md @@ -6,11 +6,11 @@ | **settings** | [**SettingsModel**](SettingsModel.md) | | [optional] [default to null] | | **directories** | [**Map**](AbstractDirectoryModel.md) | | [optional] [default to null] | | **applicationLinks** | [**Map**](ApplicationLinkModel.md) | | [optional] [default to null] | -| **authentication** | [**AuthenticationModel**](AuthenticationModel.md) | | [optional] [default to null] | | **licenses** | [**Map**](LicenseModel.md) | | [optional] [default to null] | | **mailServer** | [**MailServerModel**](MailServerModel.md) | | [optional] [default to null] | -| **permissionsGlobal** | [**PermissionsGlobalModel**](PermissionsGlobalModel.md) | | [optional] [default to null] | | **status** | [**Map**](_AllModelStatus.md) | | [optional] [default to null] | +| **authentication** | [**AuthenticationModel**](AuthenticationModel.md) | | [optional] [default to null] | +| **permissionsGlobal** | [**PermissionsGlobalModel**](PermissionsGlobalModel.md) | | [optional] [default to null] | [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) diff --git a/crowd/Apis/AllApi.md b/crowd/Apis/AllApi.md index 91725ac0..e0c0501a 100644 --- a/crowd/Apis/AllApi.md +++ b/crowd/Apis/AllApi.md @@ -13,7 +13,7 @@ All URIs are relative to *https://CROWD_URL/rest/bootstrapi/1* Apply a complete configuration - Returns the updated configuration. The per-sub-field outcome is reported in the 'status' map, keyed by the request's field paths (e.g. 'settings/general', 'mailServer/smtp'; 2xx for success, 4xx/5xx for failure with a human-readable 'message' and optional 'details'). License keys in the response are redacted (e.g. 'AAAB...wxyz#a1b2'). + Returns the updated configuration. The per-sub-field outcome is reported in the 'status' map, keyed by the request's field paths (e.g. 'settings/general', 'mailServer/smtp'; 2xx for success, 4xx/5xx for failure with a human-readable 'message' and optional 'details'). License keys in the response are redacted (e.g. 'AAAB...wxyz#a1b2'). If all sub-fields fail with the same status code, that code is returned with the same response body. Crowd does not support applying a POP mail server configuration. ### Parameters diff --git a/crowd/Models/_AllModel.md b/crowd/Models/_AllModel.md index 5f85397e..6f85b170 100644 --- a/crowd/Models/_AllModel.md +++ b/crowd/Models/_AllModel.md @@ -5,14 +5,14 @@ |------------ | ------------- | ------------- | -------------| | **settings** | [**SettingsModel**](SettingsModel.md) | | [optional] [default to null] | | **directories** | [**Map**](AbstractDirectoryModel.md) | | [optional] [default to null] | -| **applications** | [**Map**](ApplicationModel.md) | | [optional] [default to null] | | **applicationLinks** | [**Map**](ApplicationLinkModel.md) | | [optional] [default to null] | | **licenses** | [**Map**](LicenseModel.md) | | [optional] [default to null] | | **mailServer** | [**MailServerModel**](MailServerModel.md) | | [optional] [default to null] | +| **status** | [**Map**](_AllModelStatus.md) | | [optional] [default to null] | +| **applications** | [**Map**](ApplicationModel.md) | | [optional] [default to null] | | **mailTemplates** | [**MailTemplatesModel**](MailTemplatesModel.md) | | [optional] [default to null] | | **sessionConfig** | [**SessionConfigModel**](SessionConfigModel.md) | | [optional] [default to null] | | **trustedProxies** | **List** | | [optional] [default to null] | -| **status** | [**Map**](_AllModelStatus.md) | | [optional] [default to null] | [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) diff --git a/crowd/src/main/java/com/deftdevs/bootstrapi/crowd/model/SettingsBrandingLoginPageModel.java b/crowd/src/main/java/com/deftdevs/bootstrapi/crowd/model/SettingsBrandingLoginPageModel.java index cdbedbf6..0f356ec6 100644 --- a/crowd/src/main/java/com/deftdevs/bootstrapi/crowd/model/SettingsBrandingLoginPageModel.java +++ b/crowd/src/main/java/com/deftdevs/bootstrapi/crowd/model/SettingsBrandingLoginPageModel.java @@ -6,18 +6,17 @@ import lombok.NoArgsConstructor; import javax.xml.bind.annotation.XmlElement; +import com.deftdevs.bootstrapi.commons.constants.BootstrAPI; import com.deftdevs.bootstrapi.commons.model.type.SubEntityOf; import javax.xml.bind.annotation.XmlRootElement; -import static com.deftdevs.bootstrapi.commons.constants.BootstrAPI.SETTINGS; -import static com.deftdevs.bootstrapi.commons.constants.BootstrAPI.SETTINGS_BRANDING; @Data @Builder @NoArgsConstructor @AllArgsConstructor @SubEntityOf(SettingsModel.class) -@XmlRootElement(name = SETTINGS + "-" + SETTINGS_BRANDING) +@XmlRootElement(name = BootstrAPI.SETTINGS_BRANDING_LOGIN_PAGE) public class SettingsBrandingLoginPageModel { @XmlElement diff --git a/crowd/src/main/java/com/deftdevs/bootstrapi/crowd/rest/_AllResourceImpl.java b/crowd/src/main/java/com/deftdevs/bootstrapi/crowd/rest/_AllResourceImpl.java index b44f3620..92ee5a64 100644 --- a/crowd/src/main/java/com/deftdevs/bootstrapi/crowd/rest/_AllResourceImpl.java +++ b/crowd/src/main/java/com/deftdevs/bootstrapi/crowd/rest/_AllResourceImpl.java @@ -38,7 +38,8 @@ public _AllResourceImpl( @PUT @Operation( summary = BootstrAPI._ALL_PUT_SUMMARY, - description = BootstrAPI._ALL_PUT_RESPONSE_DESCRIPTION, + description = BootstrAPI._ALL_PUT_RESPONSE_DESCRIPTION + + " Crowd does not support applying a POP mail server configuration.", responses = { @ApiResponse( responseCode = "200", content = @Content(schema = @Schema(implementation = _AllModel.class)), diff --git a/jira/Apis/AllApi.md b/jira/Apis/AllApi.md index 200ebd0b..668f7959 100644 --- a/jira/Apis/AllApi.md +++ b/jira/Apis/AllApi.md @@ -13,7 +13,7 @@ All URIs are relative to *https://JIRA_URL/rest/bootstrapi/1* Apply a complete configuration - Returns the updated configuration. The per-sub-field outcome is reported in the 'status' map, keyed by the request's field paths (e.g. 'settings/general', 'mailServer/smtp'; 2xx for success, 4xx/5xx for failure with a human-readable 'message' and optional 'details'). License keys in the response are redacted (e.g. 'AAAB...wxyz#a1b2'). + Returns the updated configuration. The per-sub-field outcome is reported in the 'status' map, keyed by the request's field paths (e.g. 'settings/general', 'mailServer/smtp'; 2xx for success, 4xx/5xx for failure with a human-readable 'message' and optional 'details'). License keys in the response are redacted (e.g. 'AAAB...wxyz#a1b2'). If all sub-fields fail with the same status code, that code is returned with the same response body. ### Parameters diff --git a/jira/Models/_AllModel.md b/jira/Models/_AllModel.md index 3a2e4dee..82c41779 100644 --- a/jira/Models/_AllModel.md +++ b/jira/Models/_AllModel.md @@ -6,11 +6,11 @@ | **settings** | [**SettingsModel**](SettingsModel.md) | | [optional] [default to null] | | **directories** | [**Map**](AbstractDirectoryModel.md) | | [optional] [default to null] | | **applicationLinks** | [**Map**](ApplicationLinkModel.md) | | [optional] [default to null] | -| **authentication** | [**AuthenticationModel**](AuthenticationModel.md) | | [optional] [default to null] | | **licenses** | [**Map**](LicenseModel.md) | | [optional] [default to null] | | **mailServer** | [**MailServerModel**](MailServerModel.md) | | [optional] [default to null] | -| **permissionsGlobal** | [**PermissionsGlobalModel**](PermissionsGlobalModel.md) | | [optional] [default to null] | | **status** | [**Map**](_AllModelStatus.md) | | [optional] [default to null] | +| **authentication** | [**AuthenticationModel**](AuthenticationModel.md) | | [optional] [default to null] | +| **permissionsGlobal** | [**PermissionsGlobalModel**](PermissionsGlobalModel.md) | | [optional] [default to null] | [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) From e880204c6ab48cd2b4d5181aa2600e45ef6b9be9 Mon Sep 17 00:00:00 2001 From: Patrick Hobusch Date: Mon, 6 Jul 2026 11:41:05 +0200 Subject: [PATCH 15/18] Share the apply-and-record core between the status helpers ServiceResultUtil.setSubEntity and the _AbstractAllServiceImpl helpers duplicated the same null-skip, apply, success-or-error-status body and differed only in how the status key is derived. All of them now delegate to a single key-taking ServiceResultUtil.setEntity core. --- .../service/_AbstractAllServiceImpl.java | 24 ++++--------------- .../commons/util/ServiceResultUtil.java | 19 ++++++++++++++- 2 files changed, 22 insertions(+), 21 deletions(-) diff --git a/commons/src/main/java/com/deftdevs/bootstrapi/commons/service/_AbstractAllServiceImpl.java b/commons/src/main/java/com/deftdevs/bootstrapi/commons/service/_AbstractAllServiceImpl.java index 90467d5b..404f43e5 100644 --- a/commons/src/main/java/com/deftdevs/bootstrapi/commons/service/_AbstractAllServiceImpl.java +++ b/commons/src/main/java/com/deftdevs/bootstrapi/commons/service/_AbstractAllServiceImpl.java @@ -81,18 +81,8 @@ protected void setEntity( final Consumer resultConsumer, final Map statusMap) { - if (input == null) { - return; - } - - final String field = FieldNames.of(allModelClass, fieldType); - try { - final O updatedEntity = updateFunction.apply(input); - resultConsumer.accept(updatedEntity); - statusMap.put(field, _AllModelStatus.success()); - } catch (Exception e) { - statusMap.put(field, ServiceResultUtil.toErrorStatus(field, e)); - } + ServiceResultUtil.setEntity(statusMap, FieldNames.of(allModelClass, fieldType), + input, updateFunction, resultConsumer); } protected void setEntities( @@ -106,14 +96,8 @@ protected void setEntities( return; } - final String field = FieldNames.of(allModelClass, entityType); - try { - final Map updatedEntities = updateFunction.apply(entityMap); - resultConsumer.accept(new LinkedHashMap<>(updatedEntities)); - statusMap.put(field, _AllModelStatus.success()); - } catch (Exception e) { - statusMap.put(field, ServiceResultUtil.toErrorStatus(field, e)); - } + ServiceResultUtil.setEntity(statusMap, FieldNames.of(allModelClass, entityType), + entityMap, entities -> new LinkedHashMap<>(updateFunction.apply(entities)), resultConsumer); } } diff --git a/commons/src/main/java/com/deftdevs/bootstrapi/commons/util/ServiceResultUtil.java b/commons/src/main/java/com/deftdevs/bootstrapi/commons/util/ServiceResultUtil.java index 08e477a3..f5c4f76f 100644 --- a/commons/src/main/java/com/deftdevs/bootstrapi/commons/util/ServiceResultUtil.java +++ b/commons/src/main/java/com/deftdevs/bootstrapi/commons/util/ServiceResultUtil.java @@ -8,6 +8,7 @@ import javax.ws.rs.core.Response; import java.util.Map; import java.util.function.Consumer; +import java.util.function.Function; import java.util.function.UnaryOperator; public class ServiceResultUtil { @@ -36,11 +37,27 @@ public static void setSubEntity( final UnaryOperator updateFunction, final Consumer resultConsumer) { + setEntity(statusMap, FieldNames.of(subEntityClass), input, updateFunction, resultConsumer); + } + + /** + * Applies one field of a composite update and records its outcome under + * the given status key: a success entry when the update function returns, + * or an error entry (via {@link #toErrorStatus(String, Exception)}) when + * it throws. Does nothing when {@code input} is null, i.e. when the field + * was not part of the request. + */ + public static void setEntity( + final Map statusMap, + final String key, + final I input, + final Function updateFunction, + final Consumer resultConsumer) { + if (input == null) { return; } - final String key = FieldNames.of(subEntityClass); try { resultConsumer.accept(updateFunction.apply(input)); statusMap.put(key, _AllModelStatus.success()); From 223bace3d61a7b350a803b6e6ddc37214fb0ae8a Mon Sep 17 00:00:00 2001 From: Patrick Hobusch Date: Mon, 6 Jul 2026 13:28:58 +0200 Subject: [PATCH 16/18] Return the highest sub-field status code for the _all endpoint 207 Multi-Status belongs to the 2xx success class, so scripted callers would treat partial failure as success unless they inspect the body. The overall response code is now 200 only when every sub-field succeeded, otherwise the highest (most severe) sub-field status code. The apply is declarative and not transactional: succeeded sub-fields stay applied, re-submitting the same payload is safe, and the per-sub-field status map still distinguishes partial from total failure. --- .../commons/constants/BootstrAPI.java | 8 ++-- .../rest/_AbstractAllResourceImpl.java | 48 ++++++------------- .../rest/_AbstractAllResourceImplTest.java | 16 +++---- confluence/Apis/AllApi.md | 2 +- .../confluence/rest/_AllResourceImpl.java | 8 +++- .../confluence/rest/_AllResourceTest.java | 8 ++-- crowd/Apis/AllApi.md | 2 +- .../crowd/rest/_AllResourceImpl.java | 8 +++- .../crowd/rest/_AllResourceTest.java | 8 ++-- jira/Apis/AllApi.md | 2 +- .../jira/rest/_AllResourceImpl.java | 8 +++- .../jira/rest/_AllResourceTest.java | 8 ++-- 12 files changed, 60 insertions(+), 66 deletions(-) diff --git a/commons/src/main/java/com/deftdevs/bootstrapi/commons/constants/BootstrAPI.java b/commons/src/main/java/com/deftdevs/bootstrapi/commons/constants/BootstrAPI.java index e4a07202..e6356b13 100644 --- a/commons/src/main/java/com/deftdevs/bootstrapi/commons/constants/BootstrAPI.java +++ b/commons/src/main/java/com/deftdevs/bootstrapi/commons/constants/BootstrAPI.java @@ -65,9 +65,11 @@ public class BootstrAPI { + " 'status' map, keyed by the request's field paths (e.g. 'settings/general', 'mailServer/smtp';" + " 2xx for success, 4xx/5xx for failure with a human-readable 'message' and optional 'details')." + " License keys in the response are redacted (e.g. 'AAAB...wxyz#a1b2')." - + " If all sub-fields fail with the same status code, that code is returned with the same response body."; - public static final String _ALL_PUT_PARTIAL_RESPONSE_DESCRIPTION = "Sub-field outcomes differ. Inspect the per-sub-field 'status' map" - + " in the response body to see which fields succeeded and which failed."; + + " If any sub-field fails, the highest sub-field status code is returned with the same response body."; + public static final String _ALL_PUT_FAILURE_RESPONSE_DESCRIPTION = "One or more sub-fields failed to apply. The response code is the highest" + + " sub-field status code; inspect the per-sub-field 'status' map in the response body to see which" + + " fields succeeded and which failed. Request-level errors (e.g. a missing request body) return an" + + " error message list instead."; public static final String SETTINGS_GENERAL_GET_SUMMARY = "Get the general settings"; public static final String SETTINGS_GENERAL_GET_RESPONSE_DESCRIPTION = "Returns the general settings"; public static final String SETTINGS_GENERAL_PUT_SUMMARY = "Set the general settings"; diff --git a/commons/src/main/java/com/deftdevs/bootstrapi/commons/rest/_AbstractAllResourceImpl.java b/commons/src/main/java/com/deftdevs/bootstrapi/commons/rest/_AbstractAllResourceImpl.java index e117d1b9..6fba678b 100644 --- a/commons/src/main/java/com/deftdevs/bootstrapi/commons/rest/_AbstractAllResourceImpl.java +++ b/commons/src/main/java/com/deftdevs/bootstrapi/commons/rest/_AbstractAllResourceImpl.java @@ -12,8 +12,6 @@ public abstract class _AbstractAllResourceImpl<_AllModel extends _AllModelAccessor> implements _AllResource<_AllModel> { - static final int MULTI_STATUS = 207; - private final _AllService<_AllModel> allService; public _AbstractAllResourceImpl( @@ -37,44 +35,26 @@ public Response setAll( /** * Aggregates per-sub-field statuses into a single HTTP response code: - *

    - *
  • Empty status map → 200 OK.
  • - *
  • All entries share the same status code → that code - * (all succeeded → 200, all failed alike → that 4xx/5xx).
  • - *
  • Any mix of different codes → 207 Multi-Status.
  • - *
- * 207 signals "outcomes differ" — callers must inspect the per-field - * {@code status} map in the response body to see which fields succeeded - * and which failed. + * 200 when the status map is empty or every sub-field succeeded, + * otherwise the highest (most severe) sub-field status code. *

- * On RFC conformance: 207 Multi-Status is defined by RFC 4918 (WebDAV), - * where it implies an XML {@code multistatus} body and is likewise used - * whenever the parts of an operation end differently, whatever their - * status classes. RFC 9110 permits any registered status code over HTTP, - * and using 207 with a JSON body carrying the per-part statuses is - * established practice for bulk-style REST endpoints. A plain 200 would - * hide partial failure from clients that only check the status code, and - * a top-level 4xx/5xx would misrepresent the sub-fields that were applied - * successfully (this endpoint is not transactional). Note that 207 - * belongs to the 2xx success class, so scripted callers (e.g. - * {@code curl -f}) treat partial success as success unless they inspect - * the body. + * Failing loud is preferred over WebDAV-style 207 Multi-Status because + * 207 belongs to the 2xx success class, so scripted callers (e.g. + * {@code curl -f}) would treat partial failure as success unless they + * inspect the body. The apply is declarative and not transactional: + * sub-fields that succeeded stay applied, re-submitting the same payload + * is safe, and the per-sub-field {@code status} map in the response body + * still distinguishes partial from total failure. */ static int computeOverallStatus( final Map statusMap) { - if (statusMap == null || statusMap.isEmpty()) { - return Response.Status.OK.getStatusCode(); - } - - Integer unanimousCode = null; - for (final _AllModelStatus entry : statusMap.values()) { - if (unanimousCode == null) { - unanimousCode = entry.getStatus(); - } else if (unanimousCode != entry.getStatus()) { - return MULTI_STATUS; + int overallStatus = Response.Status.OK.getStatusCode(); + if (statusMap != null) { + for (final _AllModelStatus entry : statusMap.values()) { + overallStatus = Math.max(overallStatus, entry.getStatus()); } } - return unanimousCode; + return overallStatus; } } diff --git a/commons/src/test/java/com/deftdevs/bootstrapi/commons/rest/_AbstractAllResourceImplTest.java b/commons/src/test/java/com/deftdevs/bootstrapi/commons/rest/_AbstractAllResourceImplTest.java index 8adea3a7..416cef67 100644 --- a/commons/src/test/java/com/deftdevs/bootstrapi/commons/rest/_AbstractAllResourceImplTest.java +++ b/commons/src/test/java/com/deftdevs/bootstrapi/commons/rest/_AbstractAllResourceImplTest.java @@ -31,12 +31,12 @@ void allSuccessYieldsOk() { } @Test - void mixedOutcomesIncludingServerErrorYield207() { + void mixedOutcomesYieldTheHighestCode() { final Map map = new LinkedHashMap<>(); map.put("a", _AllModelStatus.success()); map.put("b", _AllModelStatus.error(Response.Status.BAD_REQUEST, "x", null)); map.put("c", _AllModelStatus.error(Response.Status.INTERNAL_SERVER_ERROR, "x", null)); - assertEquals(_AbstractAllResourceImpl.MULTI_STATUS, _AbstractAllResourceImpl.computeOverallStatus(map)); + assertEquals(500, _AbstractAllResourceImpl.computeOverallStatus(map)); } @Test @@ -48,19 +48,19 @@ void allSameServerErrorBubblesThat() { } @Test - void mixedSuccessAndServerErrorYields207() { + void mixedSuccessAndServerErrorYields500() { final Map map = new LinkedHashMap<>(); map.put("a", _AllModelStatus.success()); map.put("b", _AllModelStatus.error(Response.Status.INTERNAL_SERVER_ERROR, "x", null)); - assertEquals(_AbstractAllResourceImpl.MULTI_STATUS, _AbstractAllResourceImpl.computeOverallStatus(map)); + assertEquals(500, _AbstractAllResourceImpl.computeOverallStatus(map)); } @Test - void mixedSuccessAndClientErrorYields207() { + void mixedSuccessAndClientErrorYields400() { final Map map = new LinkedHashMap<>(); map.put("a", _AllModelStatus.success()); map.put("b", _AllModelStatus.error(Response.Status.BAD_REQUEST, "x", null)); - assertEquals(_AbstractAllResourceImpl.MULTI_STATUS, _AbstractAllResourceImpl.computeOverallStatus(map)); + assertEquals(400, _AbstractAllResourceImpl.computeOverallStatus(map)); } @Test @@ -72,10 +72,10 @@ void allSameClientErrorBubblesThat() { } @Test - void differentClientErrorsYield207() { + void differentClientErrorsYieldTheHighestCode() { final Map map = new LinkedHashMap<>(); map.put("a", _AllModelStatus.error(Response.Status.BAD_REQUEST, "x", null)); map.put("b", _AllModelStatus.error(Response.Status.CONFLICT, "y", null)); - assertEquals(_AbstractAllResourceImpl.MULTI_STATUS, _AbstractAllResourceImpl.computeOverallStatus(map)); + assertEquals(409, _AbstractAllResourceImpl.computeOverallStatus(map)); } } diff --git a/confluence/Apis/AllApi.md b/confluence/Apis/AllApi.md index 45bb7cb3..8e44878b 100644 --- a/confluence/Apis/AllApi.md +++ b/confluence/Apis/AllApi.md @@ -13,7 +13,7 @@ All URIs are relative to *https://CONFLUENCE_URL/rest/bootstrapi/1* Apply a complete configuration - Returns the updated configuration. The per-sub-field outcome is reported in the 'status' map, keyed by the request's field paths (e.g. 'settings/general', 'mailServer/smtp'; 2xx for success, 4xx/5xx for failure with a human-readable 'message' and optional 'details'). License keys in the response are redacted (e.g. 'AAAB...wxyz#a1b2'). If all sub-fields fail with the same status code, that code is returned with the same response body. + Returns the updated configuration. The per-sub-field outcome is reported in the 'status' map, keyed by the request's field paths (e.g. 'settings/general', 'mailServer/smtp'; 2xx for success, 4xx/5xx for failure with a human-readable 'message' and optional 'details'). License keys in the response are redacted (e.g. 'AAAB...wxyz#a1b2'). If any sub-field fails, the highest sub-field status code is returned with the same response body. ### Parameters diff --git a/confluence/src/main/java/com/deftdevs/bootstrapi/confluence/rest/_AllResourceImpl.java b/confluence/src/main/java/com/deftdevs/bootstrapi/confluence/rest/_AllResourceImpl.java index 93dbf4a5..73aa980a 100644 --- a/confluence/src/main/java/com/deftdevs/bootstrapi/confluence/rest/_AllResourceImpl.java +++ b/confluence/src/main/java/com/deftdevs/bootstrapi/confluence/rest/_AllResourceImpl.java @@ -45,8 +45,12 @@ public _AllResourceImpl( description = BootstrAPI._ALL_PUT_RESPONSE_DESCRIPTION ), @ApiResponse( - responseCode = "207", content = @Content(schema = @Schema(implementation = _AllModel.class)), - description = BootstrAPI._ALL_PUT_PARTIAL_RESPONSE_DESCRIPTION + responseCode = "4XX", content = @Content(schema = @Schema(implementation = _AllModel.class)), + description = BootstrAPI._ALL_PUT_FAILURE_RESPONSE_DESCRIPTION + ), + @ApiResponse( + responseCode = "5XX", content = @Content(schema = @Schema(implementation = _AllModel.class)), + description = BootstrAPI._ALL_PUT_FAILURE_RESPONSE_DESCRIPTION ), @ApiResponse( responseCode = "default", content = @Content(schema = @Schema(implementation = ErrorCollection.class)), diff --git a/confluence/src/test/java/com/deftdevs/bootstrapi/confluence/rest/_AllResourceTest.java b/confluence/src/test/java/com/deftdevs/bootstrapi/confluence/rest/_AllResourceTest.java index 10077d8b..0e533a6e 100644 --- a/confluence/src/test/java/com/deftdevs/bootstrapi/confluence/rest/_AllResourceTest.java +++ b/confluence/src/test/java/com/deftdevs/bootstrapi/confluence/rest/_AllResourceTest.java @@ -62,7 +62,7 @@ public void testSetAll() { } @Test - public void testSetAllReturns207OnMixedSuccessAndServerError() { + public void testSetAllReturns500OnMixedSuccessAndServerError() { final _AllModel result = new _AllModel(); final Map status = new HashMap<>(); status.put(FieldNames.pathOf(_AllModel.class, SettingsGeneralModel.class), _AllModelStatus.success()); @@ -72,11 +72,11 @@ public void testSetAllReturns207OnMixedSuccessAndServerError() { doReturn(result).when(allService).setAll(any()); final Response response = allResource.setAll(allModel); - assertEquals(207, response.getStatus()); + assertEquals(500, response.getStatus()); } @Test - public void testSetAllReturns207OnMixedSuccessAndClientError() { + public void testSetAllReturns400OnMixedSuccessAndClientError() { final _AllModel result = new _AllModel(); final Map status = new HashMap<>(); status.put(FieldNames.pathOf(_AllModel.class, SettingsGeneralModel.class), _AllModelStatus.success()); @@ -86,6 +86,6 @@ public void testSetAllReturns207OnMixedSuccessAndClientError() { doReturn(result).when(allService).setAll(any()); final Response response = allResource.setAll(allModel); - assertEquals(207, response.getStatus()); + assertEquals(400, response.getStatus()); } } diff --git a/crowd/Apis/AllApi.md b/crowd/Apis/AllApi.md index e0c0501a..8d1c4ae0 100644 --- a/crowd/Apis/AllApi.md +++ b/crowd/Apis/AllApi.md @@ -13,7 +13,7 @@ All URIs are relative to *https://CROWD_URL/rest/bootstrapi/1* Apply a complete configuration - Returns the updated configuration. The per-sub-field outcome is reported in the 'status' map, keyed by the request's field paths (e.g. 'settings/general', 'mailServer/smtp'; 2xx for success, 4xx/5xx for failure with a human-readable 'message' and optional 'details'). License keys in the response are redacted (e.g. 'AAAB...wxyz#a1b2'). If all sub-fields fail with the same status code, that code is returned with the same response body. Crowd does not support applying a POP mail server configuration. + Returns the updated configuration. The per-sub-field outcome is reported in the 'status' map, keyed by the request's field paths (e.g. 'settings/general', 'mailServer/smtp'; 2xx for success, 4xx/5xx for failure with a human-readable 'message' and optional 'details'). License keys in the response are redacted (e.g. 'AAAB...wxyz#a1b2'). If any sub-field fails, the highest sub-field status code is returned with the same response body. Crowd does not support applying a POP mail server configuration. ### Parameters diff --git a/crowd/src/main/java/com/deftdevs/bootstrapi/crowd/rest/_AllResourceImpl.java b/crowd/src/main/java/com/deftdevs/bootstrapi/crowd/rest/_AllResourceImpl.java index 92ee5a64..202b70bb 100644 --- a/crowd/src/main/java/com/deftdevs/bootstrapi/crowd/rest/_AllResourceImpl.java +++ b/crowd/src/main/java/com/deftdevs/bootstrapi/crowd/rest/_AllResourceImpl.java @@ -46,8 +46,12 @@ public _AllResourceImpl( description = BootstrAPI._ALL_PUT_RESPONSE_DESCRIPTION ), @ApiResponse( - responseCode = "207", content = @Content(schema = @Schema(implementation = _AllModel.class)), - description = BootstrAPI._ALL_PUT_PARTIAL_RESPONSE_DESCRIPTION + responseCode = "4XX", content = @Content(schema = @Schema(implementation = _AllModel.class)), + description = BootstrAPI._ALL_PUT_FAILURE_RESPONSE_DESCRIPTION + ), + @ApiResponse( + responseCode = "5XX", content = @Content(schema = @Schema(implementation = _AllModel.class)), + description = BootstrAPI._ALL_PUT_FAILURE_RESPONSE_DESCRIPTION ), @ApiResponse( responseCode = "default", content = @Content(schema = @Schema(implementation = ErrorCollection.class)), diff --git a/crowd/src/test/java/com/deftdevs/bootstrapi/crowd/rest/_AllResourceTest.java b/crowd/src/test/java/com/deftdevs/bootstrapi/crowd/rest/_AllResourceTest.java index 2fa44e93..4f07481e 100644 --- a/crowd/src/test/java/com/deftdevs/bootstrapi/crowd/rest/_AllResourceTest.java +++ b/crowd/src/test/java/com/deftdevs/bootstrapi/crowd/rest/_AllResourceTest.java @@ -67,7 +67,7 @@ public void testSetAll() { } @Test - public void testSetAllReturns207OnMixedSuccessAndServerError() { + public void testSetAllReturns500OnMixedSuccessAndServerError() { final _AllModel result = new _AllModel(); final Map status = new HashMap<>(); status.put(FieldNames.pathOf(_AllModel.class, SettingsGeneralModel.class), _AllModelStatus.success()); @@ -77,11 +77,11 @@ public void testSetAllReturns207OnMixedSuccessAndServerError() { doReturn(result).when(allService).setAll(any()); final Response response = allResource.setAll(allModel); - assertEquals(207, response.getStatus()); + assertEquals(500, response.getStatus()); } @Test - public void testSetAllReturns207OnMixedSuccessAndClientError() { + public void testSetAllReturns400OnMixedSuccessAndClientError() { final _AllModel result = new _AllModel(); final Map status = new HashMap<>(); status.put(FieldNames.pathOf(_AllModel.class, SettingsGeneralModel.class), _AllModelStatus.success()); @@ -91,6 +91,6 @@ public void testSetAllReturns207OnMixedSuccessAndClientError() { doReturn(result).when(allService).setAll(any()); final Response response = allResource.setAll(allModel); - assertEquals(207, response.getStatus()); + assertEquals(400, response.getStatus()); } } diff --git a/jira/Apis/AllApi.md b/jira/Apis/AllApi.md index 668f7959..c636e98c 100644 --- a/jira/Apis/AllApi.md +++ b/jira/Apis/AllApi.md @@ -13,7 +13,7 @@ All URIs are relative to *https://JIRA_URL/rest/bootstrapi/1* Apply a complete configuration - Returns the updated configuration. The per-sub-field outcome is reported in the 'status' map, keyed by the request's field paths (e.g. 'settings/general', 'mailServer/smtp'; 2xx for success, 4xx/5xx for failure with a human-readable 'message' and optional 'details'). License keys in the response are redacted (e.g. 'AAAB...wxyz#a1b2'). If all sub-fields fail with the same status code, that code is returned with the same response body. + Returns the updated configuration. The per-sub-field outcome is reported in the 'status' map, keyed by the request's field paths (e.g. 'settings/general', 'mailServer/smtp'; 2xx for success, 4xx/5xx for failure with a human-readable 'message' and optional 'details'). License keys in the response are redacted (e.g. 'AAAB...wxyz#a1b2'). If any sub-field fails, the highest sub-field status code is returned with the same response body. ### Parameters diff --git a/jira/src/main/java/com/deftdevs/bootstrapi/jira/rest/_AllResourceImpl.java b/jira/src/main/java/com/deftdevs/bootstrapi/jira/rest/_AllResourceImpl.java index b5ffe089..ef4baba6 100644 --- a/jira/src/main/java/com/deftdevs/bootstrapi/jira/rest/_AllResourceImpl.java +++ b/jira/src/main/java/com/deftdevs/bootstrapi/jira/rest/_AllResourceImpl.java @@ -45,8 +45,12 @@ public _AllResourceImpl( description = BootstrAPI._ALL_PUT_RESPONSE_DESCRIPTION ), @ApiResponse( - responseCode = "207", content = @Content(schema = @Schema(implementation = _AllModel.class)), - description = BootstrAPI._ALL_PUT_PARTIAL_RESPONSE_DESCRIPTION + responseCode = "4XX", content = @Content(schema = @Schema(implementation = _AllModel.class)), + description = BootstrAPI._ALL_PUT_FAILURE_RESPONSE_DESCRIPTION + ), + @ApiResponse( + responseCode = "5XX", content = @Content(schema = @Schema(implementation = _AllModel.class)), + description = BootstrAPI._ALL_PUT_FAILURE_RESPONSE_DESCRIPTION ), @ApiResponse( responseCode = "default", content = @Content(schema = @Schema(implementation = ErrorCollection.class)), diff --git a/jira/src/test/java/com/deftdevs/bootstrapi/jira/rest/_AllResourceTest.java b/jira/src/test/java/com/deftdevs/bootstrapi/jira/rest/_AllResourceTest.java index 00648efb..ac01d1f8 100644 --- a/jira/src/test/java/com/deftdevs/bootstrapi/jira/rest/_AllResourceTest.java +++ b/jira/src/test/java/com/deftdevs/bootstrapi/jira/rest/_AllResourceTest.java @@ -62,7 +62,7 @@ public void testSetAll() { } @Test - public void testSetAllReturns207OnMixedSuccessAndServerError() { + public void testSetAllReturns500OnMixedSuccessAndServerError() { final _AllModel result = new _AllModel(); final Map status = new HashMap<>(); status.put(FieldNames.pathOf(_AllModel.class, SettingsGeneralModel.class), _AllModelStatus.success()); @@ -72,11 +72,11 @@ public void testSetAllReturns207OnMixedSuccessAndServerError() { doReturn(result).when(allService).setAll(any()); final Response response = allResource.setAll(allModel); - assertEquals(207, response.getStatus()); + assertEquals(500, response.getStatus()); } @Test - public void testSetAllReturns207OnMixedSuccessAndClientError() { + public void testSetAllReturns400OnMixedSuccessAndClientError() { final _AllModel result = new _AllModel(); final Map status = new HashMap<>(); status.put(FieldNames.pathOf(_AllModel.class, SettingsGeneralModel.class), _AllModelStatus.success()); @@ -86,6 +86,6 @@ public void testSetAllReturns207OnMixedSuccessAndClientError() { doReturn(result).when(allService).setAll(any()); final Response response = allResource.setAll(allModel); - assertEquals(207, response.getStatus()); + assertEquals(400, response.getStatus()); } } From 6f227d7fc1d3974fd3a249bb42b3c5cfeae38d81 Mon Sep 17 00:00:00 2001 From: Patrick Hobusch Date: Mon, 6 Jul 2026 13:28:58 +0200 Subject: [PATCH 17/18] Derive the trusted-proxies status key from a getter reference Looking the field up by type coupled the key to 'the only List field of the model' and would break as soon as another such field is added. A getter reference (_AllModel::getTrustedProxies) assigned to a SerializableFunction carries its method metadata in the compiler's SerializedLambda, from which FieldNames now derives the backing field's serialized name - still fully programmatic, and refactoring-safe. --- .../model/type/SerializableFunction.java | 14 ++++ .../service/_AbstractAllServiceImpl.java | 17 +++++ .../bootstrapi/commons/util/FieldNames.java | 68 +++++++++++++++++++ .../commons/util/FieldNamesTest.java | 28 ++++++++ .../crowd/service/_AllServiceImpl.java | 2 +- 5 files changed, 128 insertions(+), 1 deletion(-) create mode 100644 commons/src/main/java/com/deftdevs/bootstrapi/commons/model/type/SerializableFunction.java diff --git a/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/type/SerializableFunction.java b/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/type/SerializableFunction.java new file mode 100644 index 00000000..c22b443b --- /dev/null +++ b/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/type/SerializableFunction.java @@ -0,0 +1,14 @@ +package com.deftdevs.bootstrapi.commons.model.type; + +import java.io.Serializable; +import java.util.function.Function; + +/** + * A {@link Function} that is also {@link Serializable}. Getter references + * ({@code Model::getField}) assigned to this type retain their method + * metadata at runtime, which {@link com.deftdevs.bootstrapi.commons.util.FieldNames} + * uses to derive the referenced field's serialized name. + */ +@FunctionalInterface +public interface SerializableFunction extends Function, Serializable { +} diff --git a/commons/src/main/java/com/deftdevs/bootstrapi/commons/service/_AbstractAllServiceImpl.java b/commons/src/main/java/com/deftdevs/bootstrapi/commons/service/_AbstractAllServiceImpl.java index 404f43e5..f04ddf31 100644 --- a/commons/src/main/java/com/deftdevs/bootstrapi/commons/service/_AbstractAllServiceImpl.java +++ b/commons/src/main/java/com/deftdevs/bootstrapi/commons/service/_AbstractAllServiceImpl.java @@ -1,5 +1,6 @@ package com.deftdevs.bootstrapi.commons.service; +import com.deftdevs.bootstrapi.commons.model.type.SerializableFunction; import com.deftdevs.bootstrapi.commons.model.type.ServiceResult; import com.deftdevs.bootstrapi.commons.model.type._AllModelAccessor; import com.deftdevs.bootstrapi.commons.model.type._AllModelStatus; @@ -85,6 +86,22 @@ protected void setEntity( input, updateFunction, resultConsumer); } + /** + * Variant for fields whose type is not a model class (e.g. a list of + * strings): the status key and the input are derived from the getter + * reference itself instead of a type-based field lookup. + */ + protected void setEntity( + final SerializableFunction<_AllModel, I> getter, + final _AllModel allModel, + final Function updateFunction, + final Consumer resultConsumer, + final Map statusMap) { + + ServiceResultUtil.setEntity(statusMap, FieldNames.of(getter), + getter.apply(allModel), updateFunction, resultConsumer); + } + protected void setEntities( final Class entityType, final Map entityMap, diff --git a/commons/src/main/java/com/deftdevs/bootstrapi/commons/util/FieldNames.java b/commons/src/main/java/com/deftdevs/bootstrapi/commons/util/FieldNames.java index ce1dba8d..b195ceec 100644 --- a/commons/src/main/java/com/deftdevs/bootstrapi/commons/util/FieldNames.java +++ b/commons/src/main/java/com/deftdevs/bootstrapi/commons/util/FieldNames.java @@ -1,9 +1,12 @@ package com.deftdevs.bootstrapi.commons.util; +import com.deftdevs.bootstrapi.commons.model.type.SerializableFunction; import com.deftdevs.bootstrapi.commons.model.type.SubEntityOf; import javax.xml.bind.annotation.XmlElement; +import java.lang.invoke.SerializedLambda; import java.lang.reflect.Field; +import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; @@ -66,6 +69,71 @@ public static String of(final Class modelClass, final Class fieldType) { * {@code SettingsGeneralModel} → {@code settings/general}, + * {@code MailServerModel} → {@code mailServer}). */ + /** + * Returns the serialized name of the field read by the given getter + * reference (e.g. {@code _AllModel::getTrustedProxies} → + * {@code trustedProxies}), honoring an explicit + * {@code @XmlElement(name = ...)} override. Use this for fields whose + * type is not a model class (e.g. a list of strings), where a type-based + * lookup would couple the name to "the only field of that type" and + * break as soon as another field of the same type is added. + */ + public static String of(final SerializableFunction getter) { + final SerializedLambda lambda = serializedLambda(getter); + final String className = lambda.getImplClass().replace('/', '.'); + return CACHE.computeIfAbsent(className + "::" + lambda.getImplMethodName(), + key -> resolveGetter(getter, lambda, className)); + } + + private static String resolveGetter(final SerializableFunction getter, + final SerializedLambda lambda, final String className) { + + final String methodName = lambda.getImplMethodName(); + final String fieldName; + if (methodName.startsWith("get") && methodName.length() > 3) { + fieldName = decapitalize(methodName.substring(3)); + } else if (methodName.startsWith("is") && methodName.length() > 2) { + fieldName = decapitalize(methodName.substring(2)); + } else { + throw new IllegalArgumentException( + "Not a getter reference: " + className + "::" + methodName); + } + + final Class modelClass; + try { + modelClass = Class.forName(className, false, getter.getClass().getClassLoader()); + } catch (ClassNotFoundException e) { + throw new IllegalArgumentException("Cannot load the getter's declaring class", e); + } + + for (final Field field : declaredAndInheritedFields(modelClass)) { + if (field.getName().equals(fieldName)) { + return serializedName(field); + } + } + throw new IllegalArgumentException(String.format( + "%s has no field backing %s()", className, methodName)); + } + + /** + * A getter reference assigned to a {@link SerializableFunction} carries + * its method metadata in the {@link SerializedLambda} returned by the + * compiler-generated {@code writeReplace} method. + */ + private static SerializedLambda serializedLambda(final SerializableFunction getter) { + try { + final Method writeReplace = getter.getClass().getDeclaredMethod("writeReplace"); + writeReplace.setAccessible(true); + return (SerializedLambda) writeReplace.invoke(getter); + } catch (ReflectiveOperationException e) { + throw new IllegalArgumentException("Cannot introspect the getter reference", e); + } + } + + private static String decapitalize(final String name) { + return Character.toLowerCase(name.charAt(0)) + name.substring(1); + } + public static String pathOf(final Class rootClass, final Class leafType) { return CACHE.computeIfAbsent(rootClass.getName() + "->" + leafType.getName(), key -> resolvePath(rootClass, leafType)); diff --git a/commons/src/test/java/com/deftdevs/bootstrapi/commons/util/FieldNamesTest.java b/commons/src/test/java/com/deftdevs/bootstrapi/commons/util/FieldNamesTest.java index 47b3953e..38593edf 100644 --- a/commons/src/test/java/com/deftdevs/bootstrapi/commons/util/FieldNamesTest.java +++ b/commons/src/test/java/com/deftdevs/bootstrapi/commons/util/FieldNamesTest.java @@ -32,6 +32,18 @@ private static class RootModel { @XmlElement(name = "renamed") private ThirdLeafModel other; + + public List getNames() { + return names; + } + + public ThirdLeafModel getOther() { + return other; + } + + public RootModel getSelf() { + return this; + } } private static class AmbiguousModel { @@ -71,6 +83,22 @@ void ofRejectsUnknownFieldType() { () -> FieldNames.of(GroupModel.class, RootModel.class)); } + @Test + void ofGetterResolvesTheBackingField() { + assertEquals("names", FieldNames.of(RootModel::getNames)); + } + + @Test + void ofGetterHonorsXmlElementName() { + assertEquals("renamed", FieldNames.of(RootModel::getOther)); + } + + @Test + void ofGetterRejectsMethodsWithoutBackingField() { + assertThrows(IllegalArgumentException.class, + () -> FieldNames.of(RootModel::getSelf)); + } + @Test void pathOfResolvesTopLevelField() { assertEquals("group", FieldNames.pathOf(RootModel.class, GroupModel.class)); diff --git a/crowd/src/main/java/com/deftdevs/bootstrapi/crowd/service/_AllServiceImpl.java b/crowd/src/main/java/com/deftdevs/bootstrapi/crowd/service/_AllServiceImpl.java index 28613657..746c31a9 100644 --- a/crowd/src/main/java/com/deftdevs/bootstrapi/crowd/service/_AllServiceImpl.java +++ b/crowd/src/main/java/com/deftdevs/bootstrapi/crowd/service/_AllServiceImpl.java @@ -90,7 +90,7 @@ public _AllModel setAll( setEntity(SessionConfigModel.class, allModel.getSessionConfig(), sessionConfigService::setSessionConfig, result::setSessionConfig, statusMap); - setEntity(String.class, allModel.getTrustedProxies(), + setEntity(_AllModel::getTrustedProxies, allModel, trustedProxiesService::setTrustedProxies, result::setTrustedProxies, statusMap); result.setStatus(statusMap); From 5957605c6d8c9e71d36b12dabe74f1973403ce68 Mon Sep 17 00:00:00 2001 From: Patrick Hobusch Date: Mon, 6 Jul 2026 13:28:58 +0200 Subject: [PATCH 18/18] Split the settings base so every product model extends it A single base could not fit all products: Jira has no branding, Crowd has neither security nor color-scheme branding and names its login-page branding field the same. The base is now layered by support - AbstractSettingsModel (general) is extended by AbstractSettingsSecurityModel (security), which is extended by AbstractSettingsBrandingModel (color-scheme branding). Confluence extends the branding level, Jira the security level and Crowd the base, so each @SubEntityOf anchor points at the class that declares the field and no product model aligns by convention or advertises sub-fields it cannot apply. --- .../model/AbstractSettingsBrandingModel.java | 24 +++++++++++++++++++ .../commons/model/AbstractSettingsModel.java | 16 ++++--------- .../model/AbstractSettingsSecurityModel.java | 23 ++++++++++++++++++ .../SettingsBrandingColorSchemeModel.java | 2 +- .../commons/model/SettingsSecurityModel.java | 2 +- .../confluence/model/SettingsModel.java | 4 ++-- .../bootstrapi/crowd/model/SettingsModel.java | 15 +++++------- .../service/api/CrowdSettingsService.java | 5 +++- .../service/CrowdSettingsServiceTest.java | 14 ++++++----- .../bootstrapi/jira/model/SettingsModel.java | 19 +++++---------- .../jira/service/api/JiraSettingsService.java | 6 ++++- .../jira/service/JiraSettingsServiceTest.java | 17 ++++++------- 12 files changed, 94 insertions(+), 53 deletions(-) create mode 100644 commons/src/main/java/com/deftdevs/bootstrapi/commons/model/AbstractSettingsBrandingModel.java create mode 100644 commons/src/main/java/com/deftdevs/bootstrapi/commons/model/AbstractSettingsSecurityModel.java diff --git a/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/AbstractSettingsBrandingModel.java b/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/AbstractSettingsBrandingModel.java new file mode 100644 index 00000000..c8f3800e --- /dev/null +++ b/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/AbstractSettingsBrandingModel.java @@ -0,0 +1,24 @@ +package com.deftdevs.bootstrapi.commons.model; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +import javax.xml.bind.annotation.XmlElement; + +/** + * Settings group schema for products that support color-scheme branding + * (only Confluence; Crowd brands its login page with a product-specific + * model instead). + */ +@Data +@SuperBuilder +@NoArgsConstructor +@EqualsAndHashCode(callSuper = true) +public abstract class AbstractSettingsBrandingModel extends AbstractSettingsSecurityModel { + + @XmlElement + private SettingsBrandingColorSchemeModel branding; + +} diff --git a/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/AbstractSettingsModel.java b/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/AbstractSettingsModel.java index ee2b478e..bc46ae55 100644 --- a/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/AbstractSettingsModel.java +++ b/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/AbstractSettingsModel.java @@ -7,11 +7,11 @@ import javax.xml.bind.annotation.XmlElement; /** - * Canonical settings group schema. Products extend it with the sub-fields - * they support; sub-models declare their position in the group via - * {@code @SubEntityOf(AbstractSettingsModel.class)} even when a product's - * concrete settings model does not extend this class, since the field names - * are aligned by construction. + * Canonical settings group schema shared by all products. Extended by + * {@link AbstractSettingsSecurityModel} and {@link AbstractSettingsBrandingModel} + * for the sub-fields only some products support, so that every product's + * concrete settings model inherits its fields from the class its sub-models + * name via {@code @SubEntityOf} — field names then match by construction. */ @Data @SuperBuilder @@ -21,10 +21,4 @@ public abstract class AbstractSettingsModel { @XmlElement private SettingsGeneralModel general; - @XmlElement - private SettingsSecurityModel security; - - @XmlElement - private SettingsBrandingColorSchemeModel branding; - } diff --git a/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/AbstractSettingsSecurityModel.java b/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/AbstractSettingsSecurityModel.java new file mode 100644 index 00000000..8722b480 --- /dev/null +++ b/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/AbstractSettingsSecurityModel.java @@ -0,0 +1,23 @@ +package com.deftdevs.bootstrapi.commons.model; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +import javax.xml.bind.annotation.XmlElement; + +/** + * Settings group schema for products that support security settings + * (Confluence and Jira; Crowd does not). + */ +@Data +@SuperBuilder +@NoArgsConstructor +@EqualsAndHashCode(callSuper = true) +public abstract class AbstractSettingsSecurityModel extends AbstractSettingsModel { + + @XmlElement + private SettingsSecurityModel security; + +} diff --git a/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/SettingsBrandingColorSchemeModel.java b/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/SettingsBrandingColorSchemeModel.java index 344719b8..115f2489 100644 --- a/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/SettingsBrandingColorSchemeModel.java +++ b/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/SettingsBrandingColorSchemeModel.java @@ -17,7 +17,7 @@ @Builder @NoArgsConstructor @AllArgsConstructor -@SubEntityOf(AbstractSettingsModel.class) +@SubEntityOf(AbstractSettingsBrandingModel.class) @XmlRootElement(name = SETTINGS + "-" + SETTINGS_BRANDING + "-" + COLOR_SCHEME) @XmlAccessorType(XmlAccessType.FIELD) public class SettingsBrandingColorSchemeModel { diff --git a/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/SettingsSecurityModel.java b/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/SettingsSecurityModel.java index bdc08518..14924918 100644 --- a/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/SettingsSecurityModel.java +++ b/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/SettingsSecurityModel.java @@ -18,7 +18,7 @@ @SuperBuilder @NoArgsConstructor @AllArgsConstructor -@SubEntityOf(AbstractSettingsModel.class) +@SubEntityOf(AbstractSettingsSecurityModel.class) @XmlRootElement(name = SETTINGS + "-" + SETTINGS_SECURITY) @XmlAccessorType(XmlAccessType.FIELD) public class SettingsSecurityModel { diff --git a/confluence/src/main/java/com/deftdevs/bootstrapi/confluence/model/SettingsModel.java b/confluence/src/main/java/com/deftdevs/bootstrapi/confluence/model/SettingsModel.java index ab097737..335c386a 100644 --- a/confluence/src/main/java/com/deftdevs/bootstrapi/confluence/model/SettingsModel.java +++ b/confluence/src/main/java/com/deftdevs/bootstrapi/confluence/model/SettingsModel.java @@ -1,6 +1,6 @@ package com.deftdevs.bootstrapi.confluence.model; -import com.deftdevs.bootstrapi.commons.model.AbstractSettingsModel; +import com.deftdevs.bootstrapi.commons.model.AbstractSettingsBrandingModel; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; @@ -16,7 +16,7 @@ @NoArgsConstructor @EqualsAndHashCode(callSuper = true) @XmlRootElement(name = SETTINGS) -public class SettingsModel extends AbstractSettingsModel { +public class SettingsModel extends AbstractSettingsBrandingModel { @XmlElement private SettingsCustomHtmlModel customHtml; diff --git a/crowd/src/main/java/com/deftdevs/bootstrapi/crowd/model/SettingsModel.java b/crowd/src/main/java/com/deftdevs/bootstrapi/crowd/model/SettingsModel.java index 89173705..15fbc990 100644 --- a/crowd/src/main/java/com/deftdevs/bootstrapi/crowd/model/SettingsModel.java +++ b/crowd/src/main/java/com/deftdevs/bootstrapi/crowd/model/SettingsModel.java @@ -1,10 +1,10 @@ package com.deftdevs.bootstrapi.crowd.model; -import com.deftdevs.bootstrapi.commons.model.SettingsGeneralModel; -import lombok.AllArgsConstructor; -import lombok.Builder; +import com.deftdevs.bootstrapi.commons.model.AbstractSettingsModel; import lombok.Data; +import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; import javax.xml.bind.annotation.XmlElement; import javax.xml.bind.annotation.XmlRootElement; @@ -12,14 +12,11 @@ import static com.deftdevs.bootstrapi.commons.constants.BootstrAPI.SETTINGS; @Data -@Builder +@SuperBuilder @NoArgsConstructor -@AllArgsConstructor +@EqualsAndHashCode(callSuper = true) @XmlRootElement(name = SETTINGS) -public class SettingsModel { - - @XmlElement - private SettingsGeneralModel general; +public class SettingsModel extends AbstractSettingsModel { @XmlElement private SettingsBrandingLoginPageModel branding; diff --git a/crowd/src/main/java/com/deftdevs/bootstrapi/crowd/service/api/CrowdSettingsService.java b/crowd/src/main/java/com/deftdevs/bootstrapi/crowd/service/api/CrowdSettingsService.java index a18601fb..a1d3ed0c 100644 --- a/crowd/src/main/java/com/deftdevs/bootstrapi/crowd/service/api/CrowdSettingsService.java +++ b/crowd/src/main/java/com/deftdevs/bootstrapi/crowd/service/api/CrowdSettingsService.java @@ -15,7 +15,10 @@ public interface CrowdSettingsService extends CrowdSettingsBrandingService { default SettingsModel getSettings() { - return new SettingsModel(getSettingsGeneral(), getLoginPage()); + return SettingsModel.builder() + .general(getSettingsGeneral()) + .branding(getLoginPage()) + .build(); } default ServiceResult setSettings(final SettingsModel settingsModel) { diff --git a/crowd/src/test/java/com/deftdevs/bootstrapi/crowd/service/CrowdSettingsServiceTest.java b/crowd/src/test/java/com/deftdevs/bootstrapi/crowd/service/CrowdSettingsServiceTest.java index fc173780..0ac5a722 100644 --- a/crowd/src/test/java/com/deftdevs/bootstrapi/crowd/service/CrowdSettingsServiceTest.java +++ b/crowd/src/test/java/com/deftdevs/bootstrapi/crowd/service/CrowdSettingsServiceTest.java @@ -57,9 +57,10 @@ void testSetSettingsAppliesAllSubFields() { doReturn(SettingsGeneralModel.EXAMPLE_1).when(settingsService).setSettingsGeneral(SettingsGeneralModel.EXAMPLE_1); doReturn(SettingsBrandingLoginPageModel.EXAMPLE_1).when(settingsBrandingService).setLoginPage(SettingsBrandingLoginPageModel.EXAMPLE_1); - final ServiceResult result = settingsService.setSettings(new SettingsModel( - SettingsGeneralModel.EXAMPLE_1, - SettingsBrandingLoginPageModel.EXAMPLE_1)); + final ServiceResult result = settingsService.setSettings(SettingsModel.builder() + .general(SettingsGeneralModel.EXAMPLE_1) + .branding(SettingsBrandingLoginPageModel.EXAMPLE_1) + .build()); assertEquals(SettingsGeneralModel.EXAMPLE_1, result.getModel().getGeneral()); assertEquals(SettingsBrandingLoginPageModel.EXAMPLE_1, result.getModel().getBranding()); @@ -82,9 +83,10 @@ void testSetSettingsRecordsPerSubFieldFailure() { doThrow(new BadRequestException("invalid login page")) .when(settingsBrandingService).setLoginPage(SettingsBrandingLoginPageModel.EXAMPLE_1); - final ServiceResult result = settingsService.setSettings(new SettingsModel( - SettingsGeneralModel.EXAMPLE_1, - SettingsBrandingLoginPageModel.EXAMPLE_1)); + final ServiceResult result = settingsService.setSettings(SettingsModel.builder() + .general(SettingsGeneralModel.EXAMPLE_1) + .branding(SettingsBrandingLoginPageModel.EXAMPLE_1) + .build()); assertEquals(SettingsGeneralModel.EXAMPLE_1, result.getModel().getGeneral()); assertNull(result.getModel().getBranding()); diff --git a/jira/src/main/java/com/deftdevs/bootstrapi/jira/model/SettingsModel.java b/jira/src/main/java/com/deftdevs/bootstrapi/jira/model/SettingsModel.java index 18cb985a..eb10ac59 100644 --- a/jira/src/main/java/com/deftdevs/bootstrapi/jira/model/SettingsModel.java +++ b/jira/src/main/java/com/deftdevs/bootstrapi/jira/model/SettingsModel.java @@ -1,11 +1,10 @@ package com.deftdevs.bootstrapi.jira.model; -import com.deftdevs.bootstrapi.commons.model.SettingsGeneralModel; -import com.deftdevs.bootstrapi.commons.model.SettingsSecurityModel; -import lombok.AllArgsConstructor; -import lombok.Builder; +import com.deftdevs.bootstrapi.commons.model.AbstractSettingsSecurityModel; import lombok.Data; +import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; import javax.xml.bind.annotation.XmlElement; import javax.xml.bind.annotation.XmlRootElement; @@ -13,17 +12,11 @@ import static com.deftdevs.bootstrapi.commons.constants.BootstrAPI.SETTINGS; @Data -@Builder +@SuperBuilder @NoArgsConstructor -@AllArgsConstructor +@EqualsAndHashCode(callSuper = true) @XmlRootElement(name = SETTINGS) -public class SettingsModel { - - @XmlElement - private SettingsGeneralModel general; - - @XmlElement - private SettingsSecurityModel security; +public class SettingsModel extends AbstractSettingsSecurityModel { @XmlElement private SettingsBannerModel banner; diff --git a/jira/src/main/java/com/deftdevs/bootstrapi/jira/service/api/JiraSettingsService.java b/jira/src/main/java/com/deftdevs/bootstrapi/jira/service/api/JiraSettingsService.java index bef053c6..48a44470 100644 --- a/jira/src/main/java/com/deftdevs/bootstrapi/jira/service/api/JiraSettingsService.java +++ b/jira/src/main/java/com/deftdevs/bootstrapi/jira/service/api/JiraSettingsService.java @@ -23,7 +23,11 @@ SettingsBannerModel setSettingsBanner( SettingsBannerModel settingsBannerModel); default SettingsModel getSettings() { - return new SettingsModel(getSettingsGeneral(), getSettingsSecurity(), getSettingsBanner()); + return SettingsModel.builder() + .general(getSettingsGeneral()) + .security(getSettingsSecurity()) + .banner(getSettingsBanner()) + .build(); } default ServiceResult setSettings(final SettingsModel settingsModel) { diff --git a/jira/src/test/java/com/deftdevs/bootstrapi/jira/service/JiraSettingsServiceTest.java b/jira/src/test/java/com/deftdevs/bootstrapi/jira/service/JiraSettingsServiceTest.java index d05d4652..f65e4428 100644 --- a/jira/src/test/java/com/deftdevs/bootstrapi/jira/service/JiraSettingsServiceTest.java +++ b/jira/src/test/java/com/deftdevs/bootstrapi/jira/service/JiraSettingsServiceTest.java @@ -132,10 +132,11 @@ void testSetSettingsAppliesAllSubFields() { doReturn(SettingsSecurityModel.EXAMPLE_1).when(serviceSpy).setSettingsSecurity(SettingsSecurityModel.EXAMPLE_1); doReturn(SettingsBannerModel.EXAMPLE_1).when(serviceSpy).setSettingsBanner(SettingsBannerModel.EXAMPLE_1); - final ServiceResult result = serviceSpy.setSettings(new SettingsModel( - SettingsGeneralModel.EXAMPLE_1, - SettingsSecurityModel.EXAMPLE_1, - SettingsBannerModel.EXAMPLE_1)); + final ServiceResult result = serviceSpy.setSettings(SettingsModel.builder() + .general(SettingsGeneralModel.EXAMPLE_1) + .security(SettingsSecurityModel.EXAMPLE_1) + .banner(SettingsBannerModel.EXAMPLE_1) + .build()); assertEquals(SettingsGeneralModel.EXAMPLE_1, result.getModel().getGeneral()); assertEquals(SettingsSecurityModel.EXAMPLE_1, result.getModel().getSecurity()); @@ -161,10 +162,10 @@ void testSetSettingsRecordsPerSubFieldFailure() { doReturn(SettingsGeneralModel.EXAMPLE_1).when(serviceSpy).setSettingsGeneral(SettingsGeneralModel.EXAMPLE_1); doThrow(new BadRequestException("invalid banner")).when(serviceSpy).setSettingsBanner(SettingsBannerModel.EXAMPLE_1); - final ServiceResult result = serviceSpy.setSettings(new SettingsModel( - SettingsGeneralModel.EXAMPLE_1, - null, - SettingsBannerModel.EXAMPLE_1)); + final ServiceResult result = serviceSpy.setSettings(SettingsModel.builder() + .general(SettingsGeneralModel.EXAMPLE_1) + .banner(SettingsBannerModel.EXAMPLE_1) + .build()); assertEquals(SettingsGeneralModel.EXAMPLE_1, result.getModel().getGeneral()); assertNull(result.getModel().getBanner());