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.server.adapter;
018
019import java.util.ArrayList;
020import java.util.Arrays;
021import java.util.List;
022import java.util.function.Consumer;
023import java.util.stream.Collectors;
024
025import org.springframework.beans.factory.NoSuchBeanDefinitionException;
026import org.springframework.context.ApplicationContext;
027import org.springframework.core.annotation.AnnotationAwareOrderComparator;
028import org.springframework.http.codec.ServerCodecConfigurer;
029import org.springframework.http.server.reactive.HttpHandler;
030import org.springframework.lang.Nullable;
031import org.springframework.util.Assert;
032import org.springframework.util.ObjectUtils;
033import org.springframework.web.server.ServerWebExchange;
034import org.springframework.web.server.WebExceptionHandler;
035import org.springframework.web.server.WebFilter;
036import org.springframework.web.server.WebHandler;
037import org.springframework.web.server.handler.ExceptionHandlingWebHandler;
038import org.springframework.web.server.handler.FilteringWebHandler;
039import org.springframework.web.server.i18n.LocaleContextResolver;
040import org.springframework.web.server.session.DefaultWebSessionManager;
041import org.springframework.web.server.session.WebSessionManager;
042
043/**
044 * This builder has two purposes:
045 *
046 * <p>One is to assemble a processing chain that consists of a target {@link WebHandler},
047 * then decorated with a set of {@link WebFilter WebFilters}, then further decorated with
048 * a set of {@link WebExceptionHandler WebExceptionHandlers}.
049 *
050 * <p>The second purpose is to adapt the resulting processing chain to an {@link HttpHandler}:
051 * the lowest-level reactive HTTP handling abstraction which can then be used with any of the
052 * supported runtimes. The adaptation is done with the help of {@link HttpWebHandlerAdapter}.
053 *
054 * <p>The processing chain can be assembled manually via builder methods, or detected from
055 * a Spring {@link ApplicationContext} via {@link #applicationContext}, or a mix of both.
056 *
057 * @author Rossen Stoyanchev
058 * @author Sebastien Deleuze
059 * @since 5.0
060 * @see HttpWebHandlerAdapter
061 */
062public final class WebHttpHandlerBuilder {
063
064        /** Well-known name for the target WebHandler in the bean factory. */
065        public static final String WEB_HANDLER_BEAN_NAME = "webHandler";
066
067        /** Well-known name for the WebSessionManager in the bean factory. */
068        public static final String WEB_SESSION_MANAGER_BEAN_NAME = "webSessionManager";
069
070        /** Well-known name for the ServerCodecConfigurer in the bean factory. */
071        public static final String SERVER_CODEC_CONFIGURER_BEAN_NAME = "serverCodecConfigurer";
072
073        /** Well-known name for the LocaleContextResolver in the bean factory. */
074        public static final String LOCALE_CONTEXT_RESOLVER_BEAN_NAME = "localeContextResolver";
075
076        /** Well-known name for the ForwardedHeaderTransformer in the bean factory. */
077        public static final String FORWARDED_HEADER_TRANSFORMER_BEAN_NAME = "forwardedHeaderTransformer";
078
079
080        private final WebHandler webHandler;
081
082        @Nullable
083        private final ApplicationContext applicationContext;
084
085        private final List<WebFilter> filters = new ArrayList<>();
086
087        private final List<WebExceptionHandler> exceptionHandlers = new ArrayList<>();
088
089        @Nullable
090        private WebSessionManager sessionManager;
091
092        @Nullable
093        private ServerCodecConfigurer codecConfigurer;
094
095        @Nullable
096        private LocaleContextResolver localeContextResolver;
097
098        @Nullable
099        private ForwardedHeaderTransformer forwardedHeaderTransformer;
100
101
102        /**
103         * Private constructor to use when initialized from an ApplicationContext.
104         */
105        private WebHttpHandlerBuilder(WebHandler webHandler, @Nullable ApplicationContext applicationContext) {
106                Assert.notNull(webHandler, "WebHandler must not be null");
107                this.webHandler = webHandler;
108                this.applicationContext = applicationContext;
109        }
110
111        /**
112         * Copy constructor.
113         */
114        private WebHttpHandlerBuilder(WebHttpHandlerBuilder other) {
115                this.webHandler = other.webHandler;
116                this.applicationContext = other.applicationContext;
117                this.filters.addAll(other.filters);
118                this.exceptionHandlers.addAll(other.exceptionHandlers);
119                this.sessionManager = other.sessionManager;
120                this.codecConfigurer = other.codecConfigurer;
121                this.localeContextResolver = other.localeContextResolver;
122                this.forwardedHeaderTransformer = other.forwardedHeaderTransformer;
123        }
124
125
126        /**
127         * Static factory method to create a new builder instance.
128         * @param webHandler the target handler for the request
129         * @return the prepared builder
130         */
131        public static WebHttpHandlerBuilder webHandler(WebHandler webHandler) {
132                return new WebHttpHandlerBuilder(webHandler, null);
133        }
134
135        /**
136         * Static factory method to create a new builder instance by detecting beans
137         * in an {@link ApplicationContext}. The following are detected:
138         * <ul>
139         * <li>{@link WebHandler} [1] -- looked up by the name
140         * {@link #WEB_HANDLER_BEAN_NAME}.
141         * <li>{@link WebFilter} [0..N] -- detected by type and ordered,
142         * see {@link AnnotationAwareOrderComparator}.
143         * <li>{@link WebExceptionHandler} [0..N] -- detected by type and
144         * ordered.
145         * <li>{@link WebSessionManager} [0..1] -- looked up by the name
146         * {@link #WEB_SESSION_MANAGER_BEAN_NAME}.
147         * <li>{@link ServerCodecConfigurer} [0..1] -- looked up by the name
148         * {@link #SERVER_CODEC_CONFIGURER_BEAN_NAME}.
149         * <li>{@link LocaleContextResolver} [0..1] -- looked up by the name
150         * {@link #LOCALE_CONTEXT_RESOLVER_BEAN_NAME}.
151         * </ul>
152         * @param context the application context to use for the lookup
153         * @return the prepared builder
154         */
155        public static WebHttpHandlerBuilder applicationContext(ApplicationContext context) {
156                WebHttpHandlerBuilder builder = new WebHttpHandlerBuilder(
157                                context.getBean(WEB_HANDLER_BEAN_NAME, WebHandler.class), context);
158
159                List<WebFilter> webFilters = context
160                                .getBeanProvider(WebFilter.class)
161                                .orderedStream()
162                                .collect(Collectors.toList());
163                builder.filters(filters -> filters.addAll(webFilters));
164                List<WebExceptionHandler> exceptionHandlers = context
165                                .getBeanProvider(WebExceptionHandler.class)
166                                .orderedStream()
167                                .collect(Collectors.toList());
168                builder.exceptionHandlers(handlers -> handlers.addAll(exceptionHandlers));
169
170                try {
171                        builder.sessionManager(
172                                        context.getBean(WEB_SESSION_MANAGER_BEAN_NAME, WebSessionManager.class));
173                }
174                catch (NoSuchBeanDefinitionException ex) {
175                        // Fall back on default
176                }
177
178                try {
179                        builder.codecConfigurer(
180                                        context.getBean(SERVER_CODEC_CONFIGURER_BEAN_NAME, ServerCodecConfigurer.class));
181                }
182                catch (NoSuchBeanDefinitionException ex) {
183                        // Fall back on default
184                }
185
186                try {
187                        builder.localeContextResolver(
188                                        context.getBean(LOCALE_CONTEXT_RESOLVER_BEAN_NAME, LocaleContextResolver.class));
189                }
190                catch (NoSuchBeanDefinitionException ex) {
191                        // Fall back on default
192                }
193
194                try {
195                        builder.forwardedHeaderTransformer(
196                                        context.getBean(FORWARDED_HEADER_TRANSFORMER_BEAN_NAME, ForwardedHeaderTransformer.class));
197                }
198                catch (NoSuchBeanDefinitionException ex) {
199                        // Fall back on default
200                }
201
202                return builder;
203        }
204
205
206        /**
207         * Add the given filter(s).
208         * @param filters the filter(s) to add that's
209         */
210        public WebHttpHandlerBuilder filter(WebFilter... filters) {
211                if (!ObjectUtils.isEmpty(filters)) {
212                        this.filters.addAll(Arrays.asList(filters));
213                        updateFilters();
214                }
215                return this;
216        }
217
218        /**
219         * Manipulate the "live" list of currently configured filters.
220         * @param consumer the consumer to use
221         */
222        public WebHttpHandlerBuilder filters(Consumer<List<WebFilter>> consumer) {
223                consumer.accept(this.filters);
224                updateFilters();
225                return this;
226        }
227
228        private void updateFilters() {
229                if (this.filters.isEmpty()) {
230                        return;
231                }
232
233                List<WebFilter> filtersToUse = this.filters.stream()
234                                .peek(filter -> {
235                                        if (filter instanceof ForwardedHeaderTransformer && this.forwardedHeaderTransformer == null) {
236                                                this.forwardedHeaderTransformer = (ForwardedHeaderTransformer) filter;
237                                        }
238                                })
239                                .filter(filter -> !(filter instanceof ForwardedHeaderTransformer))
240                                .collect(Collectors.toList());
241
242                this.filters.clear();
243                this.filters.addAll(filtersToUse);
244        }
245
246        /**
247         * Add the given exception handler(s).
248         * @param handlers the exception handler(s)
249         */
250        public WebHttpHandlerBuilder exceptionHandler(WebExceptionHandler... handlers) {
251                if (!ObjectUtils.isEmpty(handlers)) {
252                        this.exceptionHandlers.addAll(Arrays.asList(handlers));
253                }
254                return this;
255        }
256
257        /**
258         * Manipulate the "live" list of currently configured exception handlers.
259         * @param consumer the consumer to use
260         */
261        public WebHttpHandlerBuilder exceptionHandlers(Consumer<List<WebExceptionHandler>> consumer) {
262                consumer.accept(this.exceptionHandlers);
263                return this;
264        }
265
266        /**
267         * Configure the {@link WebSessionManager} to set on the
268         * {@link ServerWebExchange WebServerExchange}.
269         * <p>By default {@link DefaultWebSessionManager} is used.
270         * @param manager the session manager
271         * @see HttpWebHandlerAdapter#setSessionManager(WebSessionManager)
272         */
273        public WebHttpHandlerBuilder sessionManager(WebSessionManager manager) {
274                this.sessionManager = manager;
275                return this;
276        }
277
278        /**
279         * Whether a {@code WebSessionManager} is configured or not, either detected from an
280         * {@code ApplicationContext} or explicitly configured via {@link #sessionManager}.
281         * @since 5.0.9
282         */
283        public boolean hasSessionManager() {
284                return (this.sessionManager != null);
285        }
286
287        /**
288         * Configure the {@link ServerCodecConfigurer} to set on the {@code WebServerExchange}.
289         * @param codecConfigurer the codec configurer
290         */
291        public WebHttpHandlerBuilder codecConfigurer(ServerCodecConfigurer codecConfigurer) {
292                this.codecConfigurer = codecConfigurer;
293                return this;
294        }
295
296
297        /**
298         * Whether a {@code ServerCodecConfigurer} is configured or not, either detected from an
299         * {@code ApplicationContext} or explicitly configured via {@link #codecConfigurer}.
300         * @since 5.0.9
301         */
302        public boolean hasCodecConfigurer() {
303                return (this.codecConfigurer != null);
304        }
305
306        /**
307         * Configure the {@link LocaleContextResolver} to set on the
308         * {@link ServerWebExchange WebServerExchange}.
309         * @param localeContextResolver the locale context resolver
310         */
311        public WebHttpHandlerBuilder localeContextResolver(LocaleContextResolver localeContextResolver) {
312                this.localeContextResolver = localeContextResolver;
313                return this;
314        }
315
316        /**
317         * Whether a {@code LocaleContextResolver} is configured or not, either detected from an
318         * {@code ApplicationContext} or explicitly configured via {@link #localeContextResolver}.
319         * @since 5.0.9
320         */
321        public boolean hasLocaleContextResolver() {
322                return (this.localeContextResolver != null);
323        }
324
325        /**
326         * Configure the {@link ForwardedHeaderTransformer} for extracting and/or
327         * removing forwarded headers.
328         * @param transformer the transformer
329         * @since 5.1
330         */
331        public WebHttpHandlerBuilder forwardedHeaderTransformer(ForwardedHeaderTransformer transformer) {
332                this.forwardedHeaderTransformer = transformer;
333                return this;
334        }
335
336        /**
337         * Whether a {@code ForwardedHeaderTransformer} is configured or not, either
338         * detected from an {@code ApplicationContext} or explicitly configured via
339         * {@link #forwardedHeaderTransformer(ForwardedHeaderTransformer)}.
340         * @since 5.1
341         */
342        public boolean hasForwardedHeaderTransformer() {
343                return (this.forwardedHeaderTransformer != null);
344        }
345
346
347        /**
348         * Build the {@link HttpHandler}.
349         */
350        public HttpHandler build() {
351                WebHandler decorated = new FilteringWebHandler(this.webHandler, this.filters);
352                decorated = new ExceptionHandlingWebHandler(decorated,  this.exceptionHandlers);
353
354                HttpWebHandlerAdapter adapted = new HttpWebHandlerAdapter(decorated);
355                if (this.sessionManager != null) {
356                        adapted.setSessionManager(this.sessionManager);
357                }
358                if (this.codecConfigurer != null) {
359                        adapted.setCodecConfigurer(this.codecConfigurer);
360                }
361                if (this.localeContextResolver != null) {
362                        adapted.setLocaleContextResolver(this.localeContextResolver);
363                }
364                if (this.forwardedHeaderTransformer != null) {
365                        adapted.setForwardedHeaderTransformer(this.forwardedHeaderTransformer);
366                }
367                if (this.applicationContext != null) {
368                        adapted.setApplicationContext(this.applicationContext);
369                }
370                adapted.afterPropertiesSet();
371
372                return adapted;
373        }
374
375        /**
376         * Clone this {@link WebHttpHandlerBuilder}.
377         * @return the cloned builder instance
378         */
379        @Override
380        public WebHttpHandlerBuilder clone() {
381                return new WebHttpHandlerBuilder(this);
382        }
383
384}