All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.jboss.resteasy.reactive.client.handlers.ClientSendRequestHandler Maven / Gradle / Ivy

There is a newer version: 3.17.5
Show newest version
package org.jboss.resteasy.reactive.client.handlers;

import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URL;
import java.nio.file.Path;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.function.Consumer;
import java.util.function.Function;

import jakarta.ws.rs.InternalServerErrorException;
import jakarta.ws.rs.ProcessingException;
import jakarta.ws.rs.client.Entity;
import jakarta.ws.rs.core.HttpHeaders;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.MultivaluedMap;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.Variant;
import jakarta.ws.rs.ext.WriterInterceptor;

import org.jboss.logging.Logger;
import org.jboss.resteasy.reactive.client.AsyncResultUni;
import org.jboss.resteasy.reactive.client.api.ClientLogger;
import org.jboss.resteasy.reactive.client.api.LoggingScope;
import org.jboss.resteasy.reactive.client.api.QuarkusRestClientProperties;
import org.jboss.resteasy.reactive.client.impl.AsyncInvokerImpl;
import org.jboss.resteasy.reactive.client.impl.InputStreamReadStream;
import org.jboss.resteasy.reactive.client.impl.RestClientRequestContext;
import org.jboss.resteasy.reactive.client.impl.multipart.PausableHttpPostRequestEncoder;
import org.jboss.resteasy.reactive.client.impl.multipart.QuarkusMultipartForm;
import org.jboss.resteasy.reactive.client.impl.multipart.QuarkusMultipartFormUpload;
import org.jboss.resteasy.reactive.client.impl.multipart.QuarkusMultipartResponseDecoder;
import org.jboss.resteasy.reactive.client.spi.ClientRestHandler;
import org.jboss.resteasy.reactive.client.spi.MultipartResponseData;
import org.jboss.resteasy.reactive.common.core.Serialisers;
import org.jboss.resteasy.reactive.common.util.MultivaluedTreeMap;

import io.netty.handler.codec.http.HttpHeaderValues;
import io.netty.handler.codec.http.LastHttpContent;
import io.netty.handler.codec.http.multipart.InterfaceHttpData;
import io.smallrye.mutiny.Multi;
import io.smallrye.mutiny.Uni;
import io.smallrye.mutiny.vertx.ReadStreamSubscriber;
import io.smallrye.stork.api.ServiceInstance;
import io.vertx.core.AsyncResult;
import io.vertx.core.Future;
import io.vertx.core.Handler;
import io.vertx.core.MultiMap;
import io.vertx.core.Promise;
import io.vertx.core.Vertx;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.file.AsyncFile;
import io.vertx.core.file.OpenOptions;
import io.vertx.core.http.HttpClient;
import io.vertx.core.http.HttpClientRequest;
import io.vertx.core.http.HttpClientResponse;
import io.vertx.core.http.HttpClosedException;
import io.vertx.core.http.HttpMethod;
import io.vertx.core.http.HttpVersion;
import io.vertx.core.http.RequestOptions;
import io.vertx.core.streams.Pipe;
import io.vertx.core.streams.Pump;

public class ClientSendRequestHandler implements ClientRestHandler {
    private static final Logger log = Logger.getLogger(ClientSendRequestHandler.class);
    public static final String CONTENT_TYPE = "Content-Type";

    private final boolean followRedirects;
    private final LoggingScope loggingScope;
    private final ClientLogger clientLogger;
    private final Map, MultipartResponseData> multipartResponseDataMap;
    private final int maxChunkSize;

    public ClientSendRequestHandler(int maxChunkSize, boolean followRedirects, LoggingScope loggingScope, ClientLogger logger,
            Map, MultipartResponseData> multipartResponseDataMap) {
        this.maxChunkSize = maxChunkSize;
        this.followRedirects = followRedirects;
        this.loggingScope = loggingScope;
        this.clientLogger = logger;
        this.multipartResponseDataMap = multipartResponseDataMap;
    }

    @Override
    public void handle(RestClientRequestContext requestContext) {
        if (requestContext.isAborted()) {
            return;
        }
        requestContext.suspend();
        Uni future = createRequest(requestContext);

        // DNS failures happen before we send the request
        future.subscribe().with(new Consumer<>() {
            @Override
            public void accept(HttpClientRequest httpClientRequest) {
                // adapt headers to HTTP/2 depending on the underlying HTTP connection
                ClientSendRequestHandler.this.adaptRequest(httpClientRequest);

                if (requestContext.isMultipart()) {
                    Promise requestPromise = Promise.promise();
                    QuarkusMultipartFormUpload actualEntity;
                    try {
                        actualEntity = ClientSendRequestHandler.this.setMultipartHeadersAndPrepareBody(httpClientRequest,
                                requestContext);
                        if (loggingScope != LoggingScope.NONE) {
                            clientLogger.logRequest(httpClientRequest, null, true);
                        }

                        Pipe pipe = actualEntity.pipe(); // Shouldn't this be called in an earlier phase ?
                        requestPromise.future().onComplete(ar -> {

                            if (ar.succeeded()) {
                                HttpClientRequest req = ar.result();
                                if (httpClientRequest.headers() == null
                                        || !httpClientRequest.headers().contains(HttpHeaders.CONTENT_LENGTH)) {
                                    req.setChunked(true);
                                }
                                pipe.endOnFailure(false);
                                pipe.to(req, ar2 -> {
                                    if (ar2.failed()) {
                                        req.reset(0L, ar2.cause());
                                    }
                                });
                                actualEntity.run();
                            } else {
                                pipe.close();
                            }
                        });
                        Future sent = httpClientRequest.response();
                        requestPromise.complete(httpClientRequest);
                        attachSentHandlers(sent, httpClientRequest, requestContext);
                    } catch (Throwable e) {
                        reportFinish(e, requestContext);
                        requestContext.resume(e);
                        return;
                    }
                } else if (requestContext.isFileUpload()) {
                    Vertx vertx = Vertx.currentContext().owner();
                    Object entity = requestContext.getEntity().getEntity();
                    String filePathToUpload = null;
                    if (entity instanceof File) {
                        filePathToUpload = ((File) entity).getAbsolutePath();
                    } else if (entity instanceof Path) {
                        filePathToUpload = ((Path) entity).toAbsolutePath().toString();
                    }
                    vertx.fileSystem()
                            .open(filePathToUpload, new OpenOptions().setRead(true).setWrite(false),
                                    new Handler<>() {
                                        @Override
                                        public void handle(AsyncResult openedAsyncFile) {
                                            if (openedAsyncFile.failed()) {
                                                requestContext.resume(openedAsyncFile.cause());
                                                return;
                                            }

                                            MultivaluedMap headerMap = requestContext.getRequestHeadersAsMap();
                                            updateRequestHeadersFromConfig(requestContext, headerMap);

                                            // set the Vertx headers after we've run the interceptors because they can modify them
                                            setVertxHeaders(httpClientRequest, headerMap);

                                            Future sent = httpClientRequest.send(openedAsyncFile.result());
                                            attachSentHandlers(sent, httpClientRequest, requestContext);
                                        }
                                    });
                } else if (requestContext.isInputStreamUpload() && !hasWriterInterceptors(requestContext)) {
                    MultivaluedMap headerMap = requestContext.getRequestHeadersAsMap();
                    updateRequestHeadersFromConfig(requestContext, headerMap);
                    setVertxHeaders(httpClientRequest, headerMap);
                    Future sent = httpClientRequest.send(
                            new InputStreamReadStream(
                                    Vertx.currentContext().owner(), (InputStream) requestContext.getEntity().getEntity(),
                                    httpClientRequest));
                    attachSentHandlers(sent, httpClientRequest, requestContext);
                } else if (requestContext.isMultiBufferUpload()) {
                    MultivaluedMap headerMap = requestContext.getRequestHeadersAsMap();
                    updateRequestHeadersFromConfig(requestContext, headerMap);
                    setVertxHeaders(httpClientRequest, headerMap);
                    Future sent = httpClientRequest.send(ReadStreamSubscriber.asReadStream(
                            (Multi) requestContext.getEntity().getEntity(),
                            new Function<>() {
                                @Override
                                public Buffer apply(io.vertx.mutiny.core.buffer.Buffer buffer) {
                                    return buffer.getDelegate();
                                }
                            }));
                    attachSentHandlers(sent, httpClientRequest, requestContext);
                } else {
                    Future sent;
                    Buffer actualEntity;
                    try {
                        actualEntity = ClientSendRequestHandler.this
                                .setRequestHeadersAndPrepareBody(httpClientRequest, requestContext);
                    } catch (Throwable e) {
                        requestContext.resume(e);
                        return;
                    }
                    if (actualEntity == AsyncInvokerImpl.EMPTY_BUFFER) {
                        sent = httpClientRequest.send();
                        if (loggingScope != LoggingScope.NONE) {
                            clientLogger.logRequest(httpClientRequest, null, false);
                        }
                    } else {
                        sent = httpClientRequest.send(actualEntity);
                        if (loggingScope != LoggingScope.NONE) {
                            clientLogger.logRequest(httpClientRequest, actualEntity, false);
                        }
                    }
                    attachSentHandlers(sent, httpClientRequest, requestContext);
                }
            }
        }, new Consumer<>() {
            @Override
            public void accept(Throwable event) {
                // set some properties to prevent NPEs down the chain
                requestContext.setResponseHeaders(new MultivaluedTreeMap<>());
                requestContext.setResponseReasonPhrase("unknown");

                if (event instanceof IOException) {
                    ProcessingException throwable = new ProcessingException(event);
                    reportFinish(throwable, requestContext);
                    requestContext.resume(throwable);
                } else {
                    requestContext.resume(event);
                    reportFinish(event, requestContext);
                }
            }
        });
    }

    private void attachSentHandlers(Future sent,
            HttpClientRequest httpClientRequest,
            RestClientRequestContext requestContext) {
        sent.onSuccess(new Handler<>() {
            @Override
            public void handle(HttpClientResponse clientResponse) {
                try {
                    requestContext.initialiseResponse(clientResponse);
                    int status = clientResponse.statusCode();
                    if (requestContext.getCallStatsCollector() != null) {
                        if (status >= 500 && status < 600) {
                            reportFinish(new InternalServerErrorException(),
                                    requestContext);
                        } else {
                            reportFinish(null, requestContext);
                        }
                    }

                    if (isResponseMultipart(requestContext)) {
                        QuarkusMultipartResponseDecoder multipartDecoder = new QuarkusMultipartResponseDecoder(
                                clientResponse);

                        clientResponse.handler(multipartDecoder::offer);

                        clientResponse.endHandler(new Handler<>() {
                            @Override
                            public void handle(Void event) {
                                multipartDecoder.offer(LastHttpContent.EMPTY_LAST_CONTENT);

                                List datas = multipartDecoder.getBodyHttpDatas();
                                requestContext.setResponseMultipartParts(datas);

                                if (loggingScope != LoggingScope.NONE) {
                                    clientLogger.logResponse(clientResponse, false);
                                }

                                requestContext.resume();
                            }
                        });
                    } else if (!requestContext.isRegisterBodyHandler()
                            && (Response.Status.Family.familyOf(status) == Response.Status.Family.SUCCESSFUL)) { // we force the registration of a body handler if there was an error, so we can ensure the body can be read
                        clientResponse.pause();
                        if (loggingScope != LoggingScope.NONE) {
                            clientLogger.logResponse(clientResponse, false);
                        }
                        requestContext.resume();
                    } else {
                        if (requestContext.isFileDownload()) {
                            // when downloading a file we copy the bytes to the file system and manually set the entity type
                            // this is needed because large files can cause OOM or exceed the InputStream limit (of 2GB)

                            clientResponse.pause();
                            Vertx vertx = Vertx.currentContext().owner();
                            vertx.fileSystem().createTempFile("rest-client", "",
                                    new Handler<>() {
                                        @Override
                                        public void handle(AsyncResult tempFileCreation) {
                                            if (tempFileCreation.failed()) {
                                                reportFinish(tempFileCreation.cause(), requestContext);
                                                requestContext.resume(tempFileCreation.cause());
                                                return;
                                            }
                                            String tmpFilePath = tempFileCreation.result();
                                            vertx.fileSystem().open(tmpFilePath,
                                                    new OpenOptions().setWrite(true),
                                                    new Handler<>() {
                                                        @Override
                                                        public void handle(AsyncResult asyncFileOpened) {
                                                            if (asyncFileOpened.failed()) {
                                                                reportFinish(asyncFileOpened.cause(), requestContext);
                                                                requestContext.resume(asyncFileOpened.cause());
                                                                return;
                                                            }
                                                            final AsyncFile tmpAsyncFile = asyncFileOpened.result();
                                                            final Pump downloadPump = Pump.pump(clientResponse,
                                                                    tmpAsyncFile);
                                                            downloadPump.start();

                                                            clientResponse.resume();
                                                            clientResponse.endHandler(new Handler<>() {
                                                                public void handle(Void event) {
                                                                    tmpAsyncFile.flush(new Handler<>() {
                                                                        public void handle(AsyncResult flushed) {
                                                                            if (flushed.failed()) {
                                                                                reportFinish(flushed.cause(),
                                                                                        requestContext);
                                                                                requestContext.resume(flushed.cause());
                                                                                return;
                                                                            }

                                                                            if (loggingScope != LoggingScope.NONE) {
                                                                                clientLogger.logRequest(
                                                                                        httpClientRequest, null, false);
                                                                            }

                                                                            requestContext.setTmpFilePath(tmpFilePath);
                                                                            requestContext.resume();
                                                                        }
                                                                    });
                                                                }
                                                            });
                                                        }
                                                    });
                                        }
                                    });

                        } else if (requestContext.isInputStreamDownload()) {
                            if (loggingScope != LoggingScope.NONE) {
                                clientLogger.logResponse(clientResponse, false);
                            }
                            //TODO: make timeout configureable
                            requestContext
                                    .setResponseEntityStream(
                                            new VertxClientInputStream(clientResponse, 100000, requestContext));
                            requestContext.resume();
                        } else {
                            clientResponse.body(new Handler<>() {
                                @Override
                                public void handle(AsyncResult ar) {
                                    if (ar.succeeded()) {
                                        if (loggingScope != LoggingScope.NONE) {
                                            clientLogger.logResponse(clientResponse, false);
                                        }
                                        Buffer buffer = ar.result();
                                        try {
                                            if (buffer.length() > 0) {
                                                requestContext.setResponseEntityStream(
                                                        new ByteArrayInputStream(buffer.getBytes()));
                                            } else {
                                                requestContext.setResponseEntityStream(null);
                                            }
                                            requestContext.resume();
                                        } catch (Throwable t) {
                                            requestContext.resume(t);
                                        }
                                    } else {
                                        requestContext.resume(ar.cause());
                                    }
                                }
                            });
                        }
                    }
                } catch (Throwable t) {
                    reportFinish(t, requestContext);
                    requestContext.resume(t);
                }
            }
        })
                .onFailure(new Handler<>() {
                    @Override
                    public void handle(Throwable failure) {
                        if (failure instanceof HttpClosedException) {
                            // This is because of the Rest Client TCK
                            // HttpClosedException is a runtime exception. If we complete with that exception, it gets
                            // unwrapped by the rest client proxy and thus fails the TCK.
                            // By creating an IOException, we avoid that and provide a meaningful exception (because
                            // it's an I/O exception)
                            requestContext.resume(new ProcessingException(new IOException(failure.getMessage())));
                        } else if (failure instanceof IOException) {
                            requestContext.resume(new ProcessingException(failure));
                        } else {
                            requestContext.resume(failure);
                        }
                    }
                });
    }

    private boolean isResponseMultipart(RestClientRequestContext requestContext) {
        MultivaluedMap responseHeaders = requestContext.getResponseHeaders();
        List contentTypes = responseHeaders.get(CONTENT_TYPE);
        if (contentTypes != null) {
            for (String contentType : contentTypes) {
                if (contentType.toLowerCase(Locale.ROOT).startsWith(MediaType.MULTIPART_FORM_DATA)) {
                    return true;
                }
            }
        }
        return false;
    }

    private void reportFinish(Throwable throwable, RestClientRequestContext requestContext) {
        ServiceInstance serviceInstance = requestContext.getCallStatsCollector();
        if (serviceInstance != null) {
            serviceInstance.recordReply();
            serviceInstance.recordEnd(throwable);
        }
    }

    public Uni createRequest(RestClientRequestContext state) {
        HttpClient httpClient = state.getHttpClient();
        URI uri = state.getUri();
        Object readTimeout = state.getConfiguration().getProperty(QuarkusRestClientProperties.READ_TIMEOUT);
        Uni requestOptions;
        state.setMultipartResponsesData(multipartResponseDataMap);
        if (uri.getScheme() == null) { // invalid URI
            return Uni.createFrom()
                    .failure(new IllegalArgumentException("Invalid REST Client URL used: '" + uri + "'"));
        }
        try {
            URL ignored = uri.toURL();
        } catch (MalformedURLException mue) {
            log.error("Invalid REST Client URL used: '" + uri + "'");
            return Uni.createFrom()
                    .failure(new IllegalArgumentException("Invalid REST Client URL used: '" + uri + "'"));
        }

        boolean isHttps = "https".equals(uri.getScheme());
        int port = getPort(isHttps, uri.getPort());
        requestOptions = Uni.createFrom().item(new RequestOptions().setHost(uri.getHost())
                .setPort(port).setSsl(isHttps));

        return requestOptions.onItem()
                .transform(r -> r.setMethod(HttpMethod.valueOf(state.getHttpMethod()))
                        .setURI(uri.getRawPath() + (uri.getRawQuery() == null ? "" : "?" + uri.getRawQuery()))
                        .setFollowRedirects(followRedirects))
                .onItem().invoke(r -> {
                    if (readTimeout instanceof Long) {
                        r.setTimeout((Long) readTimeout);
                    }
                })
                .onItem().transformToUni(new Function>() {
                    @Override
                    public Uni apply(RequestOptions options) {
                        return AsyncResultUni.toUni(handler -> httpClient.request(options, handler));
                    }
                });
    }

    private boolean shouldMeasureTime(RestClientRequestContext state) {
        return !Multi.class.equals(state.getResponseType().getRawType());
    }

    private int getPort(boolean isHttps, int specifiedPort) {
        return specifiedPort != -1 ? specifiedPort : defaultPort(isHttps);
    }

    private int defaultPort(boolean isHttps) {
        return isHttps ? 443 : 80;
    }

    private QuarkusMultipartFormUpload setMultipartHeadersAndPrepareBody(HttpClientRequest httpClientRequest,
            RestClientRequestContext state) throws Exception {
        if (!(state.getEntity().getEntity() instanceof QuarkusMultipartForm)) {
            throw new IllegalArgumentException(
                    "Multipart form upload expects an entity of type MultipartForm, got: " + state.getEntity().getEntity());
        }

        MultivaluedMap headerMap = state.getRequestHeadersAsMap();
        updateRequestHeadersFromConfig(state, headerMap);
        QuarkusMultipartForm multipartForm = (QuarkusMultipartForm) state.getEntity().getEntity();
        multipartForm.preparePojos(state);

        Object property = state.getConfiguration().getProperty(QuarkusRestClientProperties.MULTIPART_ENCODER_MODE);
        PausableHttpPostRequestEncoder.EncoderMode mode = PausableHttpPostRequestEncoder.EncoderMode.RFC1738;
        if (property != null) {
            mode = (PausableHttpPostRequestEncoder.EncoderMode) property;
        }
        QuarkusMultipartFormUpload multipartFormUpload = new QuarkusMultipartFormUpload(Vertx.currentContext(), multipartForm,
                true, maxChunkSize, mode);
        httpClientRequest.setChunked(multipartFormUpload.isChunked());
        setEntityRelatedHeaders(headerMap, state.getEntity());

        // multipart has its own headers:
        MultiMap multipartHeaders = multipartFormUpload.headers();
        for (String multipartHeader : multipartHeaders.names()) {
            headerMap.put(multipartHeader, multipartHeaders.getAll(multipartHeader));
        }

        if (state.getEntity().getVariant() != null) {
            Variant v = state.getEntity().getVariant();
            String variantContentType = v.getMediaType().toString();
            String multipartContentType = headerMap.getFirst(HttpHeaders.CONTENT_TYPE);
            if (multipartContentType.startsWith(HttpHeaderValues.MULTIPART_FORM_DATA.toString())
                    && !variantContentType.startsWith(HttpHeaderValues.MULTIPART_FORM_DATA.toString())) {
                // this is a total hack whose purpose is to allow the @Consumes annotation to override the media type
                int semicolonIndex = multipartContentType.indexOf(';');
                headerMap.put(HttpHeaders.CONTENT_TYPE,
                        List.of(variantContentType + multipartContentType.substring(semicolonIndex)));
            }
        }

        setVertxHeaders(httpClientRequest, headerMap);
        return multipartFormUpload;
    }

    private Buffer setRequestHeadersAndPrepareBody(HttpClientRequest httpClientRequest,
            RestClientRequestContext state)
            throws IOException {
        MultivaluedMap headerMap = state.getRequestHeadersAsMap();
        updateRequestHeadersFromConfig(state, headerMap);

        Buffer actualEntity = AsyncInvokerImpl.EMPTY_BUFFER;
        Entity entity = state.getEntity();
        if (entity != null) {
            // no need to set the entity.getMediaType, it comes from the variant
            setEntityRelatedHeaders(headerMap, entity);

            actualEntity = state.writeEntity(entity, headerMap, getWriterInterceptors(state));
        } else {
            // some servers don't like the fact that a POST or PUT does not have a method body if there is no content-length header associated
            if (state.getHttpMethod().equals("POST") || state.getHttpMethod().equals("PUT")) {
                headerMap.putSingle(HttpHeaders.CONTENT_LENGTH, "0");
            }
        }
        // set the Vertx headers after we've run the interceptors because they can modify them
        setVertxHeaders(httpClientRequest, headerMap);
        return actualEntity;
    }

    private WriterInterceptor[] getWriterInterceptors(RestClientRequestContext context) {
        return context.getConfiguration().getWriterInterceptors().toArray(Serialisers.NO_WRITER_INTERCEPTOR);
    }

    private boolean hasWriterInterceptors(RestClientRequestContext context) {
        WriterInterceptor[] interceptors = getWriterInterceptors(context);
        return interceptors != null && interceptors.length > 0;
    }

    private void adaptRequest(HttpClientRequest request) {
        if (request.version() == HttpVersion.HTTP_2) {
            // When using the protocol HTTP/2, Netty which is internally used by Vert.x will validate the headers and reject
            // the requests with invalid metadata.
            // When we start a new connection, the Vert.x client will automatically upgrade the first request we make to be
            // valid in HTTP/2.
            // The problem is that in next requests, the Vert.x client reuses the same connection within the same window time
            // and hence does not upgrade the following requests. Therefore, even though the first request works fine, the
            // next requests won't work.
            // This has been reported in https://github.com/eclipse-vertx/vert.x/issues/4618.
            // To workaround this issue, we need to "upgrade" the next requests by ourselves when the version is already set
            // to HTTP/2:
            if (request.path() == null || request.path().length() == 0) {
                // HTTP/2 does not allow empty paths
                request.setURI(request.getURI() + "/");
            }
        }
    }

    private void updateRequestHeadersFromConfig(RestClientRequestContext state, MultivaluedMap headerMap) {
        Object staticHeaders = state.getConfiguration().getProperty(QuarkusRestClientProperties.STATIC_HEADERS);
        if (staticHeaders instanceof Map) {
            for (Map.Entry entry : ((Map) staticHeaders).entrySet()) {
                headerMap.putSingle(entry.getKey(), entry.getValue());
            }
        }
    }

    private void setVertxHeaders(HttpClientRequest httpClientRequest, MultivaluedMap headerMap) {
        MultiMap vertxHttpHeaders = httpClientRequest.headers();
        for (Map.Entry> entry : headerMap.entrySet()) {
            vertxHttpHeaders.add(entry.getKey(), entry.getValue());
        }
    }

    private void setEntityRelatedHeaders(MultivaluedMap headerMap, Entity entity) {
        if (entity.getVariant() != null) {
            Variant v = entity.getVariant();
            if (!headerMap.containsKey(HttpHeaders.CONTENT_TYPE)) {
                headerMap.putSingle(HttpHeaders.CONTENT_TYPE, v.getMediaType().toString());
            }
            if ((v.getLanguageString() != null) && !headerMap.containsKey(HttpHeaders.CONTENT_LANGUAGE)) {
                headerMap.putSingle(HttpHeaders.CONTENT_LANGUAGE, v.getLanguageString());
            }
            if ((v.getEncoding() != null) && !headerMap.containsKey(HttpHeaders.CONTENT_ENCODING)) {
                headerMap.putSingle(HttpHeaders.CONTENT_ENCODING, v.getEncoding());
            }
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy