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}