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}