001/*
002 * Copyright 2002-2020 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.result.method.annotation;
018
019import java.lang.reflect.Method;
020import java.util.ArrayList;
021import java.util.List;
022import java.util.Set;
023
024import kotlin.reflect.KFunction;
025import kotlin.reflect.jvm.ReflectJvmMapping;
026import org.reactivestreams.Publisher;
027import reactor.core.publisher.Mono;
028
029import org.springframework.core.KotlinDetector;
030import org.springframework.core.MethodParameter;
031import org.springframework.core.ReactiveAdapter;
032import org.springframework.core.ReactiveAdapterRegistry;
033import org.springframework.core.ResolvableType;
034import org.springframework.core.codec.Hints;
035import org.springframework.http.MediaType;
036import org.springframework.http.codec.HttpMessageWriter;
037import org.springframework.http.converter.HttpMessageNotWritableException;
038import org.springframework.lang.Nullable;
039import org.springframework.util.Assert;
040import org.springframework.util.CollectionUtils;
041import org.springframework.web.reactive.HandlerMapping;
042import org.springframework.web.reactive.accept.RequestedContentTypeResolver;
043import org.springframework.web.reactive.result.HandlerResultHandlerSupport;
044import org.springframework.web.server.NotAcceptableStatusException;
045import org.springframework.web.server.ServerWebExchange;
046
047/**
048 * Abstract base class for result handlers that handle return values by writing
049 * to the response with {@link HttpMessageWriter}.
050 *
051 * @author Rossen Stoyanchev
052 * @author Sebastien Deleuze
053 * @since 5.0
054 */
055public abstract class AbstractMessageWriterResultHandler extends HandlerResultHandlerSupport {
056
057        private static final String COROUTINES_FLOW_CLASS_NAME = "kotlinx.coroutines.flow.Flow";
058
059        private final List<HttpMessageWriter<?>> messageWriters;
060
061
062        /**
063         * Constructor with {@link HttpMessageWriter HttpMessageWriters} and a
064         * {@code RequestedContentTypeResolver}.
065         * @param messageWriters for serializing Objects to the response body stream
066         * @param contentTypeResolver for resolving the requested content type
067         */
068        protected AbstractMessageWriterResultHandler(List<HttpMessageWriter<?>> messageWriters,
069                        RequestedContentTypeResolver contentTypeResolver) {
070
071                this(messageWriters, contentTypeResolver, ReactiveAdapterRegistry.getSharedInstance());
072        }
073
074        /**
075         * Constructor with an additional {@link ReactiveAdapterRegistry}.
076         * @param messageWriters for serializing Objects to the response body stream
077         * @param contentTypeResolver for resolving the requested content type
078         * @param adapterRegistry for adapting other reactive types (e.g. rx.Observable,
079         * rx.Single, etc.) to Flux or Mono
080         */
081        protected AbstractMessageWriterResultHandler(List<HttpMessageWriter<?>> messageWriters,
082                        RequestedContentTypeResolver contentTypeResolver, ReactiveAdapterRegistry adapterRegistry) {
083
084                super(contentTypeResolver, adapterRegistry);
085                Assert.notEmpty(messageWriters, "At least one message writer is required");
086                this.messageWriters = messageWriters;
087        }
088
089
090        /**
091         * Return the configured message converters.
092         */
093        public List<HttpMessageWriter<?>> getMessageWriters() {
094                return this.messageWriters;
095        }
096
097
098        /**
099         * Write a given body to the response with {@link HttpMessageWriter}.
100         * @param body the object to write
101         * @param bodyParameter the {@link MethodParameter} of the body to write
102         * @param exchange the current exchange
103         * @return indicates completion or error
104         * @see #writeBody(Object, MethodParameter, MethodParameter, ServerWebExchange)
105         */
106        protected Mono<Void> writeBody(@Nullable Object body, MethodParameter bodyParameter, ServerWebExchange exchange) {
107                return this.writeBody(body, bodyParameter, null, exchange);
108        }
109
110        /**
111         * Write a given body to the response with {@link HttpMessageWriter}.
112         * @param body the object to write
113         * @param bodyParameter the {@link MethodParameter} of the body to write
114         * @param actualParam the actual return type of the method that returned the value;
115         * could be different from {@code bodyParameter} when processing {@code HttpEntity}
116         * for example
117         * @param exchange the current exchange
118         * @return indicates completion or error
119         * @since 5.0.2
120         */
121        @SuppressWarnings({"unchecked", "rawtypes", "ConstantConditions"})
122        protected Mono<Void> writeBody(@Nullable Object body, MethodParameter bodyParameter,
123                        @Nullable MethodParameter actualParam, ServerWebExchange exchange) {
124
125                ResolvableType bodyType = ResolvableType.forMethodParameter(bodyParameter);
126                ResolvableType actualType = (actualParam != null ? ResolvableType.forMethodParameter(actualParam) : bodyType);
127                ReactiveAdapter adapter = getAdapterRegistry().getAdapter(bodyType.resolve(), body);
128
129                Publisher<?> publisher;
130                ResolvableType elementType;
131                ResolvableType actualElementType;
132                if (adapter != null) {
133                        publisher = adapter.toPublisher(body);
134                        boolean isUnwrapped = KotlinDetector.isKotlinReflectPresent() &&
135                                        KotlinDetector.isKotlinType(bodyParameter.getContainingClass()) &&
136                                        KotlinDelegate.isSuspend(bodyParameter.getMethod()) &&
137                                        !COROUTINES_FLOW_CLASS_NAME.equals(bodyType.toClass().getName());
138                        ResolvableType genericType = isUnwrapped ? bodyType : bodyType.getGeneric();
139                        elementType = getElementType(adapter, genericType);
140                        actualElementType = elementType;
141                }
142                else {
143                        publisher = Mono.justOrEmpty(body);
144                        actualElementType = body != null ? ResolvableType.forInstance(body) : bodyType;
145                        elementType = (bodyType.toClass() == Object.class && body != null ? actualElementType : bodyType);
146                }
147
148                if (elementType.resolve() == void.class || elementType.resolve() == Void.class) {
149                        return Mono.from((Publisher<Void>) publisher);
150                }
151
152                MediaType bestMediaType = selectMediaType(exchange, () -> getMediaTypesFor(elementType));
153                if (bestMediaType != null) {
154                        String logPrefix = exchange.getLogPrefix();
155                        if (logger.isDebugEnabled()) {
156                                logger.debug(logPrefix +
157                                                (publisher instanceof Mono ? "0..1" : "0..N") + " [" + elementType + "]");
158                        }
159                        for (HttpMessageWriter<?> writer : getMessageWriters()) {
160                                if (writer.canWrite(actualElementType, bestMediaType)) {
161                                        return writer.write((Publisher) publisher, actualType, elementType,
162                                                        bestMediaType, exchange.getRequest(), exchange.getResponse(),
163                                                        Hints.from(Hints.LOG_PREFIX_HINT, logPrefix));
164                                }
165                        }
166                }
167
168                MediaType contentType = exchange.getResponse().getHeaders().getContentType();
169                boolean isPresentMediaType = (contentType != null && contentType.equals(bestMediaType));
170                Set<MediaType> producibleTypes = exchange.getAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE);
171                if (isPresentMediaType || !CollectionUtils.isEmpty(producibleTypes)) {
172                        return Mono.error(new HttpMessageNotWritableException(
173                                        "No Encoder for [" + elementType + "] with preset Content-Type '" + contentType + "'"));
174                }
175
176                List<MediaType> mediaTypes = getMediaTypesFor(elementType);
177                if (bestMediaType == null && mediaTypes.isEmpty()) {
178                        return Mono.error(new IllegalStateException("No HttpMessageWriter for " + elementType));
179                }
180
181                return Mono.error(new NotAcceptableStatusException(mediaTypes));
182        }
183
184        private ResolvableType getElementType(ReactiveAdapter adapter, ResolvableType genericType) {
185                if (adapter.isNoValue()) {
186                        return ResolvableType.forClass(Void.class);
187                }
188                else if (genericType != ResolvableType.NONE) {
189                        return genericType;
190                }
191                else {
192                        return ResolvableType.forClass(Object.class);
193                }
194        }
195
196        private List<MediaType> getMediaTypesFor(ResolvableType elementType) {
197                List<MediaType> writableMediaTypes = new ArrayList<>();
198                for (HttpMessageWriter<?> converter : getMessageWriters()) {
199                        if (converter.canWrite(elementType, null)) {
200                                writableMediaTypes.addAll(converter.getWritableMediaTypes());
201                        }
202                }
203                return writableMediaTypes;
204        }
205
206
207        /**
208         * Inner class to avoid a hard dependency on Kotlin at runtime.
209         */
210        private static class KotlinDelegate {
211
212                static private boolean isSuspend(Method method) {
213                        KFunction<?> function = ReflectJvmMapping.getKotlinFunction(method);
214                        return function != null && function.isSuspend();
215                }
216        }
217
218}