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.classic; 018 019import static org.apache.juneau.commons.httppart.HttpPartType.*; 020import static org.apache.juneau.commons.utils.Shorts.*; 021import static org.apache.juneau.commons.utils.ThrowableUtils.*; 022 023import java.lang.reflect.*; 024import java.time.*; 025import java.util.*; 026import java.util.regex.*; 027 028import org.apache.http.*; 029import org.apache.juneau.commons.lang.*; 030import org.apache.juneau.commons.reflect.*; 031import org.apache.juneau.http.classic.header.*; 032import org.apache.juneau.marshall.*; 033import org.apache.juneau.marshall.httppart.*; 034import org.apache.juneau.marshall.oapi.*; 035import org.apache.juneau.marshall.parser.ParseException; 036import org.apache.juneau.rest.client.classic.assertion.*; 037import org.apache.juneau.bct.assertions.*; 038 039/** 040 * Represents a single header on an HTTP response. 041 * 042 * <p> 043 * An extension of an HttpClient {@link Header} that provides various support for converting the header to POJOs and 044 * other convenience methods. 045 * 046 * <h5 class='section'>See Also:</h5><ul> 047 * <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/JuneauRestClient">juneau-rest-client-classic</a> 048 * </ul> 049 */ 050@SuppressWarnings({ 051 "java:S2160", // equals() inherited from BasicHeader compares name+value; request/response fields are contextual not identity-defining 052 "resource" // request and response are owned by the RestCall that created this header; lifecycle managed externally 053}) 054public class ResponseHeader extends BasicHeader { 055 056 private static final long serialVersionUID = 1L; 057 058 static final Header NULL_HEADER = new Header() { 059 060 @Override /* Overridden from Header */ 061 public HeaderElement[] getElements() throws org.apache.http.ParseException { return new HeaderElement[0]; } 062 063 @Override /* Overridden from Header */ 064 public String getName() { return null; } 065 066 @Override /* Overridden from Header */ 067 public String getValue() { return null; } 068 }; 069 070 private final transient HeaderElement[] elements; 071 private final transient RestRequest request; 072 private final transient RestResponse response; 073 private transient HttpPartParserSession parser; 074 private transient HttpPartSchema schema; 075 076 /** 077 * Constructor. 078 * @param name The header name. 079 * @param request The request object. 080 * @param response The response object. 081 * @param header The wrapped header. Can be <jk>null</jk> (treated as an absent header with no value and no elements). 082 * @throws IllegalArgumentException If name is <jk>null</jk> or empty. 083 */ 084 public ResponseHeader(String name, RestRequest request, RestResponse response, Header header) { 085 super(name, header == null ? null : header.getValue()); 086 this.request = request; 087 this.response = response; 088 this.elements = header == null ? new HeaderElement[0] : header.getElements(); 089 parser(null); 090 } 091 092 /** 093 * Converts this header to the specified type. 094 * 095 * @param <T> The type to convert to. 096 * @param type The type to convert to. 097 * @return The converted type, or an empty {@link Optional} if the header is not present. 098 */ 099 public <T> Optional<T> as(Class<T> type) { 100 return as(request.getClassMeta(type)); 101 } 102 103 /** 104 * Converts this header to the specified type. 105 * 106 * @param <T> The type to convert to. 107 * @param type The type to convert to. 108 * @return The converted type, or an empty {@link Optional} if the header is not present. 109 */ 110 public <T> Optional<T> as(ClassMeta<T> type) { 111 try { 112 return o(parser.read(HEADER, schema, getValue(), type)); 113 } catch (ParseException e) { 114 throw rex(e, "Could not parse response header %s.", getName()); 115 } 116 } 117 118 /** 119 * Converts this header to the specified type. 120 * 121 * <p> 122 * See <a class="doclink" href="https://juneau.apache.org/docs/topics/ComplexDataTypes">Complex Data Types</a> for information on defining complex generic types of {@link Map Maps} and {@link Collection Collections}. 123 * 124 * @param <T> The type to convert to. 125 * @param type The type to convert to. 126 * @param args The type parameters. 127 * @return The converted type, or an empty {@link Optional} if the header is not present. 128 */ 129 public <T> Optional<T> as(Type type, Type...args) { 130 return as(request.getClassMeta(type, args)); 131 } 132 133 /** 134 * Same as {@link #as(Class)} but sets the value in a mutable for fluent calls. 135 * 136 * @param value The mutable to set the parsed header value in. 137 * @param <T> The type to convert to. 138 * @param type The type to convert to. 139 * @return This object. 140 */ 141 public <T> RestResponse as(Holder<T> value, Class<T> type) { 142 value.set(as(type).orElse(null)); 143 return response; 144 } 145 146 /** 147 * Same as {@link #as(ClassMeta)} but sets the value in a mutable for fluent calls. 148 * 149 * @param value The mutable to set the parsed header value in. 150 * @param <T> The type to convert to. 151 * @param type The type to convert to. 152 * @return This object. 153 */ 154 public <T> RestResponse as(Holder<T> value, ClassMeta<T> type) { 155 value.set(as(type).orElse(null)); 156 return response; 157 } 158 159 /** 160 * Same as {@link #as(Type,Type...)} but sets the value in a mutable for fluent calls. 161 * 162 * <p> 163 * See <a class="doclink" href="https://juneau.apache.org/docs/topics/ComplexDataTypes">Complex Data Types</a> for information on defining complex generic types of {@link Map Maps} and {@link Collection Collections}. 164 * 165 * @param value The mutable to set the parsed header value in. 166 * @param <T> The type to convert to. 167 * @param type The type to convert to. 168 * @param args The type parameters. 169 * @return This object. 170 */ 171 @SuppressWarnings({ 172 "unchecked" // Type erasure requires unchecked casts 173 }) 174 public <T> RestResponse as(Holder<T> value, Type type, Type...args) { 175 value.set((T)as(type, args).orElse(null)); 176 return response; 177 } 178 179 /** 180 * Returns the value of this header as a boolean. 181 * 182 * @return The value of this header as a boolean, or {@link Optional#empty()} if the header was not present. 183 */ 184 public Optional<Boolean> asBoolean() { 185 return asBooleanHeader().asBoolean(); 186 } 187 188 /** 189 * Returns the value of this header as an boolean header. 190 * 191 * @return The value of this header as an boolean header, never <jk>null</jk>. 192 */ 193 public BasicBooleanHeader asBooleanHeader() { 194 return new BasicBooleanHeader(getName(), getValue()); 195 } 196 197 /** 198 * Returns the value of this header as a list from a comma-delimited string. 199 * 200 * @return The value of this header as a list from a comma-delimited string, or {@link Optional#empty()} if the header was not present. 201 */ 202 public Optional<String[]> asCsvArray() { 203 return asCsvHeader().asArray(); 204 } 205 206 /** 207 * Returns the value of this header as a CSV array header. 208 * 209 * @return The value of this header as a CSV array header, never <jk>null</jk>. 210 */ 211 public BasicCsvHeader asCsvHeader() { 212 return new BasicCsvHeader(getName(), getValue()); 213 } 214 215 /** 216 * Returns the value of this header as a date. 217 * 218 * @return The value of this header as a date, or {@link Optional#empty()} if the header was not present. 219 */ 220 public Optional<ZonedDateTime> asDate() { 221 return asDateHeader().asZonedDateTime(); 222 } 223 224 /** 225 * Returns the value of this header as a date header. 226 * 227 * @return The value of this header as a date header, never <jk>null</jk>. 228 */ 229 public BasicDateHeader asDateHeader() { 230 return new BasicDateHeader(getName(), getValue()); 231 } 232 233 /** 234 * Returns the value of this header as an entity validator header. 235 * 236 * @return The value of this header as an entity validator array, never <jk>null</jk>. 237 */ 238 public BasicEntityTagHeader asEntityTagHeader() { 239 return new BasicEntityTagHeader(getName(), getValue()); 240 } 241 242 /** 243 * Returns the value of this header as an entity validator array header. 244 * 245 * @return The value of this header as an entity validator array header, never <jk>null</jk>. 246 */ 247 public BasicEntityTagsHeader asEntityTagsHeader() { 248 return new BasicEntityTagsHeader(getName(), getValue()); 249 } 250 251 /** 252 * Returns the value of this header as a {@link BasicHeader}. 253 * 254 * @param c The subclass of {@link BasicHeader} to instantiate. 255 * @param <T> The subclass of {@link BasicHeader} to instantiate. 256 * @return The value of this header as a string, never <jk>null</jk>. 257 */ 258 public <T extends BasicHeader> T asHeader(Class<T> c) { 259 try { 260 var ci = ClassInfo.of(c); 261 var cc = ci.getPublicConstructor(x -> x.hasParameterTypes(String.class)).orElse(null); 262 if (nn(cc)) 263 return cc.newInstance(getValue()); 264 cc = ci.getPublicConstructor(x -> x.hasParameterTypes(String.class, String.class)).orElse(null); 265 if (nn(cc)) 266 return cc.newInstance(getName(), getValue()); 267 } catch (ExecutableException e) { 268 var cause = e.getCause(); 269 if (cause instanceof Exception cause2) 270 throw toRex(cause2); 271 throw toRex(cause); 272 } catch (Exception e) { 273 throw toRex(e); 274 } 275 throw rex("Could not determine a method to construct type %s", cn(c)); 276 } 277 278 /** 279 * Returns the value of this header as an integer. 280 * 281 * @return The value of this header as an integer, or {@link Optional#empty()} if the header was not present. 282 */ 283 public Optional<Integer> asInteger() { 284 return asIntegerHeader().asInteger(); 285 } 286 287 /** 288 * Returns the value of this header as an integer header. 289 * 290 * @return The value of this header as an integer header, never <jk>null</jk>. 291 */ 292 public BasicIntegerHeader asIntegerHeader() { 293 return new BasicIntegerHeader(getName(), getValue()); 294 } 295 296 /** 297 * Returns the value of this header as a long. 298 * 299 * @return The value of this header as a long, or {@link Optional#empty()} if the header was not present. 300 */ 301 public Optional<Long> asLong() { 302 return asLongHeader().asLong(); 303 } 304 305 /** 306 * Returns the value of this header as a long header. 307 * 308 * @return The value of this header as a long header, never <jk>null</jk>. 309 */ 310 public BasicLongHeader asLongHeader() { 311 return new BasicLongHeader(getName(), getValue()); 312 } 313 314 /** 315 * Matches the specified pattern against this header value. 316 * 317 * <h5 class='section'>Example:</h5> 318 * <p class='bjava'> 319 * <jc>// Parse header using a regular expression.</jc> 320 * Matcher <jv>matcher</jv> = <jv>client</jv> 321 * .get(<jsf>URI</jsf>) 322 * .run() 323 * .getResponseHeader(<js>"Content-Type"</js>).asMatcher(Pattern.<jsm>compile</jsm>(<js>"application/(.*)"</js>)); 324 * 325 * <jk>if</jk> (<jv>matcher</jv>.matches()) { 326 * String <jv>mediaType</jv> = <jv>matcher</jv>.group(1); 327 * } 328 * </p> 329 * 330 * @param pattern The regular expression pattern to match. 331 * @return The matcher. 332 */ 333 public Matcher asMatcher(Pattern pattern) { 334 return pattern.matcher(orElse("")); 335 } 336 337 /** 338 * Matches the specified pattern against this header value. 339 * 340 * <h5 class='section'>Example:</h5> 341 * <p class='bjava'> 342 * <jc>// Parse header using a regular expression.</jc> 343 * Matcher <jv>matcher</jv> = <jv>client</jv> 344 * .get(<jsf>URI</jsf>) 345 * .run() 346 * .getHeader(<js>"Content-Type"</js>).asMatcher(<js>"application/(.*)"</js>); 347 * 348 * <jk>if</jk> (<jv>matcher</jv>.matches()) { 349 * String <jv>mediaType</jv> = <jv>matcher</jv>.group(1); 350 * } 351 * </p> 352 * 353 * @param regex The regular expression pattern to match. 354 * @return The matcher. 355 */ 356 public Matcher asMatcher(String regex) { 357 return asMatcher(regex, 0); 358 } 359 360 /** 361 * Matches the specified pattern against this header value. 362 * 363 * <h5 class='section'>Example:</h5> 364 * <p class='bjava'> 365 * <jc>// Parse header using a regular expression.</jc> 366 * Matcher <jv>matcher</jv> = <jv>client</jv> 367 * .get(<jsf>URI</jsf>) 368 * .run() 369 * .getHeader(<js>"Content-Type"</js>).asMatcher(<js>"application/(.*)"</js>, <jsf>CASE_INSENSITIVE</jsf>); 370 * 371 * <jk>if</jk> (<jv>matcher</jv>.matches()) { 372 * String <jv>mediaType</jv> = <jv>matcher</jv>.group(1); 373 * } 374 * </p> 375 * 376 * @param regex The regular expression pattern to match. 377 * @param flags Pattern match flags. See {@link Pattern#compile(String, int)}. 378 * @return The matcher. 379 */ 380 public Matcher asMatcher(String regex, int flags) { 381 return asMatcher(Pattern.compile(regex, flags)); 382 } 383 384 /** 385 * Shortcut for calling <c>assertValue().asInteger()</c>. 386 * 387 * @return A new fluent assertion. 388 */ 389 public FluentIntegerAssertion<ResponseHeader> assertInteger() { 390 return new FluentResponseHeaderAssertion<>(this, this).asInteger(); 391 } 392 393 /** 394 * Shortcut for calling <c>assertValue().asLong()</c>. 395 * 396 * @return A new fluent assertion. 397 */ 398 public FluentLongAssertion<ResponseHeader> assertLong() { 399 return new FluentResponseHeaderAssertion<>(this, this).asLong(); 400 } 401 402 /** 403 * Shortcut for calling <c>assertValue().asString()</c>. 404 * 405 * @return A new fluent assertion. 406 */ 407 public FluentStringAssertion<ResponseHeader> assertString() { 408 return new FluentResponseHeaderAssertion<>(this, this).asString(); 409 } 410 411 /** 412 * Provides the ability to perform fluent-style assertions on this response header. 413 * 414 * <h5 class='section'>Examples:</h5> 415 * <p class='bjava'> 416 * <jc>// Validates the content type header is provided.</jc> 417 * <jv>client</jv> 418 * .get(<jsf>URI</jsf>) 419 * .run() 420 * .getHeader(<js>"Content-Type"</js>).assertValue().exists(); 421 * 422 * <jc>// Validates the content type is JSON.</jc> 423 * <jv>client</jv> 424 * .get(<jsf>URI</jsf>) 425 * .run() 426 * .getHeader(<js>"Content-Type"</js>).assertValue().equals(<js>"application/json"</js>); 427 * 428 * <jc>// Validates the content type is JSON using test predicate.</jc> 429 * <jv>client</jv> 430 * .get(<jsf>URI</jsf>) 431 * .run() 432 * .getHeader(<js>"Content-Type"</js>).assertValue().is(<jv>x</jv> -> <jv>x</jv>.equals(<js>"application/json"</js>)); 433 * 434 * <jc>// Validates the content type is JSON by just checking for substring.</jc> 435 * <jv>client</jv> 436 * .get(<jsf>URI</jsf>) 437 * .run() 438 * .getHeader(<js>"Content-Type"</js>).assertValue().contains(<js>"json"</js>); 439 * 440 * <jc>// Validates the content type is JSON using regular expression.</jc> 441 * <jv>client</jv> 442 * .get(<jsf>URI</jsf>) 443 * .run() 444 * .getHeader(<js>"Content-Type"</js>).assertValue().isPattern(<js>".*json.*"</js>); 445 * 446 * <jc>// Validates the content type is JSON using case-insensitive regular expression.</jc> 447 * <jv>client</jv> 448 * .get(<jsf>URI</jsf>) 449 * .run() 450 * .getHeader(<js>"Content-Type"</js>).assertValue().isPattern(<js>".*json.*"</js>, <jsf>CASE_INSENSITIVE</jsf>); 451 * </p> 452 * 453 * <p> 454 * The assertion test returns the original response object allowing you to chain multiple requests like so: 455 * <p class='bjava'> 456 * <jc>// Validates the header and converts it to a bean.</jc> 457 * MediaType <jv>mediaType</jv> = <jv>client</jv> 458 * .get(<jsf>URI</jsf>) 459 * .run() 460 * .getHeader(<js>"Content-Type"</js>).assertValue().isNotEmpty() 461 * .getHeader(<js>"Content-Type"</js>).assertValue().isPattern(<js>".*json.*"</js>) 462 * .getHeader(<js>"Content-Type"</js>).as(MediaType.<jk>class</jk>); 463 * </p> 464 * 465 * @return A new fluent assertion object. 466 */ 467 public FluentResponseHeaderAssertion<ResponseHeader> assertValue() { 468 return new FluentResponseHeaderAssertion<>(this, this); 469 } 470 471 /** 472 * Shortcut for calling <c>assertValue().asZonedDateTime()</c>. 473 * 474 * @return A new fluent assertion. 475 */ 476 public FluentZonedDateTimeAssertion<ResponseHeader> assertZonedDateTime() { 477 return new FluentResponseHeaderAssertion<>(this, this).asZonedDateTime(); 478 } 479 480 /** 481 * Same as {@link #asString()} but sets the value in a mutable for fluent calls. 482 * 483 * @param value The mutable to set the header value in. 484 * @return This object. 485 */ 486 public RestResponse asString(Holder<String> value) { 487 value.set(orElse(null)); 488 return response; 489 } 490 491 /** 492 * Returns the value of this header as a string header. 493 * 494 * @return The value of this header as a string header, never <jk>null</jk>. 495 */ 496 public BasicStringHeader asStringHeader() { 497 return new BasicStringHeader(getName(), getValue()); 498 } 499 500 /** 501 * Returns the value of this header as a range array header. 502 * 503 * @return The value of this header as a range array header, never <jk>null</jk>. 504 */ 505 public BasicStringRangesHeader asStringRangesHeader() { 506 return new BasicStringRangesHeader(getName(), getValue()); 507 } 508 509 /** 510 * Returns the value of this header as a URI header. 511 * 512 * @return The value of this header as a URI header, never <jk>null</jk>. 513 */ 514 public BasicUriHeader asUriHeader() { 515 return new BasicUriHeader(getName(), getValue()); 516 } 517 518 /** 519 * Parses the value. 520 * 521 * @return An array of {@link HeaderElement} entries, may be empty, but is never <jk>null</jk>. 522 * @throws org.apache.http.ParseException In case of a parsing error. 523 */ 524 @Override /* Overridden from Header */ 525 public HeaderElement[] getElements() throws org.apache.http.ParseException { return cp(elements); } 526 527 /** 528 * Specifies the part parser to use for this header. 529 * 530 * <p> 531 * If not specified, uses the part parser defined on the client by calling {@link RestClient.Builder#partParser(Class)}. 532 * 533 * @param value 534 * The new part parser to use for this header. 535 * <br>If <jk>null</jk>, {@link SimplePartParser#DEFAULT_SESSION} will be used. 536 * @return This object. 537 */ 538 public ResponseHeader parser(HttpPartParserSession value) { 539 this.parser = or(value, SimplePartParser.DEFAULT_SESSION); 540 return this; 541 } 542 543 /** 544 * Returns the response that created this object. 545 * 546 * @return The response that created this object. 547 */ 548 public RestResponse response() { 549 return response; 550 } 551 552 /** 553 * Specifies the part schema for this header. 554 * 555 * <p> 556 * Used by schema-based part parsers such as {@link OpenApiParser}. 557 * 558 * @param value 559 * The part schema. 560 * @return This object. 561 */ 562 public ResponseHeader schema(HttpPartSchema value) { 563 schema = value; 564 return this; 565 } 566}