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

io.helidon.webclient.http1.Http1ClientResponseImpl Maven / Gradle / Ivy

There is a newer version: 4.1.4
Show newest version
/*
 * Copyright (c) 2022, 2024 Oracle and/or its affiliates.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package io.helidon.webclient.http1;

import java.io.InputStream;
import java.lang.System.Logger.Level;
import java.util.List;
import java.util.OptionalLong;
import java.util.ServiceLoader;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicBoolean;

import io.helidon.common.GenericType;
import io.helidon.common.HelidonServiceLoader;
import io.helidon.common.LazyValue;
import io.helidon.common.buffers.BufferData;
import io.helidon.common.buffers.DataReader;
import io.helidon.common.media.type.ParserMode;
import io.helidon.http.ClientRequestHeaders;
import io.helidon.http.ClientResponseHeaders;
import io.helidon.http.ClientResponseTrailers;
import io.helidon.http.HeaderNames;
import io.helidon.http.HeaderValues;
import io.helidon.http.Headers;
import io.helidon.http.Http1HeadersParser;
import io.helidon.http.Status;
import io.helidon.http.media.MediaContext;
import io.helidon.http.media.ReadableEntity;
import io.helidon.http.media.ReadableEntityBase;
import io.helidon.webclient.api.ClientConnection;
import io.helidon.webclient.api.ClientResponseEntity;
import io.helidon.webclient.api.ClientUri;
import io.helidon.webclient.api.HttpClientConfig;
import io.helidon.webclient.spi.Source;
import io.helidon.webclient.spi.SourceHandlerProvider;

class Http1ClientResponseImpl implements Http1ClientResponse {
    private static final System.Logger LOGGER = System.getLogger(Http1ClientResponseImpl.class.getName());

    @SuppressWarnings("rawtypes")
    private static final List SOURCE_HANDLERS
            = HelidonServiceLoader.builder(ServiceLoader.load(SourceHandlerProvider.class)).build().asList();
    private static final long ENTITY_LENGTH_CHUNKED = -1;
    private final AtomicBoolean closed = new AtomicBoolean();

    private final HttpClientConfig clientConfig;
    private final Http1ClientProtocolConfig protocolConfig;
    private final Status responseStatus;
    private final ClientRequestHeaders requestHeaders;
    private final ClientResponseHeaders responseHeaders;
    private final InputStream inputStream;
    private final MediaContext mediaContext;
    private final CompletableFuture whenComplete;
    private final boolean hasTrailers;
    private final List trailerNames;
    // Media type parsing mode configured on client.
    private final ParserMode parserMode;
    private final ClientUri lastEndpointUri;

    private final ClientConnection connection;
    private final LazyValue trailers;
    private boolean entityRequested;
    private long entityLength;
    private boolean entityFullyRead = false;

    Http1ClientResponseImpl(HttpClientConfig clientConfig,
                            Http1ClientProtocolConfig protocolConfig,
                            Status responseStatus,
                            ClientRequestHeaders requestHeaders,
                            ClientResponseHeaders responseHeaders,
                            ClientConnection connection,
                            InputStream inputStream, // can be null if no entity
                            MediaContext mediaContext,
                            ClientUri lastEndpointUri,
                            CompletableFuture whenComplete) {
        this.clientConfig = clientConfig;
        this.protocolConfig = protocolConfig;
        this.responseStatus = responseStatus;
        this.requestHeaders = requestHeaders;
        this.responseHeaders = responseHeaders;
        this.connection = connection;
        this.inputStream = inputStream;
        this.mediaContext = mediaContext;
        this.parserMode = clientConfig.mediaTypeParserMode();
        this.lastEndpointUri = lastEndpointUri;
        this.whenComplete = whenComplete;
        this.trailers = LazyValue.create(() -> Http1HeadersParser.readHeaders(
                connection.reader(),
                protocolConfig.maxHeaderSize(),
                protocolConfig.validateResponseHeaders()
        ));

        OptionalLong contentLength = responseHeaders.contentLength();
        if (contentLength.isPresent()) {
            this.entityLength = contentLength.getAsLong();
        } else if (responseHeaders.contains(HeaderValues.TRANSFER_ENCODING_CHUNKED)) {
            this.entityLength = ENTITY_LENGTH_CHUNKED;
        }

        if (responseHeaders.contains(HeaderNames.TRAILER)) {
            this.hasTrailers = true;
            this.trailerNames = responseHeaders.get(HeaderNames.TRAILER).allValues(true);
        } else {
            this.hasTrailers = false;
            this.trailerNames = List.of();
        }
    }

    @Override
    public Status status() {
        return responseStatus;
    }

    @Override
    public ClientResponseHeaders headers() {
        return responseHeaders;
    }

    @Override
    public ClientResponseTrailers trailers() {
        if (hasTrailers) {
            if (!this.entityRequested) {
                throw new IllegalStateException("Trailers requested before reading entity.");
            }
            return ClientResponseTrailers.create(this.trailers.get());
        } else {
            return ClientResponseTrailers.create();
        }
    }

    @Override
    public ReadableEntity entity() {
        this.entityRequested = true;
        return entity(requestHeaders, responseHeaders);
    }

    @Override
    public void close() {
        if (closed.compareAndSet(false, true)) {
            try {
                if (headers().contains(HeaderValues.CONNECTION_CLOSE)) {
                    connection.closeResource();
                } else {
                    if (entityFullyRead || entityLength == 0 || consumeUnreadEntity()) {
                        connection.releaseResource();
                    } else {
                        connection.closeResource();
                    }
                }
            } finally {
                whenComplete.complete(null);
            }
        }
    }

    @Override
    @SuppressWarnings("unchecked")
    public > void source(GenericType sourceType, T source) {
        for (SourceHandlerProvider p : SOURCE_HANDLERS) {
            if (p.supports(sourceType, this)) {
                p.handle(source, this, mediaContext);
                return;
            }
        }
        throw new UnsupportedOperationException("No source available for " + sourceType);
    }

    @Override
    public ClientUri lastEndpointUri() {
        return lastEndpointUri;
    }

    ClientConnection connection() {
        return connection;
    }

    /**
     * Attempts to consume an unread entity for the purpose of re-using a cached
     * connection. Only works for length-prefixed responses and when the entity
     * has been loaded and has not been partially read. This method shall never
     * block on a read operation.
     *
     * @return {@code true} if consumed, {@code false} otherwise
     */
    private boolean consumeUnreadEntity() {
        if (entityLength == ENTITY_LENGTH_CHUNKED) {
            return false;
        }
        DataReader reader = connection.reader();
        if (reader.available() != entityLength) {
            return false;
        }
        try {
            reader.skip((int) entityLength);
            entityFullyRead = true;
            return true;
        } catch (RuntimeException e) {
            LOGGER.log(Level.DEBUG, "Exception while consuming entity", e);
            return false;
        }
    }

    private ReadableEntity entity(ClientRequestHeaders requestHeaders,
                                  ClientResponseHeaders responseHeaders) {
        if (inputStream == null) {
            return ReadableEntityBase.empty();
        }
        return ClientResponseEntity.create(
                this::readBytes,
                this::entityFullyRead,
                requestHeaders,
                responseHeaders,
                mediaContext
        );
    }

    private void entityFullyRead() {
        this.entityFullyRead = true;
        this.close();
    }

    private BufferData readBytes(int estimate) {
        BufferData bufferData = BufferData.create(estimate);
        int bytesRead = bufferData.readFrom(inputStream);
        if (bytesRead == -1) {
            return null;
        }
        return bufferData;
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy