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}