001/*
002 * Copyright 2002-2019 the original author or authors.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *      https://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017package org.springframework.web.reactive.function.client;
018
019import java.net.URI;
020
021import org.apache.commons.logging.Log;
022import org.apache.commons.logging.LogFactory;
023import reactor.core.publisher.Mono;
024
025import org.springframework.core.log.LogFormatUtils;
026import org.springframework.http.HttpHeaders;
027import org.springframework.http.HttpMethod;
028import org.springframework.http.HttpRequest;
029import org.springframework.http.HttpStatus;
030import org.springframework.http.client.reactive.ClientHttpConnector;
031import org.springframework.http.client.reactive.ClientHttpResponse;
032import org.springframework.http.codec.LoggingCodecSupport;
033import org.springframework.util.Assert;
034
035/**
036 * Static factory methods to create an {@link ExchangeFunction}.
037 *
038 * @author Arjen Poutsma
039 * @author Rossen Stoyanchev
040 * @since 5.0
041 */
042public abstract class ExchangeFunctions {
043
044        private static final Log logger = LogFactory.getLog(ExchangeFunctions.class);
045
046
047        /**
048         * Create an {@code ExchangeFunction} with the given {@code ClientHttpConnector}.
049         * This is the same as calling
050         * {@link #create(ClientHttpConnector, ExchangeStrategies)} and passing
051         * {@link ExchangeStrategies#withDefaults()}.
052         * @param connector the connector to use for connecting to servers
053         * @return the created {@code ExchangeFunction}
054         */
055        public static ExchangeFunction create(ClientHttpConnector connector) {
056                return create(connector, ExchangeStrategies.withDefaults());
057        }
058
059        /**
060         * Create an {@code ExchangeFunction} with the given
061         * {@code ClientHttpConnector} and {@code ExchangeStrategies}.
062         * @param connector the connector to use for connecting to servers
063         * @param strategies the {@code ExchangeStrategies} to use
064         * @return the created {@code ExchangeFunction}
065         */
066        public static ExchangeFunction create(ClientHttpConnector connector, ExchangeStrategies strategies) {
067                return new DefaultExchangeFunction(connector, strategies);
068        }
069
070
071        private static class DefaultExchangeFunction implements ExchangeFunction {
072
073                private final ClientHttpConnector connector;
074
075                private final ExchangeStrategies strategies;
076
077                private boolean enableLoggingRequestDetails;
078
079
080                public DefaultExchangeFunction(ClientHttpConnector connector, ExchangeStrategies strategies) {
081                        Assert.notNull(connector, "ClientHttpConnector must not be null");
082                        Assert.notNull(strategies, "ExchangeStrategies must not be null");
083                        this.connector = connector;
084                        this.strategies = strategies;
085
086                        strategies.messageWriters().stream()
087                                        .filter(LoggingCodecSupport.class::isInstance)
088                                        .forEach(reader -> {
089                                                if (((LoggingCodecSupport) reader).isEnableLoggingRequestDetails()) {
090                                                        this.enableLoggingRequestDetails = true;
091                                                }
092                                        });
093                }
094
095
096                @Override
097                public Mono<ClientResponse> exchange(ClientRequest clientRequest) {
098                        Assert.notNull(clientRequest, "ClientRequest must not be null");
099                        HttpMethod httpMethod = clientRequest.method();
100                        URI url = clientRequest.url();
101                        String logPrefix = clientRequest.logPrefix();
102
103                        return this.connector
104                                        .connect(httpMethod, url, httpRequest -> clientRequest.writeTo(httpRequest, this.strategies))
105                                        .doOnRequest(n -> logRequest(clientRequest))
106                                        .doOnCancel(() -> logger.debug(logPrefix + "Cancel signal (to close connection)"))
107                                        .map(httpResponse -> {
108                                                logResponse(httpResponse, logPrefix);
109                                                return new DefaultClientResponse(
110                                                                httpResponse, this.strategies, logPrefix, httpMethod.name() + " " + url,
111                                                                () -> createRequest(clientRequest));
112                                        });
113                }
114
115                private void logRequest(ClientRequest request) {
116                        LogFormatUtils.traceDebug(logger, traceOn ->
117                                        request.logPrefix() + "HTTP " + request.method() + " " + request.url() +
118                                                        (traceOn ? ", headers=" + formatHeaders(request.headers()) : "")
119                        );
120                }
121
122                private void logResponse(ClientHttpResponse response, String logPrefix) {
123                        LogFormatUtils.traceDebug(logger, traceOn -> {
124                                int code = response.getRawStatusCode();
125                                HttpStatus status = HttpStatus.resolve(code);
126                                return logPrefix + "Response " + (status != null ? status : code) +
127                                                (traceOn ? ", headers=" + formatHeaders(response.getHeaders()) : "");
128                        });
129                }
130
131                private String formatHeaders(HttpHeaders headers) {
132                        return this.enableLoggingRequestDetails ? headers.toString() : headers.isEmpty() ? "{}" : "{masked}";
133                }
134
135                private HttpRequest createRequest(ClientRequest request) {
136                        return new HttpRequest() {
137
138                                @Override
139                                public HttpMethod getMethod() {
140                                        return request.method();
141                                }
142
143                                @Override
144                                public String getMethodValue() {
145                                        return request.method().name();
146                                }
147
148                                @Override
149                                public URI getURI() {
150                                        return request.url();
151                                }
152
153                                @Override
154                                public HttpHeaders getHeaders() {
155                                        return request.headers();
156                                }
157                        };
158                }
159        }
160
161}