001package com.box.sdk; 002 003import static com.box.sdk.BoxSensitiveDataSanitizer.sanitizeHeaders; 004import static com.box.sdk.internal.utils.CollectionUtils.mapToString; 005import static java.lang.String.format; 006 007import com.box.sdk.http.ContentType; 008import com.box.sdk.http.HttpHeaders; 009import com.box.sdk.http.HttpMethod; 010import com.eclipsesource.json.Json; 011import com.eclipsesource.json.JsonObject; 012import com.eclipsesource.json.ParseException; 013import java.io.ByteArrayInputStream; 014import java.io.ByteArrayOutputStream; 015import java.io.IOException; 016import java.io.InputStream; 017import java.io.OutputStream; 018import java.net.HttpURLConnection; 019import java.net.URL; 020import java.util.ArrayList; 021import java.util.List; 022import java.util.Map; 023import java.util.Objects; 024import okhttp3.Headers; 025import okhttp3.MediaType; 026import okhttp3.Request; 027import okhttp3.RequestBody; 028import okhttp3.Response; 029 030 031/** 032 * Used to make HTTP requests to the Box API. 033 * 034 * <p>All requests to the REST API are sent using this class or one of its subclasses. This class wraps {@link 035 * HttpURLConnection} in order to provide a simpler interface that can automatically handle various conditions specific 036 * to Box's API. Requests will be authenticated using a {@link BoxAPIConnection} (if one is provided), so it isn't 037 * necessary to add authorization headers. Requests can also be sent more than once, unlike with HttpURLConnection. If 038 * an error occurs while sending a request, it will be automatically retried (with a back off delay) up to the maximum 039 * number of times set in the BoxAPIConnection.</p> 040 * 041 * <p>Specifying a body for a BoxAPIRequest is done differently than it is with HttpURLConnection. Instead of writing to 042 * an OutputStream, the request is provided an {@link InputStream} which will be read when the {@link #send} method is 043 * called. This makes it easy to retry requests since the stream can automatically reset and reread with each attempt. 044 * If the stream cannot be reset, then a new stream will need to be provided before each call to send. There is also a 045 * convenience method for specifying the body as a String, which simply wraps the String with an InputStream.</p> 046 */ 047public class BoxAPIRequest { 048 private static final BoxLogger LOGGER = BoxLogger.defaultLogger(); 049 private static final String ERROR_CREATING_REQUEST_BODY = "Error creating request body"; 050 private static final int BUFFER_SIZE = 8192; 051 private final BoxAPIConnection api; 052 private final List<RequestHeader> headers; 053 private final String method; 054 private URL url; 055 private BackoffCounter backoffCounter; 056 private int connectTimeout; 057 private int readTimeout; 058 private InputStream body; 059 private long bodyLength; 060 private boolean shouldAuthenticate; 061 private boolean followRedirects = true; 062 private final String mediaType; 063 064 /** 065 * Constructs an authenticated BoxAPIRequest using a provided BoxAPIConnection. 066 * 067 * @param api an API connection for authenticating the request. 068 * @param url the URL of the request. 069 * @param method the HTTP method of the request. 070 */ 071 public BoxAPIRequest(BoxAPIConnection api, URL url, String method) { 072 this(api, url, method, ContentType.APPLICATION_FORM_URLENCODED); 073 } 074 075 protected BoxAPIRequest(BoxAPIConnection api, URL url, String method, String mediaType) { 076 this.api = api; 077 this.url = url; 078 this.method = method; 079 this.mediaType = mediaType; 080 this.headers = new ArrayList<>(); 081 if (api != null) { 082 Map<String, String> customHeaders = api.getHeaders(); 083 if (customHeaders != null) { 084 for (String header : customHeaders.keySet()) { 085 this.addHeader(header, customHeaders.get(header)); 086 } 087 } 088 this.headers.add(new RequestHeader("X-Box-UA", api.getBoxUAHeader())); 089 } 090 this.backoffCounter = new BackoffCounter(new Time()); 091 this.shouldAuthenticate = true; 092 if (api != null) { 093 this.connectTimeout = api.getConnectTimeout(); 094 this.readTimeout = api.getReadTimeout(); 095 } else { 096 this.connectTimeout = BoxGlobalSettings.getConnectTimeout(); 097 this.readTimeout = BoxGlobalSettings.getReadTimeout(); 098 } 099 100 this.addHeader("Accept-Charset", "utf-8"); 101 } 102 103 /** 104 * Constructs an authenticated BoxAPIRequest using a provided BoxAPIConnection. 105 * 106 * @param api an API connection for authenticating the request. 107 * @param url the URL of the request. 108 * @param method the HTTP method of the request. 109 */ 110 public BoxAPIRequest(BoxAPIConnection api, URL url, HttpMethod method) { 111 this(api, url, method.name()); 112 } 113 114 /** 115 * Constructs an request, using URL and HttpMethod. 116 * 117 * @param url the URL of the request. 118 * @param method the HTTP method of the request. 119 */ 120 public BoxAPIRequest(URL url, HttpMethod method) { 121 this(null, url, method.name()); 122 } 123 124 /** 125 * @param apiException BoxAPIException thrown 126 * @return true if the request is one that should be retried, otherwise false 127 */ 128 public static boolean isRequestRetryable(BoxAPIException apiException) { 129 // Only requests that failed to send should be retried 130 return (Objects.equals(apiException.getMessage(), ERROR_CREATING_REQUEST_BODY)); 131 } 132 133 /** 134 * @param responseCode HTTP error code of the response 135 * @param apiException BoxAPIException thrown 136 * @return true if the response is one that should be retried, otherwise false 137 */ 138 public static boolean isResponseRetryable(int responseCode, BoxAPIException apiException) { 139 if (responseCode >= 500 || responseCode == 429) { 140 return true; 141 } 142 return isClockSkewError(responseCode, apiException); 143 } 144 145 private static boolean isClockSkewError(int responseCode, BoxAPIException apiException) { 146 String response = apiException.getResponse(); 147 if (response == null || response.length() == 0) { 148 return false; 149 } 150 String message = apiException.getMessage(); 151 String errorCode = ""; 152 153 try { 154 JsonObject responseBody = Json.parse(response).asObject(); 155 if (responseBody.get("code") != null) { 156 errorCode = responseBody.get("code").toString(); 157 } else if (responseBody.get("error") != null) { 158 errorCode = responseBody.get("error").toString(); 159 } 160 161 return responseCode == 400 && errorCode.contains("invalid_grant") && message.contains("exp"); 162 } catch (ParseException e) { 163 // 400 error which is not a JSON will not trigger a retry 164 throw new BoxAPIException("API returned an error", responseCode, response); 165 } 166 } 167 168 /** 169 * Adds an HTTP header to this request. 170 * 171 * @param key the header key. 172 * @param value the header value. 173 */ 174 public void addHeader(String key, String value) { 175 if (key.equals("As-User")) { 176 for (int i = 0; i < this.headers.size(); i++) { 177 if (this.headers.get(i).getKey().equals("As-User")) { 178 this.headers.remove(i); 179 } 180 } 181 } 182 if (key.equals("X-Box-UA")) { 183 throw new IllegalArgumentException("Altering the X-Box-UA header is not permitted"); 184 } 185 this.headers.add(new RequestHeader(key, value)); 186 } 187 188 /** 189 * Gets the connect timeout for the request. 190 * 191 * @return the request connection timeout. 192 */ 193 public int getConnectTimeout() { 194 return this.connectTimeout; 195 } 196 197 /** 198 * Sets a Connect timeout for this request in milliseconds. 199 * 200 * @param timeout the timeout in milliseconds. 201 */ 202 public void setConnectTimeout(int timeout) { 203 this.connectTimeout = timeout; 204 } 205 206 /** 207 * Gets the read timeout for the request. 208 * 209 * @return the request's read timeout. 210 */ 211 public int getReadTimeout() { 212 return this.readTimeout; 213 } 214 215 /** 216 * Sets a read timeout for this request in milliseconds. 217 * 218 * @param timeout the timeout in milliseconds. 219 */ 220 public void setReadTimeout(int timeout) { 221 this.readTimeout = timeout; 222 } 223 224 /** 225 * Sets whether or not to follow redirects (i.e. Location header) 226 * 227 * @param followRedirects true to follow, false to not follow 228 */ 229 public void setFollowRedirects(boolean followRedirects) { 230 this.followRedirects = followRedirects; 231 } 232 233 /** 234 * Gets the stream containing contents of this request's body. 235 * 236 * <p>Note that any bytes that read from the returned stream won't be sent unless the stream is reset back to its 237 * initial position.</p> 238 * 239 * @return an InputStream containing the contents of this request's body. 240 */ 241 public InputStream getBody() { 242 return this.body; 243 } 244 245 /** 246 * Sets the request body to the contents of an InputStream. 247 * 248 * <p>The stream must support the {@link InputStream#reset} method if auto-retry is used or if the request needs to 249 * be resent. Otherwise, the body must be manually set before each call to {@link #send}.</p> 250 * 251 * @param stream an InputStream containing the contents of the body. 252 */ 253 public void setBody(InputStream stream) { 254 this.body = stream; 255 } 256 257 /** 258 * Sets the request body to the contents of a String. 259 * 260 * <p>If the contents of the body are large, then it may be more efficient to use an {@link InputStream} instead of 261 * a String. Using a String requires that the entire body be in memory before sending the request.</p> 262 * 263 * @param body a String containing the contents of the body. 264 */ 265 public void setBody(String body) { 266 byte[] bytes = body.getBytes(StandardCharsets.UTF_8); 267 this.bodyLength = bytes.length; 268 this.body = new ByteArrayInputStream(bytes); 269 } 270 271 /** 272 * Sets the request body to the contents of an InputStream. 273 * 274 * <p>Providing the length of the InputStream allows for the progress of the request to be monitored when calling 275 * {@link #send(ProgressListener)}.</p> 276 * 277 * <p> See {@link #setBody(InputStream)} for more information on setting the body of the request.</p> 278 * 279 * @param stream an InputStream containing the contents of the body. 280 * @param length the expected length of the stream. 281 */ 282 public void setBody(InputStream stream, long length) { 283 this.bodyLength = length; 284 this.body = stream; 285 } 286 287 /** 288 * Gets the URL from the request. 289 * 290 * @return a URL containing the URL of the request. 291 */ 292 public URL getUrl() { 293 return this.url; 294 } 295 296 /** 297 * Sets the URL to the request. 298 * 299 */ 300 public void setUrl(URL url) { 301 this.url = url; 302 } 303 304 /** 305 * Gets the http method from the request. 306 * 307 * @return http method 308 */ 309 public String getMethod() { 310 return this.method; 311 } 312 313 /** 314 * Get headers as list of RequestHeader objects. 315 * 316 * @return headers as list of RequestHeader objects 317 */ 318 List<RequestHeader> getHeaders() { 319 return this.headers; 320 } 321 322 /** 323 * Sends this request and returns a BoxAPIResponse containing the server's response. 324 * 325 * <p>The type of the returned BoxAPIResponse will be based on the content type returned by the server, allowing it 326 * to be cast to a more specific type. For example, if it's known that the API call will return a JSON response, 327 * then it can be cast to a {@link BoxJSONResponse} like so:</p> 328 * 329 * <pre>BoxJSONResponse response = (BoxJSONResponse) request.sendWithoutRetry();</pre> 330 * 331 * @return a {@link BoxAPIResponse} containing the server's response. 332 * @throws BoxAPIException if the server returns an error code or if a network error occurs. 333 */ 334 public BoxAPIResponse sendWithoutRetry() { 335 return this.trySend(null); 336 } 337 338 /** 339 * Sends this request and returns a BoxAPIResponse containing the server's response. 340 * 341 * <p>The type of the returned BoxAPIResponse will be based on the content type returned by the server, allowing it 342 * to be cast to a more specific type. For example, if it's known that the API call will return a JSON response, 343 * then it can be cast to a {@link BoxJSONResponse} like so:</p> 344 * 345 * <pre>BoxJSONResponse response = (BoxJSONResponse) request.send();</pre> 346 * 347 * <p>If the server returns an error code or if a network error occurs, then the request will be automatically 348 * retried. If the maximum number of retries is reached and an error still occurs, then a {@link BoxAPIException} 349 * will be thrown.</p> 350 * 351 * <p> See {@link #send} for more information on sending requests.</p> 352 * 353 * @return a {@link BoxAPIResponse} containing the server's response. 354 * @throws BoxAPIException if the server returns an error code or if a network error occurs. 355 */ 356 public BoxAPIResponse send() { 357 return this.send(null); 358 } 359 360 /** 361 * Sends this request while monitoring its progress and returns a BoxAPIResponse containing the server's response. 362 * 363 * <p>The type of the returned BoxAPIResponse will be based on the content type returned by the server, allowing it 364 * to be cast to a more specific type. For example, if it's known that the API call will return a JSON response, 365 * then it can be cast to a {@link BoxJSONResponse} like so:</p> 366 * 367 * <p>If the server returns an error code or if a network error occurs, then the request will be automatically 368 * retried. If the maximum number of retries is reached and an error still occurs, then a {@link BoxAPIException} 369 * will be thrown.</p> 370 * 371 * <p>A ProgressListener is generally only useful when the size of the request is known beforehand. If the size is 372 * unknown, then the ProgressListener will be updated for each byte sent, but the total number of bytes will be 373 * reported as 0.</p> 374 * 375 * <p> See {@link #send} for more information on sending requests.</p> 376 * 377 * @param listener a listener for monitoring the progress of the request. 378 * @return a {@link BoxAPIResponse} containing the server's response. 379 * @throws BoxAPIException if the server returns an error code or if a network error occurs. 380 */ 381 public BoxAPIResponse send(ProgressListener listener) { 382 if (this.api == null) { 383 this.backoffCounter.reset(BoxGlobalSettings.getMaxRetryAttempts() + 1); 384 } else { 385 this.backoffCounter.reset(this.api.getMaxRetryAttempts() + 1); 386 } 387 388 while (this.backoffCounter.getAttemptsRemaining() > 0) { 389 try { 390 return this.trySend(listener); 391 } catch (BoxAPIException apiException) { 392 if (!this.backoffCounter.decrement() 393 || (!isRequestRetryable(apiException) 394 && !isResponseRetryable(apiException.getResponseCode(), apiException))) { 395 throw apiException; 396 } 397 398 LOGGER.warn( 399 format("Retrying request due to transient error status=%d body=%s headers=%s", 400 apiException.getResponseCode(), 401 apiException.getResponse(), 402 mapToString(apiException.getHeaders())) 403 ); 404 405 try { 406 this.resetBody(); 407 } catch (IOException ioException) { 408 throw apiException; 409 } 410 411 try { 412 List<String> retryAfterHeader = apiException.getHeaders().get("Retry-After"); 413 if (retryAfterHeader == null) { 414 this.backoffCounter.waitBackoff(); 415 } else { 416 int retryAfterDelay = Integer.parseInt(retryAfterHeader.get(0)) * 1000; 417 this.backoffCounter.waitBackoff(retryAfterDelay); 418 } 419 } catch (InterruptedException interruptedException) { 420 Thread.currentThread().interrupt(); 421 throw apiException; 422 } 423 } 424 } 425 426 throw new RuntimeException(); 427 } 428 429 /** 430 * Disables adding authentication header to request. 431 * Useful when you want to add your own authentication method. 432 * Default value is `true` and SKD will add authenticaton header to request. 433 * 434 * @param shouldAuthenticate use `false` to disable authentication. 435 */ 436 public void shouldAuthenticate(boolean shouldAuthenticate) { 437 this.shouldAuthenticate = shouldAuthenticate; 438 } 439 440 /** 441 * Sends a request to upload a file part and returns a BoxFileUploadSessionPart containing information 442 * about the upload part. This method is separate from send() because it has custom retry logic. 443 * 444 * <p>If the server returns an error code or if a network error occurs, then the request will be automatically 445 * retried. If the maximum number of retries is reached and an error still occurs, then a {@link BoxAPIException} 446 * will be thrown.</p> 447 * 448 * @param session The BoxFileUploadSession uploading the part 449 * @param offset Offset of the part being uploaded 450 * @return A {@link BoxFileUploadSessionPart} part that has been uploaded. 451 * @throws BoxAPIException if the server returns an error code or if a network error occurs. 452 */ 453 BoxFileUploadSessionPart sendForUploadPart(BoxFileUploadSession session, long offset) { 454 if (this.api == null) { 455 this.backoffCounter.reset(BoxGlobalSettings.getMaxRetryAttempts() + 1); 456 } else { 457 this.backoffCounter.reset(this.api.getMaxRetryAttempts() + 1); 458 } 459 460 while (this.backoffCounter.getAttemptsRemaining() > 0) { 461 try (BoxJSONResponse response = (BoxJSONResponse) this.trySend(null)) { 462 // upload sends binary data but response is JSON 463 JsonObject jsonObject = Json.parse(response.getJSON()).asObject(); 464 return new BoxFileUploadSessionPart((JsonObject) jsonObject.get("part")); 465 } catch (BoxAPIException apiException) { 466 if (!this.backoffCounter.decrement() 467 || (!isRequestRetryable(apiException) 468 && !isResponseRetryable(apiException.getResponseCode(), apiException))) { 469 throw apiException; 470 } 471 if (apiException.getResponseCode() == 500) { 472 try { 473 Iterable<BoxFileUploadSessionPart> parts = session.listParts(); 474 for (BoxFileUploadSessionPart part : parts) { 475 if (part.getOffset() == offset) { 476 return part; 477 } 478 } 479 } catch (BoxAPIException e) { 480 // ignoring exception as we are retrying 481 } 482 } 483 LOGGER.warn(format( 484 "Retrying request due to transient error status=%d body=%s", 485 apiException.getResponseCode(), 486 apiException.getResponse() 487 )); 488 489 try { 490 this.resetBody(); 491 } catch (IOException ioException) { 492 throw apiException; 493 } 494 495 try { 496 this.backoffCounter.waitBackoff(); 497 } catch (InterruptedException interruptedException) { 498 Thread.currentThread().interrupt(); 499 throw apiException; 500 } 501 } 502 } 503 504 throw new RuntimeException(); 505 } 506 507 /** 508 * Returns a String containing the URL, HTTP method, headers and body of this request. 509 * 510 * @return a String containing information about this request. 511 */ 512 @Override 513 public String toString() { 514 return toStringWithHeaders(null); 515 } 516 517 private String toStringWithHeaders(Headers headers) { 518 String lineSeparator = System.getProperty("line.separator"); 519 StringBuilder builder = new StringBuilder(); 520 builder.append("Request"); 521 builder.append(lineSeparator); 522 builder.append(this.method); 523 builder.append(' '); 524 builder.append(this.url.toString()); 525 builder.append(lineSeparator); 526 if (headers != null) { 527 builder.append("Headers:").append(lineSeparator); 528 sanitizeHeaders(headers) 529 .forEach(h -> builder.append(format("%s: [%s]%s", h.getFirst(), h.getSecond(), lineSeparator))); 530 } 531 532 String bodyString = this.bodyToString(); 533 if (bodyString != null) { 534 builder.append(lineSeparator); 535 builder.append(bodyString); 536 } 537 538 return builder.toString().trim(); 539 } 540 541 /** 542 * Returns a String representation of this request's body used in {@link #toString}. This method returns 543 * null by default. 544 * 545 * <p>A subclass may want override this method if the body can be converted to a String for logging or debugging 546 * purposes.</p> 547 * 548 * @return a String representation of this request's body. 549 */ 550 protected String bodyToString() { 551 return null; 552 } 553 554 private void writeWithBuffer(OutputStream output, ProgressListener listener) { 555 try { 556 OutputStream finalOutput = output; 557 if (listener != null) { 558 finalOutput = new ProgressOutputStream(output, listener, this.bodyLength); 559 } 560 byte[] buffer = new byte[BUFFER_SIZE]; 561 int b = this.body.read(buffer); 562 while (b != -1) { 563 finalOutput.write(buffer, 0, b); 564 b = this.body.read(buffer); 565 } 566 } catch (IOException e) { 567 throw new RuntimeException("Error writting body", e); 568 } 569 } 570 571 /** 572 * Resets the InputStream containing this request's body. 573 * 574 * <p>This method will be called before each attempt to resend the request, giving subclasses an opportunity to 575 * reset any streams that need to be read when sending the body.</p> 576 * 577 * @throws IOException if the stream cannot be reset. 578 */ 579 protected void resetBody() throws IOException { 580 if (this.body != null) { 581 this.body.reset(); 582 } 583 } 584 585 void setBackoffCounter(BackoffCounter counter) { 586 this.backoffCounter = counter; 587 } 588 589 private BoxAPIResponse trySend(ProgressListener listener) { 590 if (this.api != null) { 591 RequestInterceptor interceptor = this.api.getRequestInterceptor(); 592 if (interceptor != null) { 593 BoxAPIResponse response = interceptor.onRequest(this); 594 if (response != null) { 595 return response; 596 } 597 } 598 } 599 long start = System.currentTimeMillis(); 600 Request request = composeRequest(listener); 601 Response response; 602 this.logRequest(request.headers()); 603 if (this.followRedirects) { 604 response = api.execute(request); 605 } else { 606 response = api.executeWithoutRedirect(request); 607 } 608 logDebug(format("[trySend] connection.connect() took %dms%n", (System.currentTimeMillis() - start))); 609 610 BoxAPIResponse result = BoxAPIResponse.toBoxResponse(response); 611 long getResponseStart = System.currentTimeMillis(); 612 logDebug(format( 613 "[trySend] Get Response (read network) took %dms%n", System.currentTimeMillis() - getResponseStart 614 )); 615 return result; 616 } 617 618 private Request composeRequest(ProgressListener listener) { 619 Request.Builder requestBuilder = new Request.Builder().url(getUrl()); 620 if (this.shouldAuthenticate) { 621 requestBuilder.addHeader(HttpHeaders.AUTHORIZATION, "Bearer " + this.api.lockAccessToken()); 622 } 623 try { 624 requestBuilder.addHeader("User-Agent", this.api.getUserAgent()); 625 requestBuilder.addHeader("X-Box-UA", this.api.getBoxUAHeader()); 626 headers.forEach(h -> { 627 requestBuilder.removeHeader(h.getKey()); 628 requestBuilder.addHeader(h.getKey(), h.getValue()); 629 }); 630 631 if (this.api instanceof SharedLinkAPIConnection) { 632 SharedLinkAPIConnection sharedItemAPI = (SharedLinkAPIConnection) this.api; 633 String boxAPIValue = BoxSharedLink.getSharedLinkHeaderValue( 634 sharedItemAPI.getSharedLink(), 635 sharedItemAPI.getSharedLinkPassword() 636 ); 637 requestBuilder.addHeader("BoxApi", boxAPIValue); 638 } 639 640 641 writeMethodWithBody(requestBuilder, listener); 642 return requestBuilder.build(); 643 } finally { 644 if (this.shouldAuthenticate) { 645 this.api.unlockAccessToken(); 646 } 647 } 648 } 649 650 protected void writeMethodWithBody(Request.Builder requestBuilder, ProgressListener listener) { 651 ByteArrayOutputStream bodyBytes = new ByteArrayOutputStream(); 652 if (body != null) { 653 long writeStart = System.currentTimeMillis(); 654 writeWithBuffer(bodyBytes, listener); 655 logDebug(format("[trySend] Body write took %dms%n", (System.currentTimeMillis() - writeStart))); 656 } 657 if (method.equals("GET")) { 658 requestBuilder.get(); 659 } 660 if (method.equals("DELETE")) { 661 requestBuilder.delete(); 662 } 663 if (method.equals("OPTIONS")) { 664 if (body == null) { 665 requestBuilder.method("OPTIONS", null); 666 } else { 667 requestBuilder.method("OPTIONS", RequestBody.create(bodyBytes.toByteArray(), mediaType())); 668 } 669 } 670 if (method.equals("POST")) { 671 requestBuilder.post(RequestBody.create(bodyBytes.toByteArray(), mediaType())); 672 } 673 if (method.equals("PUT")) { 674 requestBuilder.put(RequestBody.create(bodyBytes.toByteArray(), mediaType())); 675 } 676 } 677 678 private void logDebug(String message) { 679 if (LOGGER.isDebugEnabled()) { 680 LOGGER.debug(message); 681 } 682 } 683 684 private void logRequest(Headers headers) { 685 logDebug(headers != null ? this.toStringWithHeaders(headers) : this.toString()); 686 } 687 688 /** 689 * Class for mapping a request header and value. 690 */ 691 public static final class RequestHeader { 692 private final String key; 693 private final String value; 694 695 /** 696 * Construct a request header from header key and value. 697 * 698 * @param key header name 699 * @param value header value 700 */ 701 public RequestHeader(String key, String value) { 702 this.key = key; 703 this.value = value; 704 } 705 706 /** 707 * Get header key. 708 * 709 * @return http header name 710 */ 711 public String getKey() { 712 return this.key; 713 } 714 715 /** 716 * Get header value. 717 * 718 * @return http header value 719 */ 720 public String getValue() { 721 return this.value; 722 } 723 } 724 725 protected MediaType mediaType() { 726 return MediaType.parse(mediaType); 727 } 728}