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.result;
018
019import java.util.ArrayList;
020import java.util.Arrays;
021import java.util.Comparator;
022import java.util.LinkedHashSet;
023import java.util.List;
024import java.util.Set;
025import java.util.function.Supplier;
026
027import org.apache.commons.logging.Log;
028import org.apache.commons.logging.LogFactory;
029
030import org.springframework.core.Ordered;
031import org.springframework.core.ReactiveAdapter;
032import org.springframework.core.ReactiveAdapterRegistry;
033import org.springframework.http.MediaType;
034import org.springframework.lang.Nullable;
035import org.springframework.util.Assert;
036import org.springframework.web.reactive.HandlerMapping;
037import org.springframework.web.reactive.HandlerResult;
038import org.springframework.web.reactive.accept.RequestedContentTypeResolver;
039import org.springframework.web.server.ServerWebExchange;
040
041/**
042 * Base class for {@link org.springframework.web.reactive.HandlerResultHandler
043 * HandlerResultHandler} with support for content negotiation and access to a
044 * {@code ReactiveAdapter} registry.
045 *
046 * @author Rossen Stoyanchev
047 * @since 5.0
048 */
049public abstract class HandlerResultHandlerSupport implements Ordered {
050
051        private static final List<MediaType> ALL_APPLICATION_MEDIA_TYPES =
052                        Arrays.asList(MediaType.ALL, new MediaType("application"));
053
054
055        protected final Log logger = LogFactory.getLog(getClass());
056
057        private final RequestedContentTypeResolver contentTypeResolver;
058
059        private final ReactiveAdapterRegistry adapterRegistry;
060
061        private int order = LOWEST_PRECEDENCE;
062
063
064        protected HandlerResultHandlerSupport(RequestedContentTypeResolver contentTypeResolver,
065                        ReactiveAdapterRegistry adapterRegistry) {
066
067                Assert.notNull(contentTypeResolver, "RequestedContentTypeResolver is required");
068                Assert.notNull(adapterRegistry, "ReactiveAdapterRegistry is required");
069                this.contentTypeResolver = contentTypeResolver;
070                this.adapterRegistry = adapterRegistry;
071        }
072
073
074        /**
075         * Return the configured {@link ReactiveAdapterRegistry}.
076         */
077        public ReactiveAdapterRegistry getAdapterRegistry() {
078                return this.adapterRegistry;
079        }
080
081        /**
082         * Return the configured {@link RequestedContentTypeResolver}.
083         */
084        public RequestedContentTypeResolver getContentTypeResolver() {
085                return this.contentTypeResolver;
086        }
087
088        /**
089         * Set the order for this result handler relative to others.
090         * <p>By default set to {@link Ordered#LOWEST_PRECEDENCE}, however see
091         * Javadoc of sub-classes which may change this default.
092         * @param order the order
093         */
094        public void setOrder(int order) {
095                this.order = order;
096        }
097
098        @Override
099        public int getOrder() {
100                return this.order;
101        }
102
103
104        /**
105         * Get a {@code ReactiveAdapter} for the top-level return value type.
106         * @return the matching adapter, or {@code null} if none
107         */
108        @Nullable
109        protected ReactiveAdapter getAdapter(HandlerResult result) {
110                return getAdapterRegistry().getAdapter(result.getReturnType().resolve(), result.getReturnValue());
111        }
112
113        /**
114         * Select the best media type for the current request through a content negotiation algorithm.
115         * @param exchange the current request
116         * @param producibleTypesSupplier the media types that can be produced for the current request
117         * @return the selected media type, or {@code null} if none
118         */
119        @Nullable
120        protected MediaType selectMediaType(
121                        ServerWebExchange exchange, Supplier<List<MediaType>> producibleTypesSupplier) {
122
123                MediaType contentType = exchange.getResponse().getHeaders().getContentType();
124                if (contentType != null && contentType.isConcrete()) {
125                        if (logger.isDebugEnabled()) {
126                                logger.debug(exchange.getLogPrefix() + "Found 'Content-Type:" + contentType + "' in response");
127                        }
128                        return contentType;
129                }
130
131                List<MediaType> acceptableTypes = getAcceptableTypes(exchange);
132                List<MediaType> producibleTypes = getProducibleTypes(exchange, producibleTypesSupplier);
133
134                Set<MediaType> compatibleMediaTypes = new LinkedHashSet<>();
135                for (MediaType acceptable : acceptableTypes) {
136                        for (MediaType producible : producibleTypes) {
137                                if (acceptable.isCompatibleWith(producible)) {
138                                        compatibleMediaTypes.add(selectMoreSpecificMediaType(acceptable, producible));
139                                }
140                        }
141                }
142
143                List<MediaType> result = new ArrayList<>(compatibleMediaTypes);
144                MediaType.sortBySpecificityAndQuality(result);
145
146                MediaType selected = null;
147                for (MediaType mediaType : result) {
148                        if (mediaType.isConcrete()) {
149                                selected = mediaType;
150                                break;
151                        }
152                        else if (mediaType.isPresentIn(ALL_APPLICATION_MEDIA_TYPES)) {
153                                selected = MediaType.APPLICATION_OCTET_STREAM;
154                                break;
155                        }
156                }
157
158                if (selected != null) {
159                        selected = selected.removeQualityValue();
160                        if (logger.isDebugEnabled()) {
161                                logger.debug("Using '" + selected + "' given " + acceptableTypes +
162                                                " and supported " + producibleTypes);
163                        }
164                }
165                else if (logger.isDebugEnabled()) {
166                        logger.debug(exchange.getLogPrefix() +
167                                        "No match for " + acceptableTypes + ", supported: " + producibleTypes);
168                }
169
170                return selected;
171        }
172
173        private List<MediaType> getAcceptableTypes(ServerWebExchange exchange) {
174                return getContentTypeResolver().resolveMediaTypes(exchange);
175        }
176
177        private List<MediaType> getProducibleTypes(
178                        ServerWebExchange exchange, Supplier<List<MediaType>> producibleTypesSupplier) {
179
180                Set<MediaType> mediaTypes = exchange.getAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE);
181                return (mediaTypes != null ? new ArrayList<>(mediaTypes) : producibleTypesSupplier.get());
182        }
183
184        private MediaType selectMoreSpecificMediaType(MediaType acceptable, MediaType producible) {
185                producible = producible.copyQualityValue(acceptable);
186                Comparator<MediaType> comparator = MediaType.SPECIFICITY_COMPARATOR;
187                return (comparator.compare(acceptable, producible) <= 0 ? acceptable : producible);
188        }
189
190}