diff --git a/sdk/appconfiguration/azure-data-appconfiguration/CHANGELOG.md b/sdk/appconfiguration/azure-data-appconfiguration/CHANGELOG.md index 52fadc363fb8..56bf36a68dbd 100644 --- a/sdk/appconfiguration/azure-data-appconfiguration/CHANGELOG.md +++ b/sdk/appconfiguration/azure-data-appconfiguration/CHANGELOG.md @@ -4,6 +4,8 @@ ### Features Added +- Added `checkConfigurationSettings` method to `ConfigurationClient` and `ConfigurationAsyncClient` that performs HEAD requests to efficiently check if configuration settings have changed by comparing page-level ETags without retrieving the full response body. + ### Breaking Changes ### Bugs Fixed diff --git a/sdk/appconfiguration/azure-data-appconfiguration/src/main/java/com/azure/data/appconfiguration/ConfigurationAsyncClient.java b/sdk/appconfiguration/azure-data-appconfiguration/src/main/java/com/azure/data/appconfiguration/ConfigurationAsyncClient.java index 1fee78715b51..c8ab55c04835 100644 --- a/sdk/appconfiguration/azure-data-appconfiguration/src/main/java/com/azure/data/appconfiguration/ConfigurationAsyncClient.java +++ b/sdk/appconfiguration/azure-data-appconfiguration/src/main/java/com/azure/data/appconfiguration/ConfigurationAsyncClient.java @@ -84,7 +84,8 @@ * *
  * ConfigurationAsyncClient configurationAsyncClient = new ConfigurationClientBuilder()
- *     .connectionString(connectionString)
+ *     .credential(new DefaultAzureCredentialBuilder().build())
+ *     .endpoint(endpoint)
  *     .buildAsyncClient();
  * 
* @@ -1050,6 +1051,56 @@ public PagedFlux listConfigurationSettings(SettingSelector .map(ConfigurationSettingDeserializationHelper::toConfigurationSettingWithPagedResponse))); } + /** + * Checks configuration settings using a HEAD request, returning only headers without the response body. + * This is useful for efficiently checking if settings have changed by comparing ETags. + * + *

The returned items will be empty since HEAD requests do not return a body. Use {@code byPage()} iteration + * to access page-level ETags for change detection.

+ * + *

Code Samples

+ * + *

Check all settings that use the key "prodDBConnection".

+ * + * + *
+     * SettingSelector selector = new SettingSelector().setKeyFilter("my-app/*");
+     * client.checkConfigurationSettings(selector)
+     *     .byPage()
+     *     .subscribe(page -> {
+     *         System.out.println("Status code: " + page.getStatusCode());
+     *         System.out.println("Page ETag: " + page.getHeaders().getValue(com.azure.core.http.HttpHeaderName.ETAG));
+     *     });
+     * 
+ * + * + * @param selector Optional. Selector to filter configuration setting results from the service. + * @return A Flux of ConfigurationSettings with empty items. Use {@code byPage()} to access page-level ETags. + * @throws HttpResponseException If a client or service error occurs, such as a 404, 409, 429 or 500. + */ + @ServiceMethod(returns = ReturnType.COLLECTION) + public PagedFlux checkConfigurationSettings(SettingSelector selector) { + final String keyFilter = selector == null ? null : selector.getKeyFilter(); + final String labelFilter = selector == null ? null : selector.getLabelFilter(); + final String acceptDateTime = selector == null ? null : selector.getAcceptDateTime(); + final List settingFields = selector == null ? null : toSettingFieldsList(selector.getFields()); + final List matchConditionsList = selector == null ? null : selector.getMatchConditions(); + final List tagsFilter = selector == null ? null : selector.getTagsFilter(); + AtomicInteger pageETagIndex = new AtomicInteger(0); + return new PagedFlux<>(() -> withContext(context -> serviceClient + .checkKeyValuesWithResponseAsync(keyFilter, labelFilter, null, acceptDateTime, settingFields, null, null, + getPageETag(matchConditionsList, pageETagIndex), tagsFilter, context) + .map(Utility::toHeadPagedResponse) + .onErrorResume(HttpResponseException.class, + (Function>>) Utility::handleHeadNotModifiedErrorToValidResponse)), + afterToken -> withContext(context -> serviceClient + .checkKeyValuesWithResponseAsync(keyFilter, labelFilter, afterToken, acceptDateTime, settingFields, + null, null, getPageETag(matchConditionsList, pageETagIndex), tagsFilter, context) + .map(Utility::toHeadPagedResponse) + .onErrorResume(HttpResponseException.class, + (Function>>) Utility::handleHeadNotModifiedErrorToValidResponse))); + } + /** * Fetches the configuration settings in a snapshot that matches the {@code snapshotName}. If {@code snapshotName} * is {@code null}, then all the {@link ConfigurationSetting configuration settings} are fetched with their diff --git a/sdk/appconfiguration/azure-data-appconfiguration/src/main/java/com/azure/data/appconfiguration/ConfigurationClient.java b/sdk/appconfiguration/azure-data-appconfiguration/src/main/java/com/azure/data/appconfiguration/ConfigurationClient.java index e09a97c5d6d9..7719f4e13081 100644 --- a/sdk/appconfiguration/azure-data-appconfiguration/src/main/java/com/azure/data/appconfiguration/ConfigurationClient.java +++ b/sdk/appconfiguration/azure-data-appconfiguration/src/main/java/com/azure/data/appconfiguration/ConfigurationClient.java @@ -11,6 +11,7 @@ import com.azure.core.exception.ResourceModifiedException; import com.azure.core.exception.ResourceNotFoundException; import com.azure.core.http.HttpResponse; +import com.azure.core.http.HttpHeaderName; import com.azure.core.http.MatchConditions; import com.azure.core.http.rest.PagedIterable; import com.azure.core.http.rest.PagedResponse; @@ -52,6 +53,8 @@ import static com.azure.data.appconfiguration.implementation.Utility.getETag; import static com.azure.data.appconfiguration.implementation.Utility.getPageETag; import static com.azure.data.appconfiguration.implementation.Utility.handleNotModifiedErrorToValidResponse; +import static com.azure.data.appconfiguration.implementation.Utility.toHeadPagedResponse; +import static com.azure.data.appconfiguration.implementation.Utility.handleHeadNotModifiedErrorToValidResponse; import static com.azure.data.appconfiguration.implementation.Utility.toKeyValue; import static com.azure.data.appconfiguration.implementation.Utility.toSettingFieldsList; import static com.azure.data.appconfiguration.implementation.Utility.updateSnapshotSync; @@ -84,7 +87,8 @@ * *
  * ConfigurationClient configurationClient = new ConfigurationClientBuilder()
- *     .connectionString(connectionString)
+ *     .credential(new DefaultAzureCredentialBuilder().build())
+ *     .endpoint(endpoint)
  *     .buildClient();
  * 
* @@ -1080,6 +1084,99 @@ public PagedIterable listConfigurationSettings(SettingSele }); } + /** + * Checks configuration settings using a HEAD request, returning only headers without the response body. + * This is useful for efficiently checking if settings have changed by comparing ETags. + * + *

The returned items will be empty since HEAD requests do not return a body. Use + * {@link PagedIterable#iterableByPage()} to access page-level ETags for change detection.

+ * + *

Code Samples

+ * + *

Check all settings that use the key "prodDBConnection".

+ * + * + *
+     * SettingSelector selector = new SettingSelector().setKeyFilter("my-app/*");
+     * client.checkConfigurationSettings(selector)
+     *     .iterableByPage()
+     *     .forEach(page -> {
+     *         System.out.println("Status code: " + page.getStatusCode());
+     *         System.out.println("Page ETag: " + page.getHeaders().getValue(com.azure.core.http.HttpHeaderName.ETAG));
+     *     });
+     * 
+ * + * + * @param selector Optional. Selector to filter configuration setting results from the service. + * @return A {@link PagedIterable} of ConfigurationSettings with empty items. Use {@code iterableByPage()} to access + * page-level ETags. + * @throws HttpResponseException If a client or service error occurs, such as a 404, 409, 429 or 500. + */ + @ServiceMethod(returns = ReturnType.COLLECTION) + public PagedIterable checkConfigurationSettings(SettingSelector selector) { + return checkConfigurationSettings(selector, Context.NONE); + } + + /** + * Checks configuration settings using a HEAD request, returning only headers without the response body. + * This is useful for efficiently checking if settings have changed by comparing ETags. + * + *

The returned items will be empty since HEAD requests do not return a body. Use + * {@link PagedIterable#iterableByPage()} to access page-level ETags for change detection.

+ * + *

Code Samples

+ * + *

Check all settings that use the key "prodDBConnection".

+ * + * + * + *
+     * SettingSelector selector = new SettingSelector().setKeyFilter("my-app/*");
+     * Context ctx = new Context(key1, value1);
+     * client.checkConfigurationSettings(selector, ctx)
+     *     .iterableByPage()
+     *     .forEach(page -> {
+     *         System.out.println("Status code: " + page.getStatusCode());
+     *         System.out.println("Page ETag: " + page.getHeaders().getValue(com.azure.core.http.HttpHeaderName.ETAG));
+     *     });
+     * 
+ * + * + * @param selector Optional. Selector to filter configuration setting results from the service. + * @param context Additional context that is passed through the Http pipeline during the service call. + * @return A {@link PagedIterable} of ConfigurationSettings with empty items. Use {@code iterableByPage()} to access + * page-level ETags. + * @throws HttpResponseException If a client or service error occurs, such as a 404, 409, 429 or 500. + */ + @ServiceMethod(returns = ReturnType.COLLECTION) + public PagedIterable checkConfigurationSettings(SettingSelector selector, Context context) { + final String keyFilter = selector == null ? null : selector.getKeyFilter(); + final String labelFilter = selector == null ? null : selector.getLabelFilter(); + final String acceptDateTime = selector == null ? null : selector.getAcceptDateTime(); + final List settingFields = selector == null ? null : toSettingFieldsList(selector.getFields()); + final List matchConditionsList = selector == null ? null : selector.getMatchConditions(); + final List tagsFilter = selector == null ? null : selector.getTagsFilter(); + + AtomicInteger pageETagIndex = new AtomicInteger(0); + return new PagedIterable<>(() -> { + try { + return toHeadPagedResponse(serviceClient.checkKeyValuesWithResponse(keyFilter, labelFilter, null, + acceptDateTime, settingFields, null, null, getPageETag(matchConditionsList, pageETagIndex), + tagsFilter, context)); + } catch (HttpResponseException ex) { + return handleHeadNotModifiedErrorToValidResponse(ex, LOGGER, true); + } + }, afterToken -> { + try { + return toHeadPagedResponse(serviceClient.checkKeyValuesWithResponse(keyFilter, labelFilter, afterToken, + acceptDateTime, settingFields, null, null, getPageETag(matchConditionsList, pageETagIndex), + tagsFilter, context)); + } catch (HttpResponseException ex) { + return handleHeadNotModifiedErrorToValidResponse(ex, LOGGER, true); + } + }); + } + /** * Fetches the configuration settings in a snapshot that matches the {@code snapshotName}. If {@code snapshotName} * is {@code null}, then all the {@link ConfigurationSetting configuration settings} are fetched with their current @@ -1589,4 +1686,5 @@ public PagedIterable listLabels(SettingLabelSelector selector, Con public void updateSyncToken(String token) { syncTokenPolicy.updateSyncToken(token); } + } diff --git a/sdk/appconfiguration/azure-data-appconfiguration/src/main/java/com/azure/data/appconfiguration/ConfigurationClientBuilder.java b/sdk/appconfiguration/azure-data-appconfiguration/src/main/java/com/azure/data/appconfiguration/ConfigurationClientBuilder.java index 26a0b21942b4..e5c2d81b055f 100644 --- a/sdk/appconfiguration/azure-data-appconfiguration/src/main/java/com/azure/data/appconfiguration/ConfigurationClientBuilder.java +++ b/sdk/appconfiguration/azure-data-appconfiguration/src/main/java/com/azure/data/appconfiguration/ConfigurationClientBuilder.java @@ -72,7 +72,8 @@ * *
  * ConfigurationAsyncClient configurationAsyncClient = new ConfigurationClientBuilder()
- *     .connectionString(connectionString)
+ *     .credential(new DefaultAzureCredentialBuilder().build())
+ *     .endpoint(endpoint)
  *     .buildAsyncClient();
  * 
* @@ -82,7 +83,8 @@ * *
  * ConfigurationClient configurationClient = new ConfigurationClientBuilder()
- *     .connectionString(connectionString)
+ *     .credential(new DefaultAzureCredentialBuilder().build())
+ *     .endpoint(endpoint)
  *     .buildClient();
  * 
* @@ -102,7 +104,7 @@ * ConfigurationClient configurationClient = new ConfigurationClientBuilder() * .pipeline(pipeline) * .endpoint("https://dummy.azure.net/") - * .connectionString(connectionString) + * .credential(new DefaultAzureCredentialBuilder().build()) * .buildClient(); * * diff --git a/sdk/appconfiguration/azure-data-appconfiguration/src/main/java/com/azure/data/appconfiguration/implementation/Utility.java b/sdk/appconfiguration/azure-data-appconfiguration/src/main/java/com/azure/data/appconfiguration/implementation/Utility.java index ee7c029a9279..e41c7f4f940e 100644 --- a/sdk/appconfiguration/azure-data-appconfiguration/src/main/java/com/azure/data/appconfiguration/implementation/Utility.java +++ b/sdk/appconfiguration/azure-data-appconfiguration/src/main/java/com/azure/data/appconfiguration/implementation/Utility.java @@ -15,6 +15,7 @@ import com.azure.core.util.Context; import com.azure.core.util.CoreUtils; import com.azure.core.util.logging.ClientLogger; +import com.azure.data.appconfiguration.implementation.models.CheckKeyValuesHeaders; import com.azure.data.appconfiguration.implementation.models.KeyValue; import com.azure.data.appconfiguration.implementation.models.SnapshotUpdateParameters; import com.azure.data.appconfiguration.implementation.models.UpdateSnapshotHeaders; @@ -26,6 +27,7 @@ import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Objects; @@ -210,4 +212,62 @@ public static List getTagsFilterInString(Map tagsFilter) } return tagsFilters; } + + // Parse the 'after' query parameter value from the Link header. + // Link header format: ; rel="next" + public static String parseAfterParam(String linkHeader) { + String nextLink = parseNextLink(linkHeader); + if (nextLink == null) { + return null; + } + int afterIdx = nextLink.indexOf("after="); + if (afterIdx == -1) { + return null; + } + String afterValue = nextLink.substring(afterIdx + 6); + int ampIdx = afterValue.indexOf('&'); + return ampIdx != -1 ? afterValue.substring(0, ampIdx) : afterValue; + } + + // Convert a HEAD response to a PagedResponse with empty items. + public static PagedResponse + toHeadPagedResponse(ResponseBase response) { + String continuationToken = parseAfterParam(response.getHeaders().getValue(HttpHeaderName.LINK)); + return new PagedResponseBase<>(response.getRequest(), response.getStatusCode(), response.getHeaders(), + Collections.emptyList(), continuationToken, null); + } + + // Handle 304 status code from HEAD request to a valid response - Async handler + public static Mono> + handleHeadNotModifiedErrorToValidResponse(HttpResponseException error) { + HttpResponse httpResponse = error.getResponse(); + if (httpResponse == null) { + return Mono.error(error); + } + + String continuationToken = parseAfterParam(httpResponse.getHeaderValue(HttpHeaderName.LINK)); + if (httpResponse.getStatusCode() == 304) { + return Mono.just(new PagedResponseBase<>(httpResponse.getRequest(), httpResponse.getStatusCode(), + httpResponse.getHeaders(), Collections.emptyList(), continuationToken, null)); + } + + return Mono.error(error); + } + + // Handle 304 status code from HEAD request to a valid response - Sync handler + public static PagedResponse + handleHeadNotModifiedErrorToValidResponse(HttpResponseException error, ClientLogger logger, boolean isHead) { + HttpResponse httpResponse = error.getResponse(); + if (httpResponse == null) { + throw logger.logExceptionAsError(error); + } + + String continuationToken = parseAfterParam(httpResponse.getHeaderValue(HttpHeaderName.LINK)); + if (httpResponse.getStatusCode() == 304) { + return new PagedResponseBase<>(httpResponse.getRequest(), httpResponse.getStatusCode(), + httpResponse.getHeaders(), Collections.emptyList(), continuationToken, null); + } + + throw logger.logExceptionAsError(error); + } } diff --git a/sdk/appconfiguration/azure-data-appconfiguration/src/main/java/com/azure/data/appconfiguration/package-info.java b/sdk/appconfiguration/azure-data-appconfiguration/src/main/java/com/azure/data/appconfiguration/package-info.java index 284c471a8cd7..24873502c0d1 100644 --- a/sdk/appconfiguration/azure-data-appconfiguration/src/main/java/com/azure/data/appconfiguration/package-info.java +++ b/sdk/appconfiguration/azure-data-appconfiguration/src/main/java/com/azure/data/appconfiguration/package-info.java @@ -34,7 +34,8 @@ * *
  * ConfigurationAsyncClient configurationAsyncClient = new ConfigurationClientBuilder()
- *     .connectionString(connectionString)
+ *     .credential(new DefaultAzureCredentialBuilder().build())
+ *     .endpoint(endpoint)
  *     .buildAsyncClient();
  * 
* @@ -48,7 +49,8 @@ * *
  * ConfigurationClient configurationClient = new ConfigurationClientBuilder()
- *     .connectionString(connectionString)
+ *     .credential(new DefaultAzureCredentialBuilder().build())
+ *     .endpoint(endpoint)
  *     .buildClient();
  * 
* diff --git a/sdk/appconfiguration/azure-data-appconfiguration/src/samples/java/com/azure/data/appconfiguration/CheckConfigurationSettingsForChanges.java b/sdk/appconfiguration/azure-data-appconfiguration/src/samples/java/com/azure/data/appconfiguration/CheckConfigurationSettingsForChanges.java new file mode 100644 index 000000000000..d9ba0133f4cc --- /dev/null +++ b/sdk/appconfiguration/azure-data-appconfiguration/src/samples/java/com/azure/data/appconfiguration/CheckConfigurationSettingsForChanges.java @@ -0,0 +1,107 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.data.appconfiguration; + +import com.azure.core.http.HttpHeaderName; +import com.azure.core.http.MatchConditions; +import com.azure.core.http.rest.PagedIterable; +import com.azure.core.http.rest.PagedResponse; +import com.azure.core.util.Configuration; +import com.azure.data.appconfiguration.models.ConfigurationSetting; +import com.azure.data.appconfiguration.models.SettingSelector; +import com.azure.identity.DefaultAzureCredentialBuilder; + +import java.util.ArrayList; +import java.util.List; + +/** + * Sample demonstrates how to use HEAD requests to efficiently check for configuration changes. + * This is useful for polling scenarios where you want to minimize bandwidth usage. + */ +public class CheckConfigurationSettingsForChanges { + /** + * Runs the sample algorithm demonstrating HEAD-based change detection. + * + * @param args Unused. Arguments to the program. + */ + public static void main(String[] args) { + // The endpoint can be obtained by going to your App Configuration instance in the Azure portal + // and navigating to "Overview" page. Looking for the "Endpoint" keyword. + String endpoint = Configuration.getGlobalConfiguration().get("AZ_CONFIG_ENDPOINT"); + + // Instantiate a client that will be used to call the service. + final ConfigurationClient client = new ConfigurationClientBuilder() + .credential(new DefaultAzureCredentialBuilder().build()) + .endpoint(endpoint) + .buildClient(); + + // Create test settings + final String key = "hello"; + final String value = "world"; + ConfigurationSetting setting = client.setConfigurationSetting(key, null, value); + System.out.printf("[SetConfigurationSetting] Key: %s, Value: %s%n", setting.getKey(), setting.getValue()); + + SettingSelector selector = new SettingSelector().setKeyFilter(key); + + // Perform an initial HEAD request to capture page ETags + List cachedPageETags = new ArrayList<>(); + PagedIterable headResult = client.checkConfigurationSettings(selector); + for (PagedResponse page : headResult.iterableByPage()) { + String pageETag = page.getHeaders().getValue(HttpHeaderName.ETAG); + cachedPageETags.add(pageETag); + System.out.printf("[CheckConfigurationSettings] Captured page ETag: %s%n", pageETag); + } + + // Check for changes using cached ETags with If-None-Match + // If no changes occurred, the service returns 304 Not Modified + List matchConditions = new ArrayList<>(); + for (String cachedETag : cachedPageETags) { + matchConditions.add(new MatchConditions().setIfNoneMatch(cachedETag)); + } + + SettingSelector conditionalSelector = new SettingSelector() + .setKeyFilter(key) + .setMatchConditions(matchConditions); + + PagedIterable conditionalResult = client.checkConfigurationSettings(conditionalSelector); + for (PagedResponse page : conditionalResult.iterableByPage()) { + if (page.getStatusCode() == 304) { + System.out.println("[CheckConfigurationSettings] No changes detected (304 Not Modified)"); + } else { + System.out.printf("[CheckConfigurationSettings] Changes detected. New ETag: %s%n", + page.getHeaders().getValue(HttpHeaderName.ETAG)); + } + } + + // Update the setting to simulate a change + setting = client.setConfigurationSetting(key, null, "new-value"); + System.out.printf("[SetConfigurationSetting] Updated Key: %s, Value: %s%n", setting.getKey(), setting.getValue()); + + // Check again with the same cached ETags - changes should be detected + conditionalResult = client.checkConfigurationSettings(conditionalSelector); + boolean hasChanges = false; + for (PagedResponse page : conditionalResult.iterableByPage()) { + if (page.getStatusCode() == 304) { + System.out.println("[CheckConfigurationSettings] No changes detected (304 Not Modified)"); + } else { + System.out.printf("[CheckConfigurationSettings] Changes detected. New ETag: %s%n", + page.getHeaders().getValue(HttpHeaderName.ETAG)); + hasChanges = true; + } + } + + // Fetch full data only if changes were detected + if (hasChanges) { + PagedIterable fullResult = client.listConfigurationSettings(selector); + for (ConfigurationSetting retrievedSetting : fullResult) { + System.out.printf("[ListConfigurationSettings] Key: %s, Value: %s%n", + retrievedSetting.getKey(), retrievedSetting.getValue()); + } + } + + // Clean up + setting = client.deleteConfigurationSetting(key, null); + System.out.printf("[DeleteConfigurationSetting] Key: %s, Value: %s%n", setting.getKey(), setting.getValue()); + } +} diff --git a/sdk/appconfiguration/azure-data-appconfiguration/src/test/java/com/azure/data/appconfiguration/ConfigurationAsyncClientTest.java b/sdk/appconfiguration/azure-data-appconfiguration/src/test/java/com/azure/data/appconfiguration/ConfigurationAsyncClientTest.java index f17eb6cf2cf3..d3b577359336 100644 --- a/sdk/appconfiguration/azure-data-appconfiguration/src/test/java/com/azure/data/appconfiguration/ConfigurationAsyncClientTest.java +++ b/sdk/appconfiguration/azure-data-appconfiguration/src/test/java/com/azure/data/appconfiguration/ConfigurationAsyncClientTest.java @@ -47,6 +47,7 @@ import static com.azure.data.appconfiguration.implementation.Utility.getTagsFilterInString; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -949,6 +950,75 @@ public void listConfigurationSettingsAcceptDateTime() { .verifyComplete(); } + /** + * Verifies that we can check configuration settings using HEAD requests and get page-level ETags. + */ + @Test + public void checkConfigurationSettings() { + String key = getKey(); + String key2 = getKey(); + listWithMultipleKeysRunner(key, key2, (setting, setting2) -> { + StepVerifier.create(client.addConfigurationSettingWithResponse(setting)) + .assertNext(response -> assertConfigurationEquals(setting, response)) + .verifyComplete(); + + StepVerifier.create(client.addConfigurationSettingWithResponse(setting2)) + .assertNext(response -> assertConfigurationEquals(setting2, response)) + .verifyComplete(); + + SettingSelector selector = new SettingSelector().setKeyFilter(key + "," + key2); + + // HEAD requests return empty items but pages should have ETags + StepVerifier.create(client.checkConfigurationSettings(selector).byPage()).assertNext(page -> { + assertNotNull(page.getHeaders()); + // Items should be empty since HEAD returns no body + assertTrue(page.getElements() == null || page.getElements().isEmpty()); + // Page should have an ETag header + assertNotNull(page.getHeaders().getValue(HttpHeaderName.ETAG)); + }).verifyComplete(); + + return client.listConfigurationSettings(selector).collectList().block(); + }); + } + + /** + * Verifies that we can check configuration settings using HEAD requests and detect changes + * via ETag comparison. + */ + @Test + public void checkConfigurationSettingsETagChanged() { + String key = getKey(); + ConfigurationSetting setting = new ConfigurationSetting().setKey(key).setValue("myValue"); + StepVerifier.create(client.setConfigurationSetting(setting)) + .assertNext(response -> assertConfigurationEquals(setting, response)) + .verifyComplete(); + + SettingSelector selector = new SettingSelector().setKeyFilter(key); + + // First HEAD request to get initial ETag + String initialETag = client.checkConfigurationSettings(selector) + .byPage() + .blockFirst() + .getHeaders() + .getValue(HttpHeaderName.ETAG); + assertNotNull(initialETag); + + // Modify the setting + ConfigurationSetting updatedSetting = new ConfigurationSetting().setKey(key).setValue("updatedValue"); + StepVerifier.create(client.setConfigurationSetting(updatedSetting)) + .assertNext(response -> assertConfigurationEquals(updatedSetting, response)) + .verifyComplete(); + + // Second HEAD request should give a different ETag + String updatedETag = client.checkConfigurationSettings(selector) + .byPage() + .blockFirst() + .getHeaders() + .getValue(HttpHeaderName.ETAG); + assertNotNull(updatedETag); + assertNotEquals(initialETag, updatedETag); + } + /** * Verifies that we can get all of the revisions for this ConfigurationSetting. Then verifies that we can select * specific fields. diff --git a/sdk/appconfiguration/azure-data-appconfiguration/src/test/java/com/azure/data/appconfiguration/ConfigurationClientTest.java b/sdk/appconfiguration/azure-data-appconfiguration/src/test/java/com/azure/data/appconfiguration/ConfigurationClientTest.java index f274947927ff..9f667525ad43 100644 --- a/sdk/appconfiguration/azure-data-appconfiguration/src/test/java/com/azure/data/appconfiguration/ConfigurationClientTest.java +++ b/sdk/appconfiguration/azure-data-appconfiguration/src/test/java/com/azure/data/appconfiguration/ConfigurationClientTest.java @@ -9,6 +9,7 @@ import com.azure.core.http.MatchConditions; import com.azure.core.http.policy.AddHeadersFromContextPolicy; import com.azure.core.http.rest.PagedIterable; +import com.azure.core.http.rest.PagedResponse; import com.azure.core.http.rest.Response; import com.azure.core.test.http.AssertingHttpClientBuilder; import com.azure.core.test.models.CustomMatcher; @@ -46,6 +47,7 @@ import static com.azure.data.appconfiguration.implementation.Utility.getTagsFilterInString; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -756,6 +758,66 @@ public void listConfigurationSettingsAcceptDateTime() { (client.listConfigurationSettings(options).stream().collect(Collectors.toList())).get(0)); } + /** + * Verifies that we can check configuration settings using HEAD requests and get page-level ETags. + */ + @Test + public void checkConfigurationSettings() { + String key = getKey(); + String key2 = getKey(); + listWithMultipleKeysRunner(key, key2, (setting, setting2) -> { + assertConfigurationEquals(setting, + client.addConfigurationSettingWithResponse(setting, Context.NONE).getValue()); + assertConfigurationEquals(setting2, + client.addConfigurationSettingWithResponse(setting2, Context.NONE).getValue()); + + SettingSelector selector = new SettingSelector().setKeyFilter(key + "," + key2); + PagedIterable result = client.checkConfigurationSettings(selector); + + // HEAD requests return empty items but pages should have ETags + for (PagedResponse page : result.iterableByPage()) { + assertNotNull(page.getHeaders()); + // Items should be empty since HEAD returns no body + assertTrue(page.getElements() == null || page.getElements().isEmpty()); + // Page should have an ETag header + assertNotNull(page.getHeaders().getValue(HttpHeaderName.ETAG)); + } + + return client.listConfigurationSettings(selector); + }); + } + + /** + * Verifies that we can check configuration settings using HEAD requests and detect changes + * via ETag comparison. + */ + @Test + public void checkConfigurationSettingsETagChanged() { + String key = getKey(); + ConfigurationSetting setting = new ConfigurationSetting().setKey(key).setValue("myValue"); + client.setConfigurationSetting(setting); + + SettingSelector selector = new SettingSelector().setKeyFilter(key); + + // First HEAD request to get initial ETag + String initialETag = null; + for (PagedResponse page : client.checkConfigurationSettings(selector).iterableByPage()) { + initialETag = page.getHeaders().getValue(HttpHeaderName.ETAG); + } + assertNotNull(initialETag); + + // Modify the setting + client.setConfigurationSetting(new ConfigurationSetting().setKey(key).setValue("updatedValue")); + + // Second HEAD request should give a different ETag + String updatedETag = null; + for (PagedResponse page : client.checkConfigurationSettings(selector).iterableByPage()) { + updatedETag = page.getHeaders().getValue(HttpHeaderName.ETAG); + } + assertNotNull(updatedETag); + assertNotEquals(initialETag, updatedETag); + } + /** * Verifies that we can get all of the revisions for this ConfigurationSetting. Then verifies that we can select * specific fields.