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}