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