View Javadoc
1   // Copyright (c) Microsoft Corporation. All rights reserved.
2   // Licensed under the MIT License.
3   
4   package com.azure.data.appconfiguration;
5   
6   import com.azure.core.implementation.annotation.ReturnType;
7   import com.azure.core.implementation.annotation.ServiceClient;
8   import com.azure.core.implementation.annotation.ServiceMethod;
9   import com.azure.data.appconfiguration.credentials.ConfigurationClientCredentials;
10  import com.azure.data.appconfiguration.models.ConfigurationSetting;
11  import com.azure.data.appconfiguration.models.SettingFields;
12  import com.azure.data.appconfiguration.models.SettingSelector;
13  import com.azure.core.exception.HttpResponseException;
14  import com.azure.core.exception.ResourceModifiedException;
15  import com.azure.core.exception.ResourceNotFoundException;
16  import com.azure.core.http.HttpPipeline;
17  import com.azure.core.http.rest.PagedFlux;
18  import com.azure.core.http.rest.PagedResponse;
19  import com.azure.core.http.rest.Response;
20  import com.azure.core.implementation.RestProxy;
21  import com.azure.core.util.logging.ClientLogger;
22  import com.azure.core.implementation.util.ImplUtils;
23  import com.azure.core.util.Context;
24  import org.reactivestreams.Publisher;
25  import reactor.core.publisher.Flux;
26  import reactor.core.publisher.Mono;
27  
28  import java.net.URL;
29  import java.util.Objects;
30  
31  import static com.azure.core.implementation.util.FluxUtil.withContext;
32  
33  /**
34   * This class provides a client that contains all the operations for {@link ConfigurationSetting ConfigurationSettings}
35   * in Azure App Configuration Store. Operations allowed by the client are adding, retrieving, updating, and deleting
36   * ConfigurationSettings, and listing settings or revision of a setting based on a {@link SettingSelector filter}.
37   *
38   * <p><strong>Instantiating an Asynchronous Configuration Client</strong></p>
39   *
40   * {@codesnippet com.azure.data.applicationconfig.async.configurationclient.instantiation}
41   *
42   * <p>View {@link ConfigurationClientBuilder this} for additional ways to construct the client.</p>
43   *
44   * @see ConfigurationClientBuilder
45   * @see ConfigurationClientCredentials
46   */
47  @ServiceClient(builder = ConfigurationClientBuilder.class, isAsync = true, serviceInterfaces = ConfigurationService.class)
48  public final class ConfigurationAsyncClient {
49      private final ClientLoggerLogger.html#ClientLogger">ClientLogger logger = new ClientLogger(ConfigurationAsyncClient.class);
50  
51      private static final String ETAG_ANY = "*";
52      private static final String RANGE_QUERY = "items=%s";
53  
54      private final String serviceEndpoint;
55      private final ConfigurationService service;
56  
57      /**
58       * Creates a ConfigurationAsyncClient that sends requests to the configuration service at {@code serviceEndpoint}.
59       * Each service call goes through the {@code pipeline}.
60       *
61       * @param serviceEndpoint URL for the App Configuration service.
62       * @param pipeline HttpPipeline that the HTTP requests and responses flow through.
63       */
64      ConfigurationAsyncClient(URL serviceEndpoint, HttpPipeline pipeline) {
65          this.service = RestProxy.create(ConfigurationService.class, pipeline);
66          this.serviceEndpoint = serviceEndpoint.toString();
67      }
68  
69      /**
70       * Adds a configuration value in the service if that key does not exist.
71       *
72       * <p><strong>Code Samples</strong></p>
73       *
74       * <p>Add a setting with the key "prodDBConnection" and value "db_connection".</p>
75       *
76       * {@codesnippet com.azure.data.appconfiguration.configurationasyncclient.addSetting#string-string}
77       *
78       * @param key The key of the configuration setting to add.
79       * @param value The value associated with this configuration setting key.
80       * @return The {@link ConfigurationSetting} that was created, if a key collision occurs or the key
81       * is an invalid value (which will also throw HttpResponseException described below).
82       * @throws IllegalArgumentException If {@code key} is {@code null}.
83       * @throws ResourceModifiedException If a ConfigurationSetting with the same key exists.
84       * @throws HttpResponseException If {@code key} is an empty string.
85       */
86      @ServiceMethod(returns = ReturnType.SINGLE)
87      public Mono<ConfigurationSetting> addSetting(String key, String value) {
88          return withContext(
89              context -> addSetting(new ConfigurationSetting().key(key).value(value), context))
90              .flatMap(response -> Mono.justOrEmpty(response.value()));
91      }
92  
93      /**
94       * Adds a configuration value in the service if that key and label does not exist. The label value of the
95       * ConfigurationSetting is optional.
96       *
97       * <p><strong>Code Samples</strong></p>
98       *
99       * <p>Add a setting with the key "prodDBConnection", label "westUS", and value "db_connection".</p>
100      *
101      * {@codesnippet com.azure.data.appconfiguration.configurationasyncclient.addSetting#ConfigurationSetting}
102      *
103      * @param setting The setting to add to the configuration service.
104      * @return The {@link ConfigurationSetting} that was created, if a key collision occurs or the key
105      * is an invalid value (which will also throw HttpResponseException described below).
106      * @throws NullPointerException If {@code setting} is {@code null}.
107      * @throws IllegalArgumentException If {@link ConfigurationSetting#key() key} is {@code null}.
108      * @throws ResourceModifiedException If a ConfigurationSetting with the same key and label exists.
109      * @throws HttpResponseException If {@code key} is an empty string.
110      */
111     @ServiceMethod(returns = ReturnType.SINGLE)
112     public Mono<ConfigurationSetting> addSetting(ConfigurationSetting setting) {
113         return withContext(context -> addSetting(setting, context))
114             .flatMap(response -> Mono.justOrEmpty(response.value()));
115     }
116 
117     /**
118      * Adds a configuration value in the service if that key and label does not exist. The label value of the
119      * ConfigurationSetting is optional.
120      *
121      * <p><strong>Code Samples</strong></p>
122      *
123      * <p>Add a setting with the key "prodDBConnection", label "westUS", and value "db_connection".</p>
124      *
125      * {@codesnippet com.azure.data.appconfiguration.configurationasyncclient.addSettingWithResponse#ConfigurationSetting}
126      *
127      * @param setting The setting to add to the configuration service.
128      * @return A REST response containing the {@link ConfigurationSetting} that was created, if a key collision occurs or the key
129      * is an invalid value (which will also throw HttpResponseException described below).
130      * @throws NullPointerException If {@code setting} is {@code null}.
131      * @throws IllegalArgumentException If {@link ConfigurationSetting#key() key} is {@code null}.
132      * @throws ResourceModifiedException If a ConfigurationSetting with the same key and label exists.
133      * @throws HttpResponseException If {@code key} is an empty string.
134      */
135     @ServiceMethod(returns = ReturnType.SINGLE)
136     public Mono<Response<ConfigurationSetting>> addSettingWithResponse(ConfigurationSetting setting) {
137         return withContext(context -> addSetting(setting, context));
138     }
139 
140     Mono<Response<ConfigurationSetting>> addSetting(ConfigurationSetting setting, Context context) {
141         // Validate that setting and key is not null. The key is used in the service URL so it cannot be null.
142         validateSetting(setting);
143 
144         // This service method call is similar to setSetting except we're passing If-Not-Match = "*". If the service
145         // finds any existing configuration settings, then its e-tag will match and the service will return an error.
146         return service.setKey(serviceEndpoint, setting.key(), setting.label(), setting, null, getETagValue(ETAG_ANY), context)
147             .doOnRequest(ignoredValue -> logger.info("Adding ConfigurationSetting - {}", setting))
148             .doOnSuccess(response -> logger.info("Added ConfigurationSetting - {}", response.value()))
149             .onErrorMap(ConfigurationAsyncClient::addSettingExceptionMapper)
150             .doOnError(error -> logger.warning("Failed to add ConfigurationSetting - {}", setting, error));
151     }
152 
153     /**
154      * Creates or updates a configuration value in the service with the given key.
155      *
156      * <p><strong>Code Samples</strong></p>
157      *
158      * <p>Add a setting with the key "prodDBConnection" and value "db_connection".</p>
159      *
160      * {@codesnippet com.azure.data.appconfiguration.configurationasyncclient.setSettingWithResponse#ConfigurationSetting}
161      *
162      * @param key The key of the configuration setting to create or update.
163      * @param value The value of this configuration setting.
164      * @return The {@link ConfigurationSetting} that was created or updated, if the key is an invalid
165      * value (which will also throw HttpResponseException described below).
166      * @throws IllegalArgumentException If {@code key} is {@code null}.
167      * @throws ResourceModifiedException If the setting exists and is locked.
168      * @throws HttpResponseException If {@code key} is an empty string.
169      */
170     @ServiceMethod(returns = ReturnType.SINGLE)
171     public Mono<ConfigurationSetting> setSetting(String key, String value) {
172         return withContext(
173             context -> setSetting(new ConfigurationSetting().key(key).value(value), context))
174             .flatMap(response -> Mono.justOrEmpty(response.value()));
175     }
176 
177     /**
178      * Creates or updates a configuration value in the service. Partial updates are not supported and the entire
179      * configuration setting is updated.
180      *
181      * If {@link ConfigurationSetting#etag() etag} is specified, the configuration value is updated if the current
182      * setting's etag matches. If the etag's value is equal to the wildcard character ({@code "*"}), the setting
183      * will always be updated.
184      *
185      * <p><strong>Code Samples</strong></p>
186      *
187      * <p>Add a setting with the key "prodDBConnection", label "westUS", and value "db_connection".</p>
188      *
189      * {@codesnippet com.azure.data.appconfiguration.configurationasyncclient.setSetting#ConfigurationSetting}
190      *
191      * @param setting The configuration setting to create or update.
192      * @return The {@link ConfigurationSetting} that was created or updated, if the key is an invalid
193      * value, the setting is locked, or an etag was provided but does not match the service's current etag value (which
194      * will also throw HttpResponseException described below).
195      * @throws NullPointerException If {@code setting} is {@code null}.
196      * @throws IllegalArgumentException If {@link ConfigurationSetting#key() key} is {@code null}.
197      * @throws ResourceModifiedException If the {@link ConfigurationSetting#etag() etag} was specified, is not the
198      * wildcard character, and the current configuration value's etag does not match, or the
199      * setting exists and is locked.
200      * @throws HttpResponseException If {@code key} is an empty string.
201      */
202     @ServiceMethod(returns = ReturnType.SINGLE)
203     public Mono<ConfigurationSetting> setSetting(ConfigurationSetting setting) {
204         return withContext(context -> setSetting(setting, context))
205             .flatMap(response -> Mono.justOrEmpty(response.value()));
206     }
207 
208     /**
209      * Creates or updates a configuration value in the service. Partial updates are not supported and the entire
210      * configuration setting is updated.
211      *
212      * If {@link ConfigurationSetting#etag() etag} is specified, the configuration value is updated if the current
213      * setting's etag matches. If the etag's value is equal to the wildcard character ({@code "*"}), the setting
214      * will always be updated.
215      *
216      * <p><strong>Code Samples</strong></p>
217      *
218      * <p>Add a setting with the key "prodDBConnection", label "westUS", and value "db_connection".</p>
219      *
220      * {@codesnippet com.azure.data.appconfiguration.configurationasyncclient.setSettingWithResponse#ConfigurationSetting}
221      *
222      * @param setting The configuration setting to create or update.
223      * @return A REST response containing the {@link ConfigurationSetting} that was created or updated, if the key is an invalid
224      * value, the setting is locked, or an etag was provided but does not match the service's current etag value (which
225      * will also throw HttpResponseException described below).
226      * @throws NullPointerException If {@code setting} is {@code null}.
227      * @throws IllegalArgumentException If {@link ConfigurationSetting#key() key} is {@code null}.
228      * @throws ResourceModifiedException If the {@link ConfigurationSetting#etag() etag} was specified, is not the
229      * wildcard character, and the current configuration value's etag does not match, or the
230      * setting exists and is locked.
231      * @throws HttpResponseException If {@code key} is an empty string.
232      */
233     @ServiceMethod(returns = ReturnType.SINGLE)
234     public Mono<Response<ConfigurationSetting>> setSettingWithResponse(ConfigurationSetting setting) {
235         return withContext(context -> setSetting(setting, context));
236     }
237 
238     Mono<Response<ConfigurationSetting>> setSetting(ConfigurationSetting setting, Context context) {
239         // Validate that setting and key is not null. The key is used in the service URL so it cannot be null.
240         validateSetting(setting);
241 
242         // This service method call is similar to addSetting except it will create or update a configuration setting.
243         // If the user provides an etag value, it is passed in as If-Match = "{etag value}". If the current value in the
244         // service has a matching etag then it matches, then its value is updated with what the user passed in.
245         // Otherwise, the service throws an exception because the current configuration value was updated and we have an
246         // old value locally.
247         // If no etag value was passed in, then the value is always added or updated.
248         return service.setKey(serviceEndpoint, setting.key(), setting.label(), setting, getETagValue(setting.etag()), null, context)
249             .doOnRequest(ignoredValue -> logger.info("Setting ConfigurationSetting - {}", setting))
250             .doOnSuccess(response -> logger.info("Set ConfigurationSetting - {}", response.value()))
251             .doOnError(error -> logger.warning("Failed to set ConfigurationSetting - {}", setting, error));
252     }
253 
254     /**
255      * Updates an existing configuration value in the service with the given key. The setting must already exist.
256      *
257      *
258      * ><strong>Code Samples</strong></p>
259      *
260      * <p>Update a setting with the key "prodDBConnection" to have the value "updated_db_connection".</p>
261      *
262      * {@codesnippet com.azure.data.appconfiguration.configurationasyncclient.updateSetting#string-string}
263      *
264      * @param key The key of the configuration setting to update.
265      * @param value The updated value of this configuration setting.
266      * @return The {@link ConfigurationSetting} that was updated, if the configuration value does not
267      * exist, is locked, or the key is an invalid value (which will also throw HttpResponseException described below).
268      * @throws IllegalArgumentException If {@code key} is {@code null}.
269      * @throws HttpResponseException If a ConfigurationSetting with the key does not exist or the configuration value
270      * is locked.
271      * @throws HttpResponseException If {@code key} is an empty string.
272      */
273     @ServiceMethod(returns = ReturnType.SINGLE)
274     public Mono<ConfigurationSetting> updateSetting(String key, String value) {
275         return withContext(
276             context -> updateSetting(new ConfigurationSetting().key(key).value(value), context))
277             .flatMap(response -> Mono.justOrEmpty(response.value()));
278     }
279 
280     /**
281      * Updates an existing configuration value in the service. The setting must already exist. Partial updates are not
282      * supported, the entire configuration value is replaced.
283      *
284      * If {@link ConfigurationSetting#etag() etag} is specified, the configuration value is only updated if it matches.
285      *
286      * <p><strong>Code Samples</strong></p>
287      *
288      * <p>Update the setting with the key-label pair "prodDBConnection"-"westUS" to have the value "updated_db_connection".</p>
289      *
290      * {@codesnippet com.azure.data.appconfiguration.configurationasyncclient.updateSetting#ConfigurationSetting}
291      *
292      * @param setting The setting to add or update in the service.
293      * @return The {@link ConfigurationSetting} that was updated, if the configuration value does not
294      * exist, is locked, or the key is an invalid value (which will also throw HttpResponseException described below).
295      * @throws NullPointerException If {@code setting} is {@code null}.
296      * @throws IllegalArgumentException If {@link ConfigurationSetting#key() key} is {@code null}.
297      * @throws ResourceModifiedException If a ConfigurationSetting with the same key and label does not
298      * exist, the setting is locked, or {@link ConfigurationSetting#etag() etag} is specified but does not match
299      * the current value.
300      * @throws HttpResponseException If {@code key} is an empty string.
301      */
302     @ServiceMethod(returns = ReturnType.SINGLE)
303     public Mono<ConfigurationSetting> updateSetting(ConfigurationSetting setting) {
304         return withContext(context -> updateSetting(setting, context))
305             .flatMap(response -> Mono.justOrEmpty(response.value()));
306     }
307 
308     /**
309      * Updates an existing configuration value in the service. The setting must already exist. Partial updates are not
310      * supported, the entire configuration value is replaced.
311      *
312      * If {@link ConfigurationSetting#etag() etag} is specified, the configuration value is only updated if it matches.
313      *
314      * <p><strong>Code Samples</strong></p>
315      *
316      * <p>Update the setting with the key-label pair "prodDBConnection"-"westUS" to have the value "updated_db_connection".</p>
317      *
318      * {@codesnippet com.azure.data.appconfiguration.configurationasyncclient.updateSettingWithResponse#ConfigurationSetting}
319      *
320      * @param setting The setting to add or update in the service.
321      * @return A REST response containing the {@link ConfigurationSetting} that was updated, if the configuration value does not
322      * exist, is locked, or the key is an invalid value (which will also throw HttpResponseException described below).
323      * @throws NullPointerException If {@code setting} is {@code null}.
324      * @throws IllegalArgumentException If {@link ConfigurationSetting#key() key} is {@code null}.
325      * @throws ResourceModifiedException If a ConfigurationSetting with the same key and label does not
326      * exist, the setting is locked, or {@link ConfigurationSetting#etag() etag} is specified but does not match
327      * the current value.
328      * @throws HttpResponseException If {@code key} is an empty string.
329      */
330     @ServiceMethod(returns = ReturnType.SINGLE)
331     public Mono<Response<ConfigurationSetting>> updateSettingWithResponse(ConfigurationSetting setting) {
332         return withContext(context -> updateSetting(setting, context));
333     }
334 
335     Mono<Response<ConfigurationSetting>> updateSetting(ConfigurationSetting setting, Context context) {
336         // Validate that setting and key is not null. The key is used in the service URL so it cannot be null.
337         validateSetting(setting);
338 
339         String etag = setting.etag() == null ? ETAG_ANY : setting.etag();
340 
341         return service.setKey(serviceEndpoint, setting.key(), setting.label(), setting, getETagValue(etag), null, context)
342             .doOnRequest(ignoredValue -> logger.info("Updating ConfigurationSetting - {}", setting))
343             .doOnSuccess(response -> logger.info("Updated ConfigurationSetting - {}", response.value()))
344             .doOnError(error -> logger.warning("Failed to update ConfigurationSetting - {}", setting, error));
345     }
346 
347     /**
348      * Attempts to get a ConfigurationSetting that matches the {@code key}.
349      *
350      * <p><strong>Code Samples</strong></p>
351      *
352      * <p>Retrieve the setting with the key "prodDBConnection".</p>
353      *
354      * {@codesnippet com.azure.data.appconfiguration.configurationasyncclient.getSetting#string}
355      *
356      * @param key The key of the setting to retrieve.
357      * @return The {@link ConfigurationSetting} stored in the service, if the configuration value does
358      * not exist or the key is an invalid value (which will also throw HttpResponseException described below).
359      * @throws IllegalArgumentException If {@code key} is {@code null}.
360      * @throws ResourceNotFoundException If a ConfigurationSetting with {@code key} does not exist.
361      * @throws HttpResponseException If {@code key} is an empty string.
362      */
363     @ServiceMethod(returns = ReturnType.SINGLE)
364     public Mono<ConfigurationSetting> getSetting(String key) {
365         return withContext(
366             context -> getSetting(new ConfigurationSetting().key(key), context))
367             .flatMap(response -> Mono.justOrEmpty(response.value()));
368     }
369 
370     /**
371      * Attempts to get the ConfigurationSetting given the {@code key}, optional {@code label}.
372      *
373      * <p><strong>Code Samples</strong></p>
374      *
375      * <p>Retrieve the setting with the key-label "prodDBConnection"-"westUS".</p>
376      *
377      * {@codesnippet com.azure.data.appconfiguration.configurationasyncclient.getSetting#ConfigurationSetting}
378      *
379      * @param setting The setting to retrieve based on its key and optional label combination.
380      * @return The {@link ConfigurationSetting} stored in the service, if the configuration value does
381      * not exist or the key is an invalid value (which will also throw HttpResponseException described below).
382      * @throws NullPointerException If {@code setting} is {@code null}.
383      * @throws IllegalArgumentException If {@link ConfigurationSetting#key() key} is {@code null}.
384      * @throws ResourceNotFoundException If a ConfigurationSetting with the same key and label does not exist.
385      * @throws HttpResponseException If the {@code} key is an empty string.
386      */
387     @ServiceMethod(returns = ReturnType.SINGLE)
388     public Mono<ConfigurationSetting> getSetting(ConfigurationSetting setting) {
389         return withContext(context -> getSetting(setting, context))
390             .flatMap(response -> Mono.justOrEmpty(response.value()));
391     }
392 
393     /**
394      * Attempts to get the ConfigurationSetting given the {@code key}, optional {@code label}.
395      *
396      * <p><strong>Code Samples</strong></p>
397      *
398      * <p>Retrieve the setting with the key-label "prodDBConnection"-"westUS".</p>
399      *
400      * {@codesnippet com.azure.data.appconfiguration.configurationasyncclient.getSettingWithResponse#ConfigurationSetting}
401      *
402      * @param setting The setting to retrieve based on its key and optional label combination.
403      * @return A REST response containing the {@link ConfigurationSetting} stored in the service, if the configuration value does
404      * not exist or the key is an invalid value (which will also throw HttpResponseException described below).
405      * @throws NullPointerException If {@code setting} is {@code null}.
406      * @throws IllegalArgumentException If {@link ConfigurationSetting#key() key} is {@code null}.
407      * @throws ResourceNotFoundException If a ConfigurationSetting with the same key and label does not exist.
408      * @throws HttpResponseException If the {@code} key is an empty string.
409      */
410     @ServiceMethod(returns = ReturnType.SINGLE)
411     public Mono<Response<ConfigurationSetting>> getSettingWithResponse(ConfigurationSetting setting) {
412         return withContext(context -> getSetting(setting, context));
413     }
414 
415     Mono<Response<ConfigurationSetting>> getSetting(ConfigurationSetting setting, Context context) {
416         // Validate that setting and key is not null. The key is used in the service URL so it cannot be null.
417         validateSetting(setting);
418 
419         return service.getKeyValue(serviceEndpoint, setting.key(), setting.label(), null, null, null, null, context)
420             .doOnRequest(ignoredValue -> logger.info("Retrieving ConfigurationSetting - {}", setting))
421             .doOnSuccess(response -> logger.info("Retrieved ConfigurationSetting - {}", response.value()))
422             .doOnError(error -> logger.warning("Failed to get ConfigurationSetting - {}", setting, error));
423     }
424 
425     /**
426      * Deletes the ConfigurationSetting with a matching {@code key}.
427      *
428      * <p><strong>Code Samples</strong></p>
429      *
430      * <p>Delete the setting with the key "prodDBConnection".</p>
431      *
432      * {@codesnippet com.azure.data.appconfiguration.configurationasyncclient.deleteSetting#string}
433      *
434      * @param key The key of the setting to delete.
435      * @return The deleted ConfigurationSetting or {@code null} if it didn't exist. {@code null} is also returned if
436      * the {@code key} is an invalid value (which will also throw HttpResponseException described below).
437      * @throws IllegalArgumentException If {@code key} is {@code null}.
438      * @throws ResourceModifiedException If the ConfigurationSetting is locked.
439      * @throws HttpResponseException If {@code key} is an empty string.
440      */
441     @ServiceMethod(returns = ReturnType.SINGLE)
442     public Mono<ConfigurationSetting> deleteSetting(String key) {
443         return withContext(
444             context -> deleteSetting(new ConfigurationSetting().key(key), context))
445             .flatMap(response -> Mono.justOrEmpty(response.value()));
446     }
447 
448     /**
449      * Deletes the {@link ConfigurationSetting} with a matching key, along with the given label and etag.
450      *
451      * If {@link ConfigurationSetting#etag() etag} is specified and is not the wildcard character ({@code "*"}),
452      * then the setting is <b>only</b> deleted if the etag matches the current etag; this means that no one has updated
453      * the ConfigurationSetting yet.
454      *
455      * <p><strong>Code Samples</strong></p>
456      *
457      * <p>Delete the setting with the key-label "prodDBConnection"-"westUS".</p>
458      *
459      * {@codesnippet com.azure.data.appconfiguration.configurationasyncclient.deleteSetting#ConfigurationSetting}
460      *
461      * @param setting The ConfigurationSetting to delete.
462      * @return The deleted ConfigurationSetting or {@code null} if didn't exist. {@code null} is also returned if
463      * the {@code key} is an invalid value or {@link ConfigurationSetting#etag() etag} is set but does not match the
464      * current etag (which will also throw HttpResponseException described below).
465      * @throws IllegalArgumentException If {@link ConfigurationSetting#key() key} is {@code null}.
466      * @throws NullPointerException When {@code setting} is {@code null}.
467      * @throws ResourceModifiedException If the ConfigurationSetting is locked.
468      * @throws ResourceNotFoundException If {@link ConfigurationSetting#etag() etag} is specified, not the wildcard
469      * character, and does not match the current etag value.
470      * @throws HttpResponseException If {@code key} is an empty string.
471      */
472     @ServiceMethod(returns = ReturnType.SINGLE)
473     public Mono<ConfigurationSetting> deleteSetting(ConfigurationSetting setting) {
474         return withContext(context -> deleteSetting(setting, context))
475             .flatMap(response -> Mono.justOrEmpty(response.value()));
476     }
477 
478     /**
479      * Deletes the {@link ConfigurationSetting} with a matching key, along with the given label and etag.
480      *
481      * If {@link ConfigurationSetting#etag() etag} is specified and is not the wildcard character ({@code "*"}),
482      * then the setting is <b>only</b> deleted if the etag matches the current etag; this means that no one has updated
483      * the ConfigurationSetting yet.
484      *
485      * <p><strong>Code Samples</strong></p>
486      *
487      * <p>Delete the setting with the key-label "prodDBConnection"-"westUS".</p>
488      *
489      * {@codesnippet com.azure.data.appconfiguration.configurationasyncclient.deleteSettingWithResponse#ConfigurationSetting}
490      *
491      * @param setting The ConfigurationSetting to delete.
492      * @return A REST response containing the deleted ConfigurationSetting or {@code null} if didn't exist. {@code null} is also returned if
493      * the {@code key} is an invalid value or {@link ConfigurationSetting#etag() etag} is set but does not match the
494      * current etag (which will also throw HttpResponseException described below).
495      * @throws IllegalArgumentException If {@link ConfigurationSetting#key() key} is {@code null}.
496      * @throws NullPointerException When {@code setting} is {@code null}.
497      * @throws ResourceModifiedException If the ConfigurationSetting is locked.
498      * @throws ResourceNotFoundException If {@link ConfigurationSetting#etag() etag} is specified, not the wildcard
499      * character, and does not match the current etag value.
500      * @throws HttpResponseException If {@code key} is an empty string.
501      */
502     @ServiceMethod(returns = ReturnType.SINGLE)
503     public Mono<Response<ConfigurationSetting>> deleteSettingWithResponse(ConfigurationSetting setting) {
504         return withContext(context -> deleteSetting(setting, context));
505     }
506 
507     Mono<Response<ConfigurationSetting>> deleteSetting(ConfigurationSetting setting, Context context) {
508         // Validate that setting and key is not null. The key is used in the service URL so it cannot be null.
509         validateSetting(setting);
510 
511         return service.delete(serviceEndpoint, setting.key(), setting.label(), getETagValue(setting.etag()), null, context)
512             .doOnRequest(ignoredValue -> logger.info("Deleting ConfigurationSetting - {}", setting))
513             .doOnSuccess(response -> logger.info("Deleted ConfigurationSetting - {}", response.value()))
514             .doOnError(error -> logger.warning("Failed to delete ConfigurationSetting - {}", setting, error));
515     }
516 
517     /**
518      * Fetches the configuration settings that match the {@code options}. If {@code options} is {@code null}, then all
519      * the {@link ConfigurationSetting configuration settings} are fetched with their current values.
520      *
521      * <p><strong>Code Samples</strong></p>
522      *
523      * <p>Retrieve all settings that use the key "prodDBConnection".</p>
524      *
525      * {@codesnippet com.azure.data.appconfiguration.configurationasyncclient.listsettings}
526      *
527      * @param options Optional. Options to filter configuration setting results from the service.
528      * @return A Flux of ConfigurationSettings that matches the {@code options}. If no options were provided, the Flux
529      * contains all of the current settings in the service.
530      */
531     @ServiceMethod(returns = ReturnType.COLLECTION)
532     public PagedFlux<ConfigurationSetting> listSettings(SettingSelector options) {
533         return new PagedFlux<>(() -> withContext(context -> listFirstPageSettings(options, context)),
534             continuationToken -> withContext(context -> listNextPageSettings(context, continuationToken)));
535     }
536 
537     PagedFlux<ConfigurationSetting> listSettings(SettingSelector options, Context context) {
538         return new PagedFlux<>(() -> listFirstPageSettings(options, context),
539             continuationToken -> listNextPageSettings(context, continuationToken));
540     }
541 
542     private Mono<PagedResponse<ConfigurationSetting>> listNextPageSettings(Context context, String continuationToken) {
543         if (continuationToken == null || continuationToken.isEmpty()) {
544             return Mono.empty();
545         }
546 
547         return service.listKeyValues(serviceEndpoint, continuationToken, context)
548             .doOnRequest(ignoredValue -> logger.info("Retrieving the next listing page - Page {}", continuationToken))
549             .doOnSuccess(response -> logger.info("Retrieved the next listing page - Page {}", continuationToken))
550             .doOnError(error -> logger.warning("Failed to retrieve the next listing page - Page {}", continuationToken,
551                 error));
552     }
553 
554     private Mono<PagedResponse<ConfigurationSetting>> listFirstPageSettings(SettingSelector options, Context context) {
555         if (options == null) {
556             return service.listKeyValues(serviceEndpoint, null, null, null, null, context)
557                 .doOnRequest(ignoredValue -> logger.info("Listing all ConfigurationSettings"))
558                 .doOnSuccess(response -> logger.info("Listed all ConfigurationSettings"))
559                 .doOnError(error -> logger.warning("Failed to list all ConfigurationSetting", error));
560         }
561 
562         String fields = ImplUtils.arrayToString(options.fields(), SettingFields::toStringMapper);
563         String keys = ImplUtils.arrayToString(options.keys(), key -> key);
564         String labels = ImplUtils.arrayToString(options.labels(), label -> label);
565 
566         return service.listKeyValues(serviceEndpoint, keys, labels, fields, options.acceptDateTime(), context)
567             .doOnRequest(ignoredValue -> logger.info("Listing ConfigurationSettings - {}", options))
568             .doOnSuccess(response -> logger.info("Listed ConfigurationSettings - {}", options))
569             .doOnError(error -> logger.warning("Failed to list ConfigurationSetting - {}", options, error));
570 
571     }
572 
573     /**
574      * Lists chronological/historical representation of {@link ConfigurationSetting} resource(s). Revisions are provided
575      * in descending order from their {@link ConfigurationSetting#lastModified() lastModified} date. Revisions expire
576      * after a period of time. The service maintains change history for up to 7 days.
577      *
578      * If {@code options} is {@code null}, then all the {@link ConfigurationSetting ConfigurationSettings} are fetched
579      * in their current state. Otherwise, the results returned match the parameters given in {@code options}.
580      *
581      * <p><strong>Code Samples</strong></p>
582      *
583      * <p>Retrieve all revisions of the setting that has the key "prodDBConnection".</p>
584      *
585      * {@codesnippet com.azure.data.appconfiguration.configurationasyncclient.listsettingrevisions}
586      *
587      * @param selector Optional. Used to filter configuration setting revisions from the service.
588      * @return Revisions of the ConfigurationSetting
589      */
590     @ServiceMethod(returns = ReturnType.COLLECTION)
591     public PagedFlux<ConfigurationSetting> listSettingRevisions(SettingSelector selector) {
592         return new PagedFlux<>(() ->
593             withContext(context -> listSettingRevisionsFirstPage(selector, context)),
594             continuationToken -> withContext(context -> listSettingRevisionsNextPage(continuationToken, context)));
595     }
596 
597     Mono<PagedResponse<ConfigurationSetting>> listSettingRevisionsFirstPage(SettingSelector selector, Context context) {
598         Mono<PagedResponse<ConfigurationSetting>> result;
599 
600         if (selector != null) {
601             String fields = ImplUtils.arrayToString(selector.fields(), SettingFields::toStringMapper);
602             String keys = ImplUtils.arrayToString(selector.keys(), key -> key);
603             String labels = ImplUtils.arrayToString(selector.labels(), label -> label);
604             String range = selector.range() != null ? String.format(RANGE_QUERY, selector.range()) : null;
605 
606             result = service.listKeyValueRevisions(serviceEndpoint, keys, labels, fields, selector.acceptDateTime(), range, context)
607                 .doOnRequest(ignoredValue -> logger.info("Listing ConfigurationSetting revisions - {}", selector))
608                 .doOnSuccess(response -> logger.info("Listed ConfigurationSetting revisions - {}", selector))
609                 .doOnError(error -> logger.warning("Failed to list ConfigurationSetting revisions - {}", selector, error));
610         } else {
611             result = service.listKeyValueRevisions(serviceEndpoint, null, null, null, null, null, context)
612                 .doOnRequest(ignoredValue -> logger.info("Listing ConfigurationSetting revisions"))
613                 .doOnSuccess(response -> logger.info("Listed ConfigurationSetting revisions"))
614                 .doOnError(error -> logger.warning("Failed to list all ConfigurationSetting revisions", error));
615         }
616 
617         return result;
618     }
619 
620     Mono<PagedResponse<ConfigurationSetting>> listSettingRevisionsNextPage(String nextPageLink, Context context) {
621         Mono<PagedResponse<ConfigurationSetting>> result = service.listKeyValues(serviceEndpoint, nextPageLink, context)
622             .doOnRequest(ignoredValue -> logger.info("Retrieving the next listing page - Page {}", nextPageLink))
623             .doOnSuccess(response -> logger.info("Retrieved the next listing page - Page {}", nextPageLink))
624             .doOnError(error -> logger.warning("Failed to retrieve the next listing page - Page {}", nextPageLink, error));
625         return result;
626     }
627 
628     Flux<ConfigurationSetting> listSettingRevisions(SettingSelector selector, Context context) {
629         Mono<PagedResponse<ConfigurationSetting>> result;
630 
631         if (selector != null) {
632             String fields = ImplUtils.arrayToString(selector.fields(), SettingFields::toStringMapper);
633             String keys = ImplUtils.arrayToString(selector.keys(), key -> key);
634             String labels = ImplUtils.arrayToString(selector.labels(), label -> label);
635             String range = selector.range() != null ? String.format(RANGE_QUERY, selector.range()) : null;
636 
637             result = service.listKeyValueRevisions(serviceEndpoint, keys, labels, fields, selector.acceptDateTime(), range, context)
638                 .doOnRequest(ignoredValue -> logger.info("Listing ConfigurationSetting revisions - {}", selector))
639                 .doOnSuccess(response -> logger.info("Listed ConfigurationSetting revisions - {}", selector))
640                 .doOnError(error -> logger.warning("Failed to list ConfigurationSetting revisions - {}", selector, error));
641         } else {
642             result = service.listKeyValueRevisions(serviceEndpoint, null, null, null, null, null, context)
643                 .doOnRequest(ignoredValue -> logger.info("Listing ConfigurationSetting revisions"))
644                 .doOnSuccess(response -> logger.info("Listed ConfigurationSetting revisions"))
645                 .doOnError(error -> logger.warning("Failed to list all ConfigurationSetting revisions", error));
646         }
647 
648         return result.flatMapMany(r -> extractAndFetchConfigurationSettings(r, context));
649     }
650 
651     private Flux<ConfigurationSetting> listSettings(String nextPageLink, Context context) {
652         Mono<PagedResponse<ConfigurationSetting>> result = service.listKeyValues(serviceEndpoint, nextPageLink, context)
653             .doOnRequest(ignoredValue -> logger.info("Retrieving the next listing page - Page {}", nextPageLink))
654             .doOnSuccess(response -> logger.info("Retrieved the next listing page - Page {}", nextPageLink))
655             .doOnError(error -> logger.warning("Failed to retrieve the next listing page - Page {}", nextPageLink, error));
656 
657         return result.flatMapMany(r -> extractAndFetchConfigurationSettings(r, context));
658     }
659 
660     private Publisher<ConfigurationSetting> extractAndFetchConfigurationSettings(PagedResponse<ConfigurationSetting> page, Context context) {
661         return ImplUtils.extractAndFetch(page, context, this::listSettings);
662     }
663 
664     /*
665      * Azure Configuration service requires that the etag value is surrounded in quotation marks.
666      *
667      * @param etag The etag to get the value for. If null is pass in, an empty string is returned.
668      * @return The etag surrounded by quotations. (ex. "etag")
669      */
670     private static String getETagValue(String etag) {
671         return etag == null ? "" : "\"" + etag + "\"";
672     }
673 
674     /*
675      * Ensure that setting is not null. And, key cannot be null because it is part of the service REST URL.
676      */
677     private static void validateSetting(ConfigurationSetting setting) {
678         Objects.requireNonNull(setting);
679 
680         if (setting.key() == null) {
681             throw new IllegalArgumentException("Parameter 'key' is required and cannot be null.");
682         }
683     }
684 
685     /*
686      * Remaps the exception returned from the service if it is a PRECONDITION_FAILED response. This is performed since
687      * add setting returns PRECONDITION_FAILED when the configuration already exists, all other uses of setKey return
688      * this status when the configuration doesn't exist.
689      *
690      * @param throwable Error response from the service.
691      *
692      * @return Exception remapped to a ResourceModifiedException if the throwable was a ResourceNotFoundException,
693      * otherwise the throwable is returned unmodified.
694      */
695     private static Throwable addSettingExceptionMapper(Throwable throwable) {
696         if (!(throwable instanceof ResourceNotFoundException)) {
697             return throwable;
698         }
699 
700         ResourceNotFoundExceptioneption/ResourceNotFoundException.html#ResourceNotFoundException">ResourceNotFoundException notFoundException = (ResourceNotFoundException) throwable;
701         return new ResourceModifiedException(notFoundException.getMessage(), notFoundException.response());
702     }
703 }