001/* 002 * Copyright 2002-2018 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.http.client.reactive; 018 019import java.util.ArrayList; 020import java.util.List; 021import java.util.concurrent.atomic.AtomicReference; 022import java.util.function.Supplier; 023import java.util.stream.Collectors; 024 025import org.reactivestreams.Publisher; 026import reactor.core.publisher.Flux; 027import reactor.core.publisher.Mono; 028 029import org.springframework.http.HttpCookie; 030import org.springframework.http.HttpHeaders; 031import org.springframework.lang.Nullable; 032import org.springframework.util.Assert; 033import org.springframework.util.CollectionUtils; 034import org.springframework.util.LinkedMultiValueMap; 035import org.springframework.util.MultiValueMap; 036 037/** 038 * Base class for {@link ClientHttpRequest} implementations. 039 * 040 * @author Rossen Stoyanchev 041 * @author Brian Clozel 042 * @since 5.0 043 */ 044public abstract class AbstractClientHttpRequest implements ClientHttpRequest { 045 046 /** 047 * COMMITTING -> COMMITTED is the period after doCommit is called but before 048 * the response status and headers have been applied to the underlying 049 * response during which time pre-commit actions can still make changes to 050 * the response status and headers. 051 */ 052 private enum State {NEW, COMMITTING, COMMITTED} 053 054 055 private final HttpHeaders headers; 056 057 private final MultiValueMap<String, HttpCookie> cookies; 058 059 private final AtomicReference<State> state = new AtomicReference<>(State.NEW); 060 061 private final List<Supplier<? extends Publisher<Void>>> commitActions = new ArrayList<>(4); 062 063 064 public AbstractClientHttpRequest() { 065 this(new HttpHeaders()); 066 } 067 068 public AbstractClientHttpRequest(HttpHeaders headers) { 069 Assert.notNull(headers, "HttpHeaders must not be null"); 070 this.headers = headers; 071 this.cookies = new LinkedMultiValueMap<>(); 072 } 073 074 075 @Override 076 public HttpHeaders getHeaders() { 077 if (State.COMMITTED.equals(this.state.get())) { 078 return HttpHeaders.readOnlyHttpHeaders(this.headers); 079 } 080 return this.headers; 081 } 082 083 @Override 084 public MultiValueMap<String, HttpCookie> getCookies() { 085 if (State.COMMITTED.equals(this.state.get())) { 086 return CollectionUtils.unmodifiableMultiValueMap(this.cookies); 087 } 088 return this.cookies; 089 } 090 091 @Override 092 public void beforeCommit(Supplier<? extends Mono<Void>> action) { 093 Assert.notNull(action, "Action must not be null"); 094 this.commitActions.add(action); 095 } 096 097 @Override 098 public boolean isCommitted() { 099 return (this.state.get() != State.NEW); 100 } 101 102 /** 103 * A variant of {@link #doCommit(Supplier)} for a request without body. 104 * @return a completion publisher 105 */ 106 protected Mono<Void> doCommit() { 107 return doCommit(null); 108 } 109 110 /** 111 * Apply {@link #beforeCommit(Supplier) beforeCommit} actions, apply the 112 * request headers/cookies, and write the request body. 113 * @param writeAction the action to write the request body (may be {@code null}) 114 * @return a completion publisher 115 */ 116 protected Mono<Void> doCommit(@Nullable Supplier<? extends Publisher<Void>> writeAction) { 117 if (!this.state.compareAndSet(State.NEW, State.COMMITTING)) { 118 return Mono.empty(); 119 } 120 121 this.commitActions.add(() -> 122 Mono.fromRunnable(() -> { 123 applyHeaders(); 124 applyCookies(); 125 this.state.set(State.COMMITTED); 126 })); 127 128 if (writeAction != null) { 129 this.commitActions.add(writeAction); 130 } 131 132 List<? extends Publisher<Void>> actions = this.commitActions.stream() 133 .map(Supplier::get).collect(Collectors.toList()); 134 135 return Flux.concat(actions).then(); 136 } 137 138 139 /** 140 * Apply header changes from {@link #getHeaders()} to the underlying request. 141 * This method is called once only. 142 */ 143 protected abstract void applyHeaders(); 144 145 /** 146 * Add cookies from {@link #getHeaders()} to the underlying request. 147 * This method is called once only. 148 */ 149 protected abstract void applyCookies(); 150 151}