001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.juneau.rest.client;
018
019import static org.apache.juneau.commons.utils.CollectionUtils.*;
020import static org.apache.juneau.commons.utils.Shorts.*;
021
022import java.io.*;
023import java.util.*;
024import java.util.function.*;
025import java.util.stream.*;
026import java.util.logging.*;
027
028import org.apache.juneau.commons.logging.*;
029import org.apache.juneau.commons.http.*;
030import org.apache.juneau.http.*;
031import org.apache.juneau.http.entity.*;
032import org.apache.juneau.http.header.*;
033import org.apache.juneau.http.part.*;
034import org.apache.juneau.http.remote.*;
035import org.apache.juneau.marshall.parser.*;
036import org.apache.juneau.marshall.serializer.*;
037import org.apache.juneau.rest.client.remote.*;
038
039/**
040 * Next-generation transport-agnostic REST client.
041 *
042 * <p>
043 * Create instances via {@link #create()} or {@link #builder()}:
044 * <p class='bjava'>
045 *    <jc>// Minimal — uses the built-in JDK HttpClient transport</jc>
046 *    RestClient <jv>client</jv> = RestClient.<jsm>create</jsm>();
047 *
048 *    <jc>// Explicit transport (e.g. for tests)</jc>
049 *    RestClient <jv>client</jv> = RestClient.<jsm>builder</jsm>()
050 *       .transport(<jv>mockTransport</jv>)
051 *       .header(<js>"Accept"</js>, <js>"application/json"</js>)
052 *       .build();
053 * </p>
054 *
055 * <p>
056 * By default, {@code RestClient} uses {@link JavaHttpTransport} (backed by the JDK's
057 * {@link java.net.http.HttpClient}), which ships built into the {@code juneau-rest-client} artifact and
058 * requires no extra dependencies on Java 11+.  Pulling in one of the optional transport modules
059 * ({@code juneau-rest-client-apache-httpclient-45}, {@code -apache-httpclient-50}, {@code -okhttp},
060 * {@code -jetty}) registers a higher-priority provider via {@link ServiceLoader} that takes
061 * over automatically.  An explicit {@link Builder#transport(HttpTransport)} call always wins.
062 *
063 * <h2 class='section'>See Also:</h2><ul>
064 *    <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/NextGenRestClient">Next-Generation REST Client</a>
065 * </ul>
066 *
067 * @since 9.2.1
068 */
069@SuppressWarnings({
070   "java:S1192", // Duplicated literals (argument/property names) read more clearly inline than as constants
071   "resource" // transport is closed in RestClient.close(); this class owns it
072})
073public final class RestClient implements Closeable {
074
075   /**
076    * Default body converters applied in order when {@link RestRequest#body(Object)} is called.
077    *
078    * <p>
079    * Custom converters registered via {@link Builder#bodyConverter(BodyConverter[])} are prepended to this list.
080    * Call {@link Builder#bodyConverters(BodyConverter[])} to replace all defaults.
081    */
082   public static final List<BodyConverter<?>> DEFAULT_BODY_CONVERTERS = List.of(
083      BodyConverter.of(HttpBody.class, TransportBody::of),
084      BodyConverter.of(InputStream.class, is -> TransportBody.of(StreamBody.of(is))),
085      BodyConverter.of(byte[].class, bytes -> TransportBody.of(ByteArrayBody.of(bytes))),
086      BodyConverter.of(File.class, file -> TransportBody.of(FileBody.of(file)))
087   );
088
089   final HttpTransport transport;
090   final List<HttpHeader> defaultHeaders;
091   final List<HttpPart> defaultQueryData;
092   final String rootUrl;
093   final List<RestCallInterceptor> interceptors;
094   final RichLogger debugLogger;
095   final RestClientDebugFormatter debugFormatter;
096   final List<BodyConverter<?>> bodyConverters;
097   final SerializerSet serializers;
098   final ParserSet parsers;
099   final Serializer defaultSerializer;
100   final Parser defaultParser;
101   final boolean allowPrivateUrls;
102   final RemoteUrlPolicy.AllowedRanges allowedAddressRanges;
103   final List<String> redirectForwardHeaders;
104
105   private RestClient(Builder builder) {
106      this.transport = reqnn("transport",
107         builder.transport != null ? builder.transport : discoverTransport());
108      this.defaultHeaders = List.copyOf(builder.defaultHeaders);
109      this.defaultQueryData = List.copyOf(builder.defaultQueryData);
110      this.rootUrl = builder.rootUrl;
111      this.interceptors = buildInterceptors(builder);
112      this.debugLogger = RichLogger.getLogger(builder.debugLoggerName != null ? builder.debugLoggerName : RestClient.class.getName());
113      this.debugFormatter = builder.debugFormatter != null ? builder.debugFormatter : new BasicRestClientDebugFormatter();
114      this.bodyConverters = List.copyOf(builder.bodyConverters);
115      this.serializers = builder.serializers;
116      this.parsers = builder.parsers;
117      this.defaultSerializer = builder.defaultSerializer;
118      this.defaultParser = builder.defaultParser;
119      this.allowPrivateUrls = builder.allowPrivateUrls || Boolean.getBoolean(RemoteUrlPolicy.ALLOW_PRIVATE_URLS_PROPERTY);
120      this.allowedAddressRanges = RemoteUrlPolicy.AllowedRanges.of(builder.allowedAddressRanges.toArray(new String[0]));
121      this.redirectForwardHeaders = List.copyOf(builder.redirectForwardHeaders);
122   }
123
124   /**
125    * Builds the effective interceptor list, prepending the built-in {@code X-Request-Id} auto-send interceptor when
126    * {@link Builder#sendRequestId(boolean)} is enabled (the default).
127    *
128    * <p>
129    * The built-in runs first so its {@code onInit} stamps the id before any caller interceptor observes the request,
130    * and it is additive with {@link Builder#interceptors(RestCallInterceptor...)} rather than replacing it.
131    */
132   private static List<RestCallInterceptor> buildInterceptors(Builder builder) {
133      if (! builder.sendRequestId)
134         return List.copyOf(builder.interceptors);
135      var l = new ArrayList<RestCallInterceptor>(builder.interceptors.size() + 1);
136      l.add(new RequestIdInterceptor());
137      l.addAll(builder.interceptors);
138      return List.copyOf(l);
139   }
140
141   /**
142    * Returns the effective builder/settings-level {@code allowPrivateUrls} opt-in for this client (the logical OR
143    * of {@link Builder#allowPrivateUrls(boolean)} and the {@value RemoteUrlPolicy#ALLOW_PRIVATE_URLS_PROPERTY}
144    * system property).
145    *
146    * <p>
147    * The fully effective per-call value additionally ORs in the {@code @Remote}/{@code @RemoteOp}
148    * {@code allowPrivateUrls} annotation attribute (see {@link Remote#allowPrivateUrls()}).
149    *
150    * @return {@code true} if this client opts into allowing private/loopback/link-local/metadata {@code @Remote} targets.
151    */
152   public boolean isAllowPrivateUrls() {
153      return allowPrivateUrls;
154   }
155
156   /**
157    * Returns the per-client special-purpose address ranges this client re-permits in the SSRF guardrail (see
158    * {@link Builder#allowAddressRanges(String...)}).  They are unioned with the
159    * {@value RemoteUrlPolicy#ALLOW_ADDRESS_RANGES_PROPERTY} system property at each check.
160    *
161    * @return The allowed ranges. Never <jk>null</jk>.
162    */
163   public RemoteUrlPolicy.AllowedRanges getAllowedAddressRanges() {
164      return allowedAddressRanges;
165   }
166
167   private static HttpTransport discoverTransport() {
168      // Lowest priority value wins; ties resolve to the first discovered provider.
169      return ServiceLoader.load(HttpTransportProvider.class).stream()
170         .map(ServiceLoader.Provider::get)
171         .filter(HttpTransportProvider::isAvailable)
172         .min(Comparator.comparingInt(HttpTransportProvider::getPriority))
173         .map(HttpTransportProvider::create)
174         .orElseGet(JavaHttpTransport::create);  // defensive fallback if META-INF/services was stripped (e.g. uber-jar shading)
175   }
176
177   /**
178    * Creates a new {@link RestClient} using auto-discovered transport.
179    *
180    * @return A new instance. Never <jk>null</jk>.
181    */
182   public static RestClient create() {
183      return builder().build();
184   }
185
186   /** Returns a new {@link Builder}. */
187   public static Builder builder() {
188      return new Builder();
189   }
190
191   /**
192    * Creates a GET request to the given URL.
193    *
194    * @param url The target URL. Must not be <jk>null</jk>.
195    * @return A new {@link RestRequest}. Never <jk>null</jk>.
196    */
197   public RestRequest get(String url) {
198      return request("GET", url);
199   }
200
201   /**
202    * Creates a POST request to the given URL.
203    *
204    * @param url The target URL. Must not be <jk>null</jk>.
205    * @return A new {@link RestRequest}. Never <jk>null</jk>.
206    */
207   public RestRequest post(String url) {
208      return request("POST", url);
209   }
210
211   /**
212    * Creates a PUT request to the given URL.
213    *
214    * @param url The target URL. Must not be <jk>null</jk>.
215    * @return A new {@link RestRequest}. Never <jk>null</jk>.
216    */
217   public RestRequest put(String url) {
218      return request("PUT", url);
219   }
220
221   /**
222    * Creates a PATCH request to the given URL.
223    *
224    * @param url The target URL. Must not be <jk>null</jk>.
225    * @return A new {@link RestRequest}. Never <jk>null</jk>.
226    */
227   public RestRequest patch(String url) {
228      return request("PATCH", url);
229   }
230
231   /**
232    * Creates a DELETE request to the given URL.
233    *
234    * @param url The target URL. Must not be <jk>null</jk>.
235    * @return A new {@link RestRequest}. Never <jk>null</jk>.
236    */
237   public RestRequest delete(String url) {
238      return request("DELETE", url);
239   }
240
241   /**
242    * Creates a HEAD request to the given URL.
243    *
244    * @param url The target URL. Must not be <jk>null</jk>.
245    * @return A new {@link RestRequest}. Never <jk>null</jk>.
246    */
247   public RestRequest head(String url) {
248      return request("HEAD", url);
249   }
250
251   /**
252    * Creates a request with the given HTTP method and URL.
253    *
254    * @param method The HTTP method. Must not be <jk>null</jk>.
255    * @param url The target URL. Must not be <jk>null</jk>.
256    * @return A new {@link RestRequest}. Never <jk>null</jk>.
257    */
258   public RestRequest request(String method, String url) {
259      reqnn("method", method);
260      reqnn("url", url);
261      var resolvedUrl = rootUrl != null && !url.contains("://") ? rootUrl + url : url;
262      return new RestRequest(this, method, resolvedUrl);
263   }
264
265   /**
266    * Returns the underlying transport used by this client.
267    *
268    * @return The transport. Never <jk>null</jk>.
269    */
270   public HttpTransport getTransport() {
271      return transport;
272   }
273
274   /**
275    * Returns the explicitly-configured default serializer used for outbound bodies when the format is not otherwise
276    * discernable.
277    *
278    * <p>
279    * Only an explicit default set via {@link Builder#defaultSerializer(Serializer)} is honored — there is no implicit
280    * JSON fallback and no lone-registered-entry fallback.  A fully-unconfigured client returns {@link Optional#empty()}.
281    *
282    * @return The configured default serializer, or {@link Optional#empty()} if none was set.
283    */
284   public Optional<Serializer> getDefaultSerializer() {
285      return o(defaultSerializer);
286   }
287
288   /**
289    * Returns the parser matching the given response {@code Content-Type}, or the explicitly-configured default parser.
290    *
291    * <p>
292    * Resolution precedence: an exact media-type match against the client's {@link ParserSet} wins; otherwise the
293    * explicitly-configured {@link Builder#defaultParser(Parser) default parser} is used; otherwise
294    * {@link Optional#empty()} is returned.  There is no implicit JSON fallback and no lone-registered-entry fallback,
295    * so a fully-unconfigured client resolves to empty (callers throw <c>415 Unsupported Media Type</c>).
296    *
297    * @param contentType The response {@code Content-Type} header value. May be <jk>null</jk>.
298    * @return The matching parser, or {@link Optional#empty()} if nothing matched and no default parser is configured.
299    */
300   public Optional<Parser> getMatchingParser(String contentType) {
301      if (parsers != null && contentType != null) {
302         var p = parsers.getParser(contentType);
303         if (p.isPresent())
304            return p;
305      }
306      return o(defaultParser);
307   }
308
309   /**
310    * Returns the registered request serializer matching the given media type, or {@link Optional#empty()} if none matches.
311    *
312    * <p>
313    * Unlike {@link #getDefaultSerializer()}, this method performs media-type-driven <i>selection</i> against the
314    * client's {@link SerializerSet} (via {@link SerializerSet#getSerializer(String)}) and returns
315    * {@link Optional#empty()} on no match so the caller can apply the locked no-match fallback (use the default
316    * serializer but still send the overridden {@code Content-Type} label).  An explicitly-configured single default
317    * serializer is also consulted when no set is registered.
318    *
319    * @param mediaType The desired request media type. May be <jk>null</jk>/empty.
320    * @return The matching serializer, or {@link Optional#empty()} if no registered serializer matches.
321    */
322   public Optional<Serializer> getSerializerForMediaType(String mediaType) {
323      if (mediaType == null || mediaType.isEmpty() || serializers == null)
324         return oe();
325      return serializers.getSerializer(mediaType);
326   }
327
328   /**
329    * Returns the registered parser matching the given media type, or {@link Optional#empty()} if none matches.
330    *
331    * <p>
332    * Used by the next-gen {@code RemoteClient} as the {@code accept} fallback parser: it is consulted only when the
333    * response {@code Content-Type} matched no registered parser (or the response was unlabeled).  Returns
334    * {@link Optional#empty()} on no match so the caller can fall back to the default parser.
335    *
336    * @param mediaType The desired parse media type. May be <jk>null</jk>/empty.
337    * @return The matching parser, or {@link Optional#empty()} if no registered parser matches.
338    */
339   public Optional<Parser> getParserForMediaType(String mediaType) {
340      if (mediaType == null || mediaType.isEmpty() || parsers == null)
341         return oe();
342      return parsers.getParser(mediaType);
343   }
344
345   /**
346    * Returns the default {@code Accept} header value advertising what this client can read.
347    *
348    * @return The default {@code Accept} header value, or <jk>null</jk> if no parsers and no default parser are
349    *    configured (no implicit {@code application/json} fallback).
350    */
351   public String getDefaultAccept() {
352      if (parsers != null) {
353         var mts = parsers.getSupportedMediaTypes();
354         if (! mts.isEmpty())
355            return mts.stream().map(MediaType::toString).collect(Collectors.joining(", "));
356      }
357      if (defaultParser != null) {
358         var mts = defaultParser.getMediaTypes();
359         if (! mts.isEmpty())
360            return mts.stream().map(MediaType::toString).collect(Collectors.joining(", "));
361      }
362      return null;
363   }
364
365   /**
366    * Creates a Java proxy for the given {@link Remote}-annotated interface.
367    *
368    * <p>
369    * Each method call on the returned proxy will be translated into an HTTP request using this client.
370    *
371    * <p class='bjava'>
372    *    UserService <jv>svc</jv> = client.remote(UserService.<jk>class</jk>);
373    *    String <jv>user</jv> = <jv>svc</jv>.getUser(<js>"42"</js>);
374    * </p>
375    *
376    * @param <T> The interface type.
377    * @param iface The interface class. Must be annotated with {@link Remote}. Must not be <jk>null</jk>.
378    * @return A proxy instance backed by this client. Never <jk>null</jk>.
379    * @throws IllegalArgumentException If {@code iface} is not an interface or not annotated with {@code @Remote}.
380    */
381   public <T> T remote(Class<T> iface) {
382      return new RemoteClient(this).create(iface);
383   }
384
385   @Override /* Closeable */
386   public void close() throws IOException {
387      transport.close();
388   }
389
390   // -----------------------------------------------------------------------------------------------------------------
391   // Builder
392   // -----------------------------------------------------------------------------------------------------------------
393
394   /**
395    * Fluent builder for {@link RestClient}.
396    *
397    * @since 9.2.1
398    */
399   public static final class Builder {
400
401      HttpTransport transport;
402      final List<HttpHeader> defaultHeaders = l();
403      final List<HttpPart> defaultQueryData = l();
404      String rootUrl;
405      final List<RestCallInterceptor> interceptors = l();
406      RestClientDebugFormatter debugFormatter;
407      String debugLoggerName;
408      List<BodyConverter<?>> bodyConverters = new ArrayList<>(DEFAULT_BODY_CONVERTERS);
409      SerializerSet serializers;
410      ParserSet parsers;
411      Serializer defaultSerializer;
412      Parser defaultParser;
413      final List<Serializer> serializerList = l();
414      final List<Parser> parserList = l();
415      boolean allowPrivateUrls;
416      final List<String> allowedAddressRanges = l();
417      final List<String> redirectForwardHeaders = l();
418      boolean sendRequestId = true;
419
420      private Builder() {}
421
422      /**
423       * Sets the HTTP transport to use.
424       *
425       * <p>
426       * If not set, {@link RestClient} discovers a transport via {@link ServiceLoader}.  When no
427       * sibling transport module is on the classpath, the built-in {@link JavaHttpTransport} (backed by the JDK's
428       * {@link java.net.http.HttpClient}) is used as the default.
429       *
430       * @param value The transport. Can be <jk>null</jk> (a transport is auto-discovered via {@link ServiceLoader}).
431       * @return This object.
432       */
433      public Builder transport(HttpTransport value) {
434         transport = value;
435         return this;
436      }
437
438      /**
439       * Sets a root URL prefix applied to all relative request URLs (those without {@code ://}).
440       *
441       * @param value The root URL (e.g. {@code "https://api.example.com/v1"}). May be <jk>null</jk>.
442       * @return This object.
443       */
444      public Builder rootUrl(String value) {
445         rootUrl = value;
446         return this;
447      }
448
449      /**
450       * Opts every {@code @Remote}/{@code @Url} call made through this client out of the default deny-private SSRF
451       * guardrail (see {@code org.apache.juneau.http.remote.RemoteUrlPolicy}), for local-dev/intranet targets that
452       * are intentionally private.
453       *
454       * <p>
455       * The effective per-call value is the logical OR of this setting, the
456       * {@value RemoteUrlPolicy#ALLOW_PRIVATE_URLS_PROPERTY} system property, and the {@code @Remote}/{@code @RemoteOp}
457       * {@code allowPrivateUrls} annotation attribute: any one of the three opting in is sufficient. The {@code http}/
458       * {@code https} scheme requirement is unaffected.
459       *
460       * @param value {@code true} to allow private/loopback/link-local/metadata {@code @Remote} targets. Default {@code false}.
461       * @return This object.
462       */
463      public Builder allowPrivateUrls(boolean value) {
464         allowPrivateUrls = value;
465         return this;
466      }
467
468      /**
469       * Re-permits, for this client only, addresses that the default SSRF guardrail denies solely because they fall in
470       * one of the special-purpose ranges <c>192.0.0.0/24</c>, <c>198.18.0.0/15</c>, <c>224.0.0.0/4</c>,
471       * <c>240.0.0.0/4</c> or <c>ff00::/8</c> (for example a lab or appliance network on <c>198.18.0.0/15</c>).
472       *
473       * <p>
474       * This is the per-client counterpart of the {@value RemoteUrlPolicy#ALLOW_ADDRESS_RANGES_PROPERTY} system
475       * property and has exactly its narrow semantics: every entry (a CIDR range or a single address, parsed as a
476       * literal with no DNS lookup) must lie entirely inside one of those five ranges, and it never re-permits
477       * loopback, link-local/metadata (<c>169.254.169.254</c>), private, CGNAT or any other denied address.  It applies
478       * to the literal-host check, the connect-time (DNS-pinned) address selection and every redirect hop of this
479       * client's {@code @Remote} calls, on every first-party transport.
480       *
481       * <p>
482       * The effective allow-list is the <b>union</b> of these ranges and the system property's ranges (both are
483       * narrow, so the union is too).  Calls accumulate.  An invalid entry is reported by {@link #build()} as an
484       * {@link IllegalArgumentException}.
485       *
486       * @param value The CIDR ranges or single addresses to allow; each element may also hold several comma- or
487       *    whitespace-separated entries.  <jk>null</jk> or empty adds nothing.
488       * @return This object.
489       */
490      public Builder allowAddressRanges(String...value) {
491         if (value != null)
492            allowedAddressRanges.addAll(Arrays.asList(value));
493         return this;
494      }
495
496      /**
497       * Adds header names that may be replayed against another origin by a redirect.
498       *
499       * <p>
500       * A request replayed against another origin (or across an {@code https} to {@code http} downgrade) keeps only the
501       * headers on {@link RedirectSecurity#forwardOnCrossOrigin()} (content negotiation and framing headers); every other
502       * caller-set header, including custom authentication headers, is removed.  Use this to forward an additional,
503       * non-sensitive header.  Calls accumulate.  Matching is case-insensitive.
504       *
505       * <h4 class='section'>Notes:</h4><ul>
506       *    <li class='note'>Applied by the Juneau-controlled redirect loop used for requests under the SSRF guardrail.  Redirect
507       *       guards that a transport installs on its own HTTP client use the built-in list only.
508       * </ul>
509       *
510       * @param value The header names. <jk>null</jk> or empty adds nothing.
511       * @return This object.
512       */
513      public Builder redirectForwardHeaders(String...value) {
514         if (value != null)
515            redirectForwardHeaders.addAll(Arrays.asList(value));
516         return this;
517      }
518
519      /**
520       * Controls the built-in {@code X-Request-Id} auto-send interceptor (enabled by default).
521       *
522       * <p>
523       * When enabled, every request that does not already carry an {@code X-Request-Id} header (case-insensitive) is
524       * stamped with a freshly minted version-7 UUID before it is sent, and the server's echoed id is captured on the
525       * response ({@link RestResponse#getRequestId()}).  Set to {@code false} to disable the interceptor entirely:
526       * no id is sent or captured, and a later {@link #interceptors(RestCallInterceptor...)} call does not resurrect
527       * it.  Callers can still set an id per request via {@link RestRequest#requestId(String)}.
528       *
529       * @param value {@code false} to disable the auto-send interceptor. Default {@code true}.
530       * @return This object.
531       * @since 10.0.0
532       */
533      public Builder sendRequestId(boolean value) {
534         sendRequestId = value;
535         return this;
536      }
537
538      /**
539       * Adds default headers sent with every request.
540       *
541       * @param value The headers to add. Must not be <jk>null</jk>.
542       * @return This object.
543       */
544      public Builder headers(HttpHeader... value) {
545         defaultHeaders.addAll(Arrays.asList(value));
546         return this;
547      }
548
549      /**
550       * Adds a default header sent with every request.
551       *
552       * @param name The header name. Must not be <jk>null</jk>.
553       * @param value The header value (eager). May be <jk>null</jk>.
554       * @return This object.
555       */
556      public Builder header(String name, String value) {
557         defaultHeaders.add(HttpHeaderBean.of(name, value));
558         return this;
559      }
560
561      /**
562       * Adds a default header with a lazy value, evaluated at each request.
563       *
564       * @param name The header name. Must not be <jk>null</jk>.
565       * @param value Supplier for the header value. Must not be <jk>null</jk>.
566       * @return This object.
567       */
568      public Builder header(String name, Supplier<String> value) {
569         defaultHeaders.add(HttpHeaderBean.of(name, value));
570         return this;
571      }
572
573      /**
574       * Adds default query parameters appended to every request URL.
575       *
576       * @param value The parts to add. Must not be <jk>null</jk>.
577       * @return This object.
578       */
579      public Builder queryData(HttpPart... value) {
580         defaultQueryData.addAll(Arrays.asList(value));
581         return this;
582      }
583
584      /**
585       * Adds a default query parameter appended to every request URL.
586       *
587       * @param name The parameter name. Must not be <jk>null</jk>.
588       * @param value The parameter value (eager). May be <jk>null</jk>.
589       * @return This object.
590       */
591      public Builder queryData(String name, String value) {
592         defaultQueryData.add(HttpPartBean.of(name, value));
593         return this;
594      }
595
596      /**
597       * Adds one or more lifecycle interceptors called before/after each request.
598       *
599       * @param value The interceptors to add. Must not be <jk>null</jk>.
600       * @return This object.
601       */
602      public Builder interceptors(RestCallInterceptor... value) {
603         interceptors.addAll(Arrays.asList(value));
604         return this;
605      }
606
607      /**
608       * Sets the formatter used for level-driven REST debug logging.
609       *
610       * <p>
611       * The formatter renders cumulative content based on the resolved debug logger level:
612       * <ul>
613       *    <li>{@link Level#INFO INFO} - basic line ({@link RestClientDebugFormatter#formatBasic(RestRequest, RestResponse)}).
614       *    <li>{@link Level#FINE FINE} - basic + headers.
615       *    <li>{@link Level#FINEST FINEST} - basic + headers + body.
616       * </ul>
617       *
618       * @param value The formatter. Can be <jk>null</jk> to restore the default formatter.
619       * @return This object.
620       */
621      public Builder debugFormatter(RestClientDebugFormatter value) {
622         debugFormatter = value;
623         return this;
624      }
625
626      /**
627       * Sets the logger name used for REST debug records.
628       *
629       * <p>
630       * If not set, defaults to {@code RestClient.class.getName()}.
631       *
632       * @param value The logger name. Can be <jk>null</jk> to use the default logger name.
633       * @return This object.
634       */
635      public Builder debugLoggerName(String value) {
636         debugLoggerName = value;
637         return this;
638      }
639
640
641      /**
642       * Prepends custom body converters to the default converter list.
643       *
644       * <p>
645       * Custom converters are checked before the defaults when {@link RestRequest#body(Object)} is called.
646       *
647       * @param value The converters to prepend. Must not be <jk>null</jk>.
648       * @return This object.
649       */
650      public Builder bodyConverter(BodyConverter<?>... value) {
651         var prepended = new ArrayList<BodyConverter<?>>(Arrays.asList(value));
652         prepended.addAll(bodyConverters);
653         bodyConverters = prepended;
654         return this;
655      }
656
657      /**
658       * Replaces the entire body converter list (including defaults).
659       *
660       * <p>
661       * Use this when you want full control over body conversion, including disabling the built-in defaults.
662       *
663       * @param value The complete replacement converter list. Must not be <jk>null</jk>.
664       * @return This object.
665       */
666      public Builder bodyConverters(BodyConverter<?>... value) {
667         bodyConverters = list(value);
668         return this;
669      }
670
671      /**
672       * Sets the serializer registry used for outbound bodies.
673       *
674       * @param value The serializer set. May be <jk>null</jk>.
675       * @return This object.
676       */
677      public Builder serializers(SerializerSet value) {
678         serializers = value;
679         return this;
680      }
681
682      /**
683       * Sets the parser registry used for inbound bodies.
684       *
685       * @param value The parser set. May be <jk>null</jk>.
686       * @return This object.
687       */
688      public Builder parsers(ParserSet value) {
689         parsers = value;
690         return this;
691      }
692
693      /**
694       * Appends serializers (built into a {@link SerializerSet} at build time if no set was supplied).
695       *
696       * @param value The serializers to append. Must not be <jk>null</jk>.
697       * @return This object.
698       */
699      public Builder serializer(Serializer... value) {
700         serializerList.addAll(Arrays.asList(value));
701         return this;
702      }
703
704      /**
705       * Appends parsers (built into a {@link ParserSet} at build time if no set was supplied).
706       *
707       * @param value The parsers to append. Must not be <jk>null</jk>.
708       * @return This object.
709       */
710      public Builder parser(Parser... value) {
711         parserList.addAll(Arrays.asList(value));
712         return this;
713      }
714
715      /**
716       * Designates the default serializer used for outbound bodies when no registered serializer matches the
717       * requested media type.
718       *
719       * <p>
720       * This is the explicit, opt-in way to restore a fallback serializer.  Without it, a request that cannot be
721       * matched to a registered serializer (and any body requiring serialization on a serializer-less client) fails
722       * rather than silently defaulting to JSON.  Set {@code defaultSerializer(JsonSerializer.DEFAULT)} to recover
723       * the pre-10.0 implicit-JSON behavior.
724       *
725       * @param value The default serializer. May be <jk>null</jk>.
726       * @return This object.
727       */
728      public Builder defaultSerializer(Serializer value) {
729         defaultSerializer = value;
730         return this;
731      }
732
733      /**
734       * Designates the default parser used when the response {@code Content-Type} is absent or matches no registered
735       * parser.
736       *
737       * <p>
738       * This is the explicit, opt-in way to restore a fallback parser.  Without it, a response whose
739       * {@code Content-Type} matches no registered parser fails with <c>415 Unsupported Media Type</c> rather than
740       * silently parsing as JSON.  Set {@code defaultParser(JsonParser.DEFAULT)} to recover the pre-10.0
741       * implicit-JSON behavior.
742       *
743       * @param value The default parser. May be <jk>null</jk>.
744       * @return This object.
745       */
746      public Builder defaultParser(Parser value) {
747         defaultParser = value;
748         return this;
749      }
750
751      /**
752       * Builds and returns the {@link RestClient}.
753       *
754       * @return A new instance. Never <jk>null</jk>.
755       */
756      public RestClient build() {
757         if (serializers == null && ! serializerList.isEmpty())
758            serializers = SerializerSet.create().add(serializerList.toArray(new Serializer[0])).build();
759         if (parsers == null && ! parserList.isEmpty())
760            parsers = ParserSet.create().add(parserList.toArray(new Parser[0])).build();
761         return new RestClient(this);
762      }
763   }
764}