From 1b35340dba7ae628f33380d4a74785aa4ab1a791 Mon Sep 17 00:00:00 2001 From: Roger Floriano <31597636+petruki@users.noreply.github.com> Date: Thu, 19 Jun 2025 17:09:31 -0700 Subject: [PATCH 1/2] Added switcher.check context configuration to validate Switchers during start (#345) * Added switcher.check context configuration to validate Switchers during start * chore: removed unused env variable from master CI --- .github/workflows/master-2.yml | 60 +++++++++++++++++++ .github/workflows/master.yml | 2 - README.md | 11 ++++ .../switcherapi/client/ContextBuilder.java | 9 +++ .../switcherapi/client/SwitcherConfig.java | 27 +++++++++ .../client/SwitcherContextBase.java | 14 ++++- .../client/SwitcherPropertiesImpl.java | 2 + .../switcherapi/client/model/ContextKey.java | 5 ++ .../client/remote/dto/SwitchersCheck.java | 2 + .../SwitcherBasicCriteriaResponseTest.java | 1 - .../switcherapi/client/SwitcherBasicTest.java | 1 - .../client/SwitcherConfigTest.java | 5 ++ .../client/SwitcherForceResolveTest.java | 1 - .../client/remote/ClientRemoteTest.java | 35 +++++++++-- .../fixture/MockWebServerHelper.java | 8 +-- src/test/resources/switcherapi.properties | 2 + 16 files changed, 167 insertions(+), 18 deletions(-) create mode 100644 .github/workflows/master-2.yml diff --git a/.github/workflows/master-2.yml b/.github/workflows/master-2.yml new file mode 100644 index 0000000..3a9ff28 --- /dev/null +++ b/.github/workflows/master-2.yml @@ -0,0 +1,60 @@ +name: Master CI v2 + +on: + push: + branches: [master-2] + pull_request: + branches: [master-2] + +jobs: + build-scan: + name: SonarCloud Scan + runs-on: ubuntu-latest + if: "! contains(toJSON(github.event.commits.*.message), '[skip ci]')" + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + java-version: 17 + distribution: 'temurin' + cache: maven + + - name: Build/Test & SonarCloud Scan + run: mvn -B clean verify -Pcoverage,sonar -Dsonar.token=${{ secrets.SONAR_TOKEN }} + + build-test: + name: Build & Test - JDK ${{ matrix.java }} on ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + java: ['11', '17', '21'] + os: [ubuntu-latest, windows-latest] + runs-on: ${{ matrix.os }} + if: "! contains(toJSON(github.event.commits.*.message), '[skip ci]')" + + steps: + - uses: actions/checkout@v4 + + - name: Setup Java + uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: ${{ matrix.java }} + + - name: Show Versions + run: mvn -version + + - name: Cache Maven packages + uses: actions/cache@v4 + with: + path: ~/.m2/repository + key: ${{ runner.os }}-m2-${{ matrix.java }} + restore-keys: ${{ runner.os }}-m2- + + - name: Build/Test + run: mvn -B clean package diff --git a/.github/workflows/master.yml b/.github/workflows/master.yml index 4315680..ba59f18 100644 --- a/.github/workflows/master.yml +++ b/.github/workflows/master.yml @@ -26,8 +26,6 @@ jobs: - name: Build/Test & SonarCloud Scan run: mvn -B clean verify -Pcoverage,sonar -Dsonar.token=${{ secrets.SONAR_TOKEN }} - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} build-test: name: Build & Test - JDK ${{ matrix.java }} on ${{ matrix.os }} diff --git a/README.md b/README.md index 89c82ad..9b33a3f 100644 --- a/README.md +++ b/README.md @@ -70,6 +70,7 @@ switcher.domain -> Domain name #optional switcher.environment -> Environment name switcher.local -> true/false When local, it will only use a local snapshot +switcher.check -> true/false When true, it will check Switcher Keys switcher.relay.restrict -> true/false When true, it will check snapshot relay status switcher.snapshot.location -> Folder from where snapshots will be saved/read switcher.snapshot.auto -> true/false Automated lookup for snapshot when initializing the client @@ -314,6 +315,16 @@ void testSwitchers() { } ``` +Alternatively, you can also set the Switcher Context configuration to check during the client initialization. + +```java +MyAppFeatures.configure(ContextBuilder.builder() + ... + .checkSwitchers(true)); + +MyAppFeatures.initializeClient(); +``` + #### SwitcherTest annotation - Requires JUnit 5 Jupiter Predefine Switchers result outside your test methods with the SwitcherTest annotation.
It encapsulates the test and makes sure that the Switcher returns to its original state after concluding the test. diff --git a/src/main/java/com/github/switcherapi/client/ContextBuilder.java b/src/main/java/com/github/switcherapi/client/ContextBuilder.java index cb0fad8..15b5139 100644 --- a/src/main/java/com/github/switcherapi/client/ContextBuilder.java +++ b/src/main/java/com/github/switcherapi/client/ContextBuilder.java @@ -182,6 +182,15 @@ public ContextBuilder local(boolean local) { return this; } + /** + * @param checkSwitchers true/false When true, it will check switcher keys + * @return ContextBuilder + */ + public ContextBuilder checkSwitchers(boolean checkSwitchers) { + switcherProperties.setValue(ContextKey.CHECK_SWITCHERS, checkSwitchers); + return this; + } + /** * @param restrictRelay true/false When true, it will check snapshot relay status * @return ContextBuilder diff --git a/src/main/java/com/github/switcherapi/client/SwitcherConfig.java b/src/main/java/com/github/switcherapi/client/SwitcherConfig.java index c2acdbd..e7db78d 100644 --- a/src/main/java/com/github/switcherapi/client/SwitcherConfig.java +++ b/src/main/java/com/github/switcherapi/client/SwitcherConfig.java @@ -11,14 +11,17 @@ abstract class SwitcherConfig { protected String environment; protected boolean local; + protected boolean check; protected String silent; protected Integer timeout; protected Integer regexTimeout; protected Integer poolSize; + protected RelayConfig relay; protected SnapshotConfig snapshot; protected TruststoreConfig truststore; SwitcherConfig() { + this.relay = new RelayConfig(); this.snapshot = new SnapshotConfig(); this.truststore = new TruststoreConfig(); } @@ -35,10 +38,15 @@ protected void updateSwitcherConfig(SwitcherProperties properties) { setComponent(properties.getValue(ContextKey.COMPONENT)); setEnvironment(properties.getValue(ContextKey.ENVIRONMENT)); setLocal(properties.getBoolean(ContextKey.LOCAL_MODE)); + setCheck(properties.getBoolean(ContextKey.CHECK_SWITCHERS)); setSilent(properties.getValue(ContextKey.SILENT_MODE)); setTimeout(properties.getInt(ContextKey.TIMEOUT_MS)); setPoolSize(properties.getInt(ContextKey.POOL_CONNECTION_SIZE)); + RelayConfig relayConfig = new RelayConfig(); + relayConfig.setRestrict(properties.getBoolean(ContextKey.RESTRICT_RELAY)); + setRelay(relayConfig); + SnapshotConfig snapshotConfig = new SnapshotConfig(); snapshotConfig.setLocation(properties.getValue(ContextKey.SNAPSHOT_LOCATION)); snapshotConfig.setAuto(properties.getBoolean(ContextKey.SNAPSHOT_AUTO_LOAD)); @@ -93,6 +101,10 @@ public void setLocal(boolean local) { this.local = local; } + public void setCheck(boolean check) { + this.check = check; + } + public void setSilent(String silent) { this.silent = silent; } @@ -109,6 +121,9 @@ public void setPoolSize(Integer poolSize) { this.poolSize = poolSize; } + public void setRelay(RelayConfig relay) { + this.relay = relay; + } public void setSnapshot(SnapshotConfig snapshot) { this.snapshot = snapshot; } @@ -117,6 +132,18 @@ public void setTruststore(TruststoreConfig truststore) { this.truststore = truststore; } + public static class RelayConfig { + private boolean restrict; + + public boolean isRestrict() { + return restrict; + } + + public void setRestrict(boolean restrict) { + this.restrict = restrict; + } + } + public static class SnapshotConfig { private String location; private boolean auto; diff --git a/src/main/java/com/github/switcherapi/client/SwitcherContextBase.java b/src/main/java/com/github/switcherapi/client/SwitcherContextBase.java index 267d559..f7f7aba 100644 --- a/src/main/java/com/github/switcherapi/client/SwitcherContextBase.java +++ b/src/main/java/com/github/switcherapi/client/SwitcherContextBase.java @@ -107,6 +107,8 @@ protected void configureClient() { .environment(environment) .component(component) .local(local) + .checkSwitchers(check) + .restrictRelay(relay.isRestrict()) .silentMode(silent) .regexTimeout(regexTimeout) .timeoutMs(timeout) @@ -181,7 +183,7 @@ public static void initializeClient() { validateContext(); registerSwitcherKeys(); switcherExecutor = buildInstance(); - + loadSwitchers(); scheduleSnapshotAutoUpdate(contextStr(ContextKey.SNAPSHOT_AUTO_UPDATE_INTERVAL)); ContextBuilder.preConfigure(switcherProperties); @@ -250,8 +252,14 @@ private static void registerSwitcherKey(Field[] fields) { /** * Load Switcher instances into a map cache + * + * @throws SwitchersValidationException if "switcher.check" is enabled and one or more Switcher Keys are not found */ - private static void loadSwitchers() { + private static void loadSwitchers() throws SwitchersValidationException { + if (contextBol(ContextKey.CHECK_SWITCHERS)) { + checkSwitchers(); + } + if (Objects.isNull(switchers)) { switchers = new HashMap<>(); } @@ -447,7 +455,7 @@ public static void stopWatchingSnapshot() { * * @throws SwitchersValidationException when one or more Switcher Key is not found */ - public static void checkSwitchers() { + public static void checkSwitchers() throws SwitchersValidationException { switcherExecutor.checkSwitchers(switcherKeys); } diff --git a/src/main/java/com/github/switcherapi/client/SwitcherPropertiesImpl.java b/src/main/java/com/github/switcherapi/client/SwitcherPropertiesImpl.java index d8ee7d9..ea0f88e 100644 --- a/src/main/java/com/github/switcherapi/client/SwitcherPropertiesImpl.java +++ b/src/main/java/com/github/switcherapi/client/SwitcherPropertiesImpl.java @@ -23,6 +23,7 @@ public SwitcherPropertiesImpl() { setValue(ContextKey.SNAPSHOT_AUTO_LOAD, false); setValue(ContextKey.SNAPSHOT_SKIP_VALIDATION, false); setValue(ContextKey.LOCAL_MODE, false); + setValue(ContextKey.CHECK_SWITCHERS, false); setValue(ContextKey.RESTRICT_RELAY, true); } @@ -40,6 +41,7 @@ public void loadFromProperties(Properties prop) { setValue(ContextKey.SNAPSHOT_AUTO_UPDATE_INTERVAL, SwitcherUtils.resolveProperties(ContextKey.SNAPSHOT_AUTO_UPDATE_INTERVAL.getParam(), prop)); setValue(ContextKey.SILENT_MODE, SwitcherUtils.resolveProperties(ContextKey.SILENT_MODE.getParam(), prop)); setValue(ContextKey.LOCAL_MODE, getBoolDefault(SwitcherUtils.resolveProperties(ContextKey.LOCAL_MODE.getParam(), prop), false)); + setValue(ContextKey.CHECK_SWITCHERS, getBoolDefault(SwitcherUtils.resolveProperties(ContextKey.CHECK_SWITCHERS.getParam(), prop), false)); setValue(ContextKey.RESTRICT_RELAY, getBoolDefault(SwitcherUtils.resolveProperties(ContextKey.RESTRICT_RELAY.getParam(), prop), true)); setValue(ContextKey.REGEX_TIMEOUT, getIntDefault(SwitcherUtils.resolveProperties(ContextKey.REGEX_TIMEOUT.getParam(), prop), DEFAULT_REGEX_TIMEOUT)); setValue(ContextKey.TRUSTSTORE_PATH, SwitcherUtils.resolveProperties(ContextKey.TRUSTSTORE_PATH.getParam(), prop)); diff --git a/src/main/java/com/github/switcherapi/client/model/ContextKey.java b/src/main/java/com/github/switcherapi/client/model/ContextKey.java index e0d6227..a87f75f 100644 --- a/src/main/java/com/github/switcherapi/client/model/ContextKey.java +++ b/src/main/java/com/github/switcherapi/client/model/ContextKey.java @@ -69,6 +69,11 @@ public enum ContextKey { */ LOCAL_MODE("switcher.local"), + /** + * (boolean) Defines if client will check the switchers before using them (default is false). + */ + CHECK_SWITCHERS("switcher.check"), + /** * (boolean) Defines if client will trigger local snapshot relay verification (default is true) */ diff --git a/src/main/java/com/github/switcherapi/client/remote/dto/SwitchersCheck.java b/src/main/java/com/github/switcherapi/client/remote/dto/SwitchersCheck.java index a43bd3b..b676b3d 100644 --- a/src/main/java/com/github/switcherapi/client/remote/dto/SwitchersCheck.java +++ b/src/main/java/com/github/switcherapi/client/remote/dto/SwitchersCheck.java @@ -4,6 +4,7 @@ import java.util.Set; import com.github.switcherapi.client.remote.ClientWS; +import com.google.gson.annotations.SerializedName; /** * Request/Response model to use with {@link ClientWS#checkSwitchers(Set, String)} @@ -21,6 +22,7 @@ public class SwitchersCheck { /** * Response field */ + @SerializedName("not_found") private String[] notFound; public SwitchersCheck() {} diff --git a/src/test/java/com/github/switcherapi/client/SwitcherBasicCriteriaResponseTest.java b/src/test/java/com/github/switcherapi/client/SwitcherBasicCriteriaResponseTest.java index 353af42..875ceaf 100644 --- a/src/test/java/com/github/switcherapi/client/SwitcherBasicCriteriaResponseTest.java +++ b/src/test/java/com/github/switcherapi/client/SwitcherBasicCriteriaResponseTest.java @@ -25,7 +25,6 @@ static void setup() throws IOException { MockWebServerHelper.setupMockServer(); Switchers.loadProperties(); // Load default properties from resources - Switchers.initializeClient(); // SwitcherContext requires preload before config override Switchers.configure(ContextBuilder.builder() // Override default properties .url(String.format("http://localhost:%s", mockBackEnd.getPort())) .local(false) diff --git a/src/test/java/com/github/switcherapi/client/SwitcherBasicTest.java b/src/test/java/com/github/switcherapi/client/SwitcherBasicTest.java index 7c3dd13..5a57c12 100644 --- a/src/test/java/com/github/switcherapi/client/SwitcherBasicTest.java +++ b/src/test/java/com/github/switcherapi/client/SwitcherBasicTest.java @@ -23,7 +23,6 @@ static void setup() throws IOException { MockWebServerHelper.setupMockServer(); Switchers.loadProperties(); // Load default properties from resources - Switchers.initializeClient(); // SwitcherContext requires preload before config override Switchers.configure(ContextBuilder.builder() // Override default properties .url(String.format("http://localhost:%s", mockBackEnd.getPort())) .local(false) diff --git a/src/test/java/com/github/switcherapi/client/SwitcherConfigTest.java b/src/test/java/com/github/switcherapi/client/SwitcherConfigTest.java index 5c7ec79..7619320 100644 --- a/src/test/java/com/github/switcherapi/client/SwitcherConfigTest.java +++ b/src/test/java/com/github/switcherapi/client/SwitcherConfigTest.java @@ -38,6 +38,9 @@ void shouldInitializeClientFromSwitcherConfig_Minimal() { private T buildSwitcherClientConfig(T classConfig, String component, String domain) { + SwitcherConfig.RelayConfig relay = new SwitcherConfig.RelayConfig(); + relay.setRestrict(false); + SwitcherConfig.SnapshotConfig snapshot = new SwitcherConfig.SnapshotConfig(); snapshot.setLocation(SNAPSHOTS_LOCAL); snapshot.setUpdateInterval(null); @@ -54,10 +57,12 @@ private T buildSwitcherClientConfig(T classConfig, St classConfig.setDomain(domain); classConfig.setEnvironment("fixture1"); classConfig.setLocal(true); + classConfig.setCheck(false); classConfig.setSilent("5m"); classConfig.setTimeout(3000); classConfig.setRegexTimeout(1000); classConfig.setPoolSize(2); + classConfig.setRelay(relay); classConfig.setSnapshot(snapshot); classConfig.setTruststore(truststore); return classConfig; diff --git a/src/test/java/com/github/switcherapi/client/SwitcherForceResolveTest.java b/src/test/java/com/github/switcherapi/client/SwitcherForceResolveTest.java index 5f2d45c..7dfb2f1 100644 --- a/src/test/java/com/github/switcherapi/client/SwitcherForceResolveTest.java +++ b/src/test/java/com/github/switcherapi/client/SwitcherForceResolveTest.java @@ -22,7 +22,6 @@ static void setup() throws IOException { MockWebServerHelper.setupMockServer(); Switchers.loadProperties(); // Load default properties from resources - Switchers.initializeClient(); // SwitcherContext requires preload before config override Switchers.configure(ContextBuilder.builder() // Override default properties .url(String.format("http://localhost:%s", mockBackEnd.getPort())) .local(true) diff --git a/src/test/java/com/github/switcherapi/client/remote/ClientRemoteTest.java b/src/test/java/com/github/switcherapi/client/remote/ClientRemoteTest.java index 539717a..66bad3e 100644 --- a/src/test/java/com/github/switcherapi/client/remote/ClientRemoteTest.java +++ b/src/test/java/com/github/switcherapi/client/remote/ClientRemoteTest.java @@ -2,7 +2,9 @@ import com.github.switcherapi.Switchers; import com.github.switcherapi.client.ContextBuilder; +import com.github.switcherapi.client.SwitcherContext; import com.github.switcherapi.client.SwitcherProperties; +import com.github.switcherapi.client.exception.SwitchersValidationException; import com.github.switcherapi.client.model.SwitcherRequest; import com.github.switcherapi.client.model.SwitcherResult; import com.github.switcherapi.client.remote.dto.SwitchersCheck; @@ -29,8 +31,7 @@ import java.util.concurrent.Executors; import static com.github.switcherapi.client.remote.Constants.DEFAULT_TIMEOUT; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.*; class ClientRemoteTest extends MockWebServerHelper { @@ -60,7 +61,7 @@ void resetSwitcherContextState() { clientRemote = new ClientRemoteService(ClientWSImpl.build(switcherProperties, executorService, DEFAULT_TIMEOUT), switcherProperties); ((QueueDispatcher) mockBackEnd.getDispatcher()).clear(); - Switchers.configure(ContextBuilder.builder()); + Switchers.configure(ContextBuilder.builder().checkSwitchers(false)); Switchers.initializeClient(); } @@ -84,7 +85,7 @@ void shouldExecuteCriteria() { } @Test - void shouldCheckSwitchers() { + void shouldCheckSwitchersError() { //given final Set switcherKeys = new HashSet<>(); switcherKeys.add("KEY"); @@ -97,4 +98,30 @@ void shouldCheckSwitchers() { assertEquals(1, actual.getNotFound().length); } + @Test + void shouldCheckSwitchersError_throughContextConfiguration() { + //given + final Set switcherKeys = new HashSet<>(); + switcherKeys.add("KEY"); + + givenResponse(generateMockAuth(100)); + givenResponse(generateCheckSwitchersResponse(switcherKeys)); + + //test + Switchers.configure(ContextBuilder.builder().checkSwitchers(true)); + SwitchersValidationException exception = assertThrows(SwitchersValidationException.class, SwitcherContext::initializeClient); + assertEquals("Something went wrong: Unable to load the following Switcher Key(s): [KEY]", exception.getMessage()); + } + + @Test + void shouldCheckSwitchersSuccess_throughContextConfiguration() { + //given + givenResponse(generateMockAuth(100)); + givenResponse(generateCheckSwitchersResponse(new HashSet<>())); + + //test + Switchers.configure(ContextBuilder.builder().checkSwitchers(true)); + assertDoesNotThrow(SwitcherContext::initializeClient); + } + } diff --git a/src/test/java/com/github/switcherapi/fixture/MockWebServerHelper.java b/src/test/java/com/github/switcherapi/fixture/MockWebServerHelper.java index bec19f6..ca5721a 100644 --- a/src/test/java/com/github/switcherapi/fixture/MockWebServerHelper.java +++ b/src/test/java/com/github/switcherapi/fixture/MockWebServerHelper.java @@ -2,7 +2,6 @@ import com.github.switcherapi.client.model.criteria.Data; import com.github.switcherapi.client.model.criteria.Snapshot; -import com.github.switcherapi.client.remote.dto.SwitchersCheck; import com.github.switcherapi.client.remote.ClientWSImpl; import com.github.switcherapi.client.remote.dto.CriteriaRequest; import com.github.switcherapi.client.utils.SnapshotLoader; @@ -178,13 +177,10 @@ protected MockResponse generateCriteriaResponse(T metadata) { * @return Generated mock /criteria/check_switchers */ protected MockResponse generateCheckSwitchersResponse(Set switchersNotFound) { - SwitchersCheck switchersCheckNotFound = new SwitchersCheck(); - switchersCheckNotFound.setNotFound( - switchersNotFound.toArray(new String[0])); + String jsonResponse = "{ \"not_found\": [%s] }"; - Gson gson = new Gson(); MockResponse.Builder builder = new MockResponse.Builder(); - builder.body(gson.toJson(switchersCheckNotFound)); + builder.body(String.format(jsonResponse, String.join("\",\"", switchersNotFound))); builder.addHeader("Content-Type", "application/json"); return builder.build(); } diff --git a/src/test/resources/switcherapi.properties b/src/test/resources/switcherapi.properties index 3c50e3d..9215354 100644 --- a/src/test/resources/switcherapi.properties +++ b/src/test/resources/switcherapi.properties @@ -7,6 +7,8 @@ switcher.domain=switcher-domain #optional switcher.local= +switcher.check= +switcher.relay.restrict= switcher.snapshot.location= switcher.snapshot.auto= switcher.snapshot.updateinterval= From d8465f49c9ef037c0c948ebf4bb250c30ec8c326 Mon Sep 17 00:00:00 2001 From: petruki <31597636+petruki@users.noreply.github.com> Date: Thu, 19 Jun 2025 17:26:41 -0700 Subject: [PATCH 2/2] Fixed notFound serialization using Jackson lib --- .../switcherapi/client/remote/dto/SwitchersCheck.java | 8 ++++---- .../github/switcherapi/fixture/MockWebServerHelper.java | 5 ++++- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/github/switcherapi/client/remote/dto/SwitchersCheck.java b/src/main/java/com/github/switcherapi/client/remote/dto/SwitchersCheck.java index b676b3d..575b00b 100644 --- a/src/main/java/com/github/switcherapi/client/remote/dto/SwitchersCheck.java +++ b/src/main/java/com/github/switcherapi/client/remote/dto/SwitchersCheck.java @@ -1,11 +1,11 @@ package com.github.switcherapi.client.remote.dto; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.switcherapi.client.remote.ClientWS; + import java.util.Arrays; import java.util.Set; -import com.github.switcherapi.client.remote.ClientWS; -import com.google.gson.annotations.SerializedName; - /** * Request/Response model to use with {@link ClientWS#checkSwitchers(Set, String)} * @@ -22,7 +22,7 @@ public class SwitchersCheck { /** * Response field */ - @SerializedName("not_found") + @JsonProperty("not_found") private String[] notFound; public SwitchersCheck() {} diff --git a/src/test/java/com/github/switcherapi/fixture/MockWebServerHelper.java b/src/test/java/com/github/switcherapi/fixture/MockWebServerHelper.java index ca5721a..0a45fd1 100644 --- a/src/test/java/com/github/switcherapi/fixture/MockWebServerHelper.java +++ b/src/test/java/com/github/switcherapi/fixture/MockWebServerHelper.java @@ -180,8 +180,11 @@ protected MockResponse generateCheckSwitchersResponse(Set switchersNotFo String jsonResponse = "{ \"not_found\": [%s] }"; MockResponse.Builder builder = new MockResponse.Builder(); - builder.body(String.format(jsonResponse, String.join("\",\"", switchersNotFound))); builder.addHeader("Content-Type", "application/json"); + builder.body(String.format(jsonResponse, switchersNotFound.stream() + .map(s -> "\"" + s + "\"") + .collect(java.util.stream.Collectors.joining(",")))); + return builder.build(); }