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