Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@
"AssetsRepo": "Azure/azure-sdk-assets",
"AssetsRepoPrefixPath": "java",
"TagPrefix": "java/appconfiguration/azure-data-appconfiguration",
"Tag": "java/appconfiguration/azure-data-appconfiguration_90b5086be3"
"Tag": "java/appconfiguration/azure-data-appconfiguration_2b8d5a50c6"
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,15 +31,14 @@
import com.azure.data.appconfiguration.models.ConfigurationSnapshot;
import com.azure.data.appconfiguration.models.ConfigurationSnapshotStatus;
import com.azure.data.appconfiguration.models.FeatureFlagConfigurationSetting;
import com.azure.data.appconfiguration.models.SettingLabelSelector;
import com.azure.data.appconfiguration.models.SecretReferenceConfigurationSetting;
import com.azure.data.appconfiguration.models.SettingFields;
import com.azure.data.appconfiguration.models.SettingLabel;
import com.azure.data.appconfiguration.models.SettingLabelFields;
import com.azure.data.appconfiguration.models.SettingLabelSelector;
import com.azure.data.appconfiguration.models.SettingSelector;
import com.azure.data.appconfiguration.models.SnapshotFields;
import com.azure.data.appconfiguration.models.SnapshotSelector;
import reactor.core.publisher.Mono;

import java.time.OffsetDateTime;
import java.util.List;
Expand All @@ -57,6 +56,8 @@
import static com.azure.data.appconfiguration.implementation.Utility.updateSnapshotAsync;
import static com.azure.data.appconfiguration.implementation.Utility.validateSettingAsync;

import reactor.core.publisher.Mono;

/**
* <p>This class provides a client that contains all the operations for {@link ConfigurationSetting ConfigurationSettings},
* {@link FeatureFlagConfigurationSetting FeatureFlagConfigurationSetting} or
Expand Down Expand Up @@ -1050,6 +1051,53 @@ public PagedFlux<ConfigurationSetting> 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.
*
* <p>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.</p>
*
* <p><strong>Code Samples</strong></p>
*
* <p>Check all settings that use the key "prodDBConnection".</p>
*
* <!-- src_embed com.azure.data.appconfiguration.configurationasyncclient.checkConfigurationSettings -->
* <pre>
* client.checkConfigurationSettings&#40;new SettingSelector&#40;&#41;.setKeyFilter&#40;&quot;prodDBConnection&quot;&#41;&#41;
* .contextWrite&#40;Context.of&#40;key1, value1, key2, value2&#41;&#41;
* .subscribe&#40;setting -&gt;
* System.out.printf&#40;&quot;Key: %s, Value: %s&quot;, setting.getKey&#40;&#41;, setting.getValue&#40;&#41;&#41;&#41;;
* </pre>
* <!-- end com.azure.data.appconfiguration.configurationasyncclient.checkConfigurationSettings -->
Comment thread
mrm9084 marked this conversation as resolved.
*
* @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.
*/
@ServiceMethod(returns = ReturnType.COLLECTION)
public PagedFlux<ConfigurationSetting> 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> settingFields = selector == null ? null : toSettingFieldsList(selector.getFields());
final List<MatchConditions> matchConditionsList = selector == null ? null : selector.getMatchConditions();
final List<String> 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<HttpResponseException, Mono<PagedResponse<ConfigurationSetting>>>) 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<HttpResponseException, Mono<PagedResponse<ConfigurationSetting>>>) 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
Expand All @@ -1070,7 +1118,7 @@ public PagedFlux<ConfigurationSetting> listConfigurationSettings(SettingSelector
* be the name of the snapshot.
* @return A Flux of ConfigurationSettings that matches the {@code selector}. If no options were provided, the Flux
* contains all the current settings in the service.
* @throws HttpResponseException If a client or service error occurs, such as a 404, 409, 429 or 500.
* @throws HttpResponseException If a client or service error occurs.
*/
@ServiceMethod(returns = ReturnType.COLLECTION)
public PagedFlux<ConfigurationSetting> listConfigurationSettingsForSnapshot(String snapshotName) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,11 @@
import com.azure.data.appconfiguration.models.ConfigurationSnapshot;
import com.azure.data.appconfiguration.models.ConfigurationSnapshotStatus;
import com.azure.data.appconfiguration.models.FeatureFlagConfigurationSetting;
import com.azure.data.appconfiguration.models.SettingLabelSelector;
import com.azure.data.appconfiguration.models.SecretReferenceConfigurationSetting;
import com.azure.data.appconfiguration.models.SettingFields;
import com.azure.data.appconfiguration.models.SettingLabel;
import com.azure.data.appconfiguration.models.SettingLabelFields;
import com.azure.data.appconfiguration.models.SettingLabelSelector;
import com.azure.data.appconfiguration.models.SettingSelector;
import com.azure.data.appconfiguration.models.SnapshotFields;
import com.azure.data.appconfiguration.models.SnapshotSelector;
Expand All @@ -51,7 +51,9 @@
import static com.azure.data.appconfiguration.implementation.Utility.ETAG_ANY;
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.handleHeadNotModifiedErrorToValidResponse;
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.toKeyValue;
import static com.azure.data.appconfiguration.implementation.Utility.toSettingFieldsList;
import static com.azure.data.appconfiguration.implementation.Utility.updateSnapshotSync;
Expand Down Expand Up @@ -1080,6 +1082,92 @@ public PagedIterable<ConfigurationSetting> 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.
*
* <p>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.</p>
*
* <p><strong>Code Samples</strong></p>
*
* <p>Check all settings that use the key "prodDBConnection".</p>
*
* <!-- src_embed com.azure.data.applicationconfig.configurationclient.checkConfigurationSettings#settingSelector -->
* <pre>
* SettingSelector settingSelector = new SettingSelector&#40;&#41;.setKeyFilter&#40;&quot;prodDBConnection&quot;&#41;;
* configurationClient.checkConfigurationSettings&#40;settingSelector&#41;.forEach&#40;setting -&gt; &#123;
* System.out.printf&#40;&quot;Key: %s, Value: %s&quot;, setting.getKey&#40;&#41;, setting.getValue&#40;&#41;&#41;;
* &#125;&#41;;
* </pre>
* <!-- end com.azure.data.applicationconfig.configurationclient.checkConfigurationSettings#settingSelector -->
*
* @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.
*/
@ServiceMethod(returns = ReturnType.COLLECTION)
public PagedIterable<ConfigurationSetting> 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.
*
* <p>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.</p>
*
* <p><strong>Code Samples</strong></p>
*
* <p>Check all settings that use the key "prodDBConnection".</p>
*
* <!-- src_embed com.azure.data.applicationconfig.configurationclient.checkConfigurationSettings#settingSelector-context -->
* <pre>
* SettingSelector settingSelector = new SettingSelector&#40;&#41;.setKeyFilter&#40;&quot;prodDBConnection&quot;&#41;;
* Context ctx = new Context&#40;key2, value2&#41;;
* configurationClient.checkConfigurationSettings&#40;settingSelector, ctx&#41;.forEach&#40;setting -&gt; &#123;
* System.out.printf&#40;&quot;Key: %s, Value: %s&quot;, setting.getKey&#40;&#41;, setting.getValue&#40;&#41;&#41;;
* &#125;&#41;;
* </pre>
* <!-- end com.azure.data.applicationconfig.configurationclient.checkConfigurationSettings#settingSelector-context -->
*
* @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.
*/
@ServiceMethod(returns = ReturnType.COLLECTION)
public PagedIterable<ConfigurationSetting> 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> settingFields = selector == null ? null : toSettingFieldsList(selector.getFields());
final List<MatchConditions> matchConditionsList = selector == null ? null : selector.getMatchConditions();
final List<String> 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);
}
}, 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);
}
});
}

/**
* 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
Expand Down Expand Up @@ -1589,4 +1677,5 @@ public PagedIterable<SettingLabel> listLabels(SettingLabelSelector selector, Con
public void updateSyncToken(String token) {
syncTokenPolicy.updateSyncToken(token);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -15,22 +15,27 @@
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;
import com.azure.data.appconfiguration.models.ConfigurationSetting;
import com.azure.data.appconfiguration.models.ConfigurationSnapshot;
import com.azure.data.appconfiguration.models.ConfigurationSnapshotStatus;
import com.azure.data.appconfiguration.models.SettingFields;
import reactor.core.publisher.Mono;

import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicInteger;

import reactor.core.publisher.Mono;

/**
* App Configuration Utility methods, use internally.
*/
Expand All @@ -44,6 +49,7 @@ public class Utility {
public static final String NAME = "name";
public static final String PARAMETERS = "parameters";
public static final String URI = "uri";
private static final String AFTER_TAG = "after=";

/**
* Represents any value in Etag.
Expand Down Expand Up @@ -210,4 +216,70 @@ public static List<String> getTagsFilterInString(Map<String, String> tagsFilter)
}
return tagsFilters;
}

// Parse the 'after' query parameter value from the Link header.
// Link header format: </kv?api-version=2023-10-01&$Select=&after=a2V5MTg4Cg%3D%3D>; rel="next"
Comment on lines +220 to +221
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need a new parse method for after token? Dont we already support pagination for GET requests? Can we reuse that parsing code?

Copy link
Copy Markdown
Member Author

@mrm9084 mrm9084 May 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Something about how the Java stuff is built makes this part be built into how the @Get that is used works. So no as it doesn't exist in our code.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure I follow. There is a parseNextLink method that is being used in the existing code. Why is that not enough for HEAD requests? Why do we need after token value specifically for HEAD?

private static String parseAfterParam(String linkHeader) {
String nextLink = parseNextLink(linkHeader);
if (nextLink == null) {
return null;
}
int queryStart = nextLink.indexOf('?');
if (queryStart == -1) {
return null;
}
for (String param : nextLink.substring(queryStart + 1).split("&")) {
if (param.startsWith(AFTER_TAG)) {
try {
return URLDecoder.decode(param.substring(AFTER_TAG.length()), StandardCharsets.UTF_8.name());
} catch (java.io.UnsupportedEncodingException e) {
// UTF-8 is always supported
throw new RuntimeException(e);
}
}
}
return null;
}

// Convert a HEAD response to a PagedResponse with empty items.
public static PagedResponse<ConfigurationSetting>
toHeadPagedResponse(ResponseBase<CheckKeyValuesHeaders, Void> 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<PagedResponse<ConfigurationSetting>>
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<ConfigurationSetting>
handleHeadNotModifiedErrorToValidResponse(HttpResponseException error, ClientLogger logger) {
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);
}
}
Loading