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.view;
018
019import java.util.HashMap;
020import java.util.List;
021import java.util.Locale;
022import java.util.Map;
023import java.util.TimeZone;
024
025import org.springframework.context.MessageSource;
026import org.springframework.context.MessageSourceResolvable;
027import org.springframework.context.NoSuchMessageException;
028import org.springframework.context.i18n.LocaleContext;
029import org.springframework.context.i18n.TimeZoneAwareLocaleContext;
030import org.springframework.http.server.reactive.ServerHttpRequest;
031import org.springframework.lang.Nullable;
032import org.springframework.util.Assert;
033import org.springframework.util.StringUtils;
034import org.springframework.validation.BindException;
035import org.springframework.validation.BindingResult;
036import org.springframework.validation.Errors;
037import org.springframework.web.bind.EscapedErrors;
038import org.springframework.web.server.ServerWebExchange;
039import org.springframework.web.util.HtmlUtils;
040import org.springframework.web.util.UriComponentsBuilder;
041
042/**
043 * Context holder for request-specific state, like the {@link MessageSource} to
044 * use, current locale, binding errors, etc. Provides easy access to localized
045 * messages and Errors instances.
046 *
047 * <p>Suitable for exposition to views, and usage within FreeMarker templates
048 * and tag libraries.
049 *
050 * <p>Can be instantiated manually or automatically exposed to views as a model
051 * attribute via AbstractView's "requestContextAttribute" property.
052 *
053 * @author Rossen Stoyanchev
054 * @since 5.0
055 */
056public class RequestContext {
057
058        private final ServerWebExchange exchange;
059
060        private final Map<String, Object> model;
061
062        private final MessageSource messageSource;
063
064        private Locale locale;
065
066        private TimeZone timeZone;
067
068        @Nullable
069        private Boolean defaultHtmlEscape;
070
071        @Nullable
072        private Map<String, Errors> errorsMap;
073
074        @Nullable
075        private RequestDataValueProcessor dataValueProcessor;
076
077
078        public RequestContext(ServerWebExchange exchange, Map<String, Object> model, MessageSource messageSource) {
079                this(exchange, model, messageSource, null);
080        }
081
082        public RequestContext(ServerWebExchange exchange, Map<String, Object> model, MessageSource messageSource,
083                        @Nullable RequestDataValueProcessor dataValueProcessor) {
084
085                Assert.notNull(exchange, "ServerWebExchange is required");
086                Assert.notNull(model, "Model is required");
087                Assert.notNull(messageSource, "MessageSource is required");
088                this.exchange = exchange;
089                this.model = model;
090                this.messageSource = messageSource;
091
092                LocaleContext localeContext = exchange.getLocaleContext();
093                Locale locale = localeContext.getLocale();
094                this.locale = (locale != null ? locale : Locale.getDefault());
095                TimeZone timeZone = (localeContext instanceof TimeZoneAwareLocaleContext ?
096                                ((TimeZoneAwareLocaleContext) localeContext).getTimeZone() : null);
097                this.timeZone = (timeZone != null ? timeZone : TimeZone.getDefault());
098
099                this.defaultHtmlEscape = null;  // TODO
100                this.dataValueProcessor = dataValueProcessor;
101        }
102
103
104        protected final ServerWebExchange getExchange() {
105                return this.exchange;
106        }
107
108        /**
109         * Return the MessageSource in use with this request.
110         */
111        public MessageSource getMessageSource() {
112                return this.messageSource;
113        }
114
115        /**
116         * Return the model Map that this RequestContext encapsulates, if any.
117         * @return the populated model Map, or {@code null} if none available
118         */
119        @Nullable
120        public Map<String, Object> getModel() {
121                return this.model;
122        }
123
124        /**
125         * Return the current Locale.
126         */
127        public final Locale getLocale() {
128                return this.locale;
129        }
130
131        /**
132         * Return the current TimeZone.
133         */
134        public TimeZone getTimeZone() {
135                return this.timeZone;
136        }
137
138        /**
139         * Change the current locale to the specified one.
140         */
141        public void changeLocale(Locale locale) {
142                this.locale = locale;
143        }
144
145        /**
146         * Change the current locale to the specified locale and time zone context.
147         */
148        public void changeLocale(Locale locale, TimeZone timeZone) {
149                this.locale = locale;
150                this.timeZone = timeZone;
151        }
152
153        /**
154         * (De)activate default HTML escaping for messages and errors, for the scope
155         * of this RequestContext.
156         * <p>TODO: currently no application-wide setting ...
157         */
158        public void setDefaultHtmlEscape(boolean defaultHtmlEscape) {
159                this.defaultHtmlEscape = defaultHtmlEscape;
160        }
161
162        /**
163         * Is default HTML escaping active? Falls back to {@code false} in case of
164         * no explicit default given.
165         */
166        public boolean isDefaultHtmlEscape() {
167                return (this.defaultHtmlEscape != null && this.defaultHtmlEscape.booleanValue());
168        }
169
170        /**
171         * Return the default HTML escape setting, differentiating between no default
172         * specified and an explicit value.
173         * @return whether default HTML escaping is enabled (null = no explicit default)
174         */
175        @Nullable
176        public Boolean getDefaultHtmlEscape() {
177                return this.defaultHtmlEscape;
178        }
179
180        /**
181         * Return the {@link RequestDataValueProcessor} instance to apply to in form
182         * tag libraries and to redirect URLs.
183         */
184        @Nullable
185        public RequestDataValueProcessor getRequestDataValueProcessor() {
186                return this.dataValueProcessor;
187        }
188
189        /**
190         * Return the context path of the current web application. This is
191         * useful for building links to other resources within the application.
192         * <p>Delegates to {@link ServerHttpRequest#getPath()}.
193         */
194        public String getContextPath() {
195                return this.exchange.getRequest().getPath().contextPath().value();
196        }
197
198        /**
199         * Return a context-aware URl for the given relative URL.
200         * @param relativeUrl the relative URL part
201         * @return a URL that points back to the current web application with an
202         * absolute path also URL-encoded accordingly
203         */
204        public String getContextUrl(String relativeUrl) {
205                String url = StringUtils.applyRelativePath(getContextPath() + "/", relativeUrl);
206                return getExchange().transformUrl(url);
207        }
208
209        /**
210         * Return a context-aware URl for the given relative URL with placeholders --
211         * named keys with braces {@code {}}. For example, send in a relative URL
212         * {@code foo/{bar}?spam={spam}} and a parameter map {@code {bar=baz,spam=nuts}}
213         * and the result will be {@code [contextpath]/foo/baz?spam=nuts}.
214         * @param relativeUrl the relative URL part
215         * @param params a map of parameters to insert as placeholders in the url
216         * @return a URL that points back to the current web application with an
217         * absolute path also URL-encoded accordingly
218         */
219        public String getContextUrl(String relativeUrl, Map<String, ?> params) {
220                String url = StringUtils.applyRelativePath(getContextPath() + "/", relativeUrl);
221                url = UriComponentsBuilder.fromUriString(url).buildAndExpand(params).encode().toUri().toASCIIString();
222                return getExchange().transformUrl(url);
223        }
224
225        /**
226         * Return the request path of the request. This is useful as HTML form
227         * action target, also in combination with the original query string.
228         */
229        public String getRequestPath() {
230                return this.exchange.getRequest().getURI().getPath();
231        }
232
233        /**
234         * Return the query string of the current request. This is useful for
235         * building an HTML form action target in combination with the original
236         * request path.
237         */
238        public String getQueryString() {
239                return this.exchange.getRequest().getURI().getQuery();
240        }
241
242        /**
243         * Retrieve the message for the given code, using the "defaultHtmlEscape" setting.
244         * @param code the code of the message
245         * @param defaultMessage the String to return if the lookup fails
246         * @return the message
247         */
248        public String getMessage(String code, String defaultMessage) {
249                return getMessage(code, null, defaultMessage, isDefaultHtmlEscape());
250        }
251
252        /**
253         * Retrieve the message for the given code, using the "defaultHtmlEscape" setting.
254         * @param code the code of the message
255         * @param args arguments for the message, or {@code null} if none
256         * @param defaultMessage the String to return if the lookup fails
257         * @return the message
258         */
259        public String getMessage(String code, @Nullable Object[] args, String defaultMessage) {
260                return getMessage(code, args, defaultMessage, isDefaultHtmlEscape());
261        }
262
263        /**
264         * Retrieve the message for the given code, using the "defaultHtmlEscape" setting.
265         * @param code the code of the message
266         * @param args arguments for the message as a List, or {@code null} if none
267         * @param defaultMessage the String to return if the lookup fails
268         * @return the message
269         */
270        public String getMessage(String code, @Nullable List<?> args, String defaultMessage) {
271                return getMessage(code, (args != null ? args.toArray() : null), defaultMessage, isDefaultHtmlEscape());
272        }
273
274        /**
275         * Retrieve the message for the given code.
276         * @param code the code of the message
277         * @param args arguments for the message, or {@code null} if none
278         * @param defaultMessage the String to return if the lookup fails
279         * @param htmlEscape if the message should be HTML-escaped
280         * @return the message
281         */
282        public String getMessage(String code, @Nullable Object[] args, String defaultMessage, boolean htmlEscape) {
283                String msg = this.messageSource.getMessage(code, args, defaultMessage, this.locale);
284                if (msg == null) {
285                        return "";
286                }
287                return (htmlEscape ? HtmlUtils.htmlEscape(msg) : msg);
288        }
289
290        /**
291         * Retrieve the message for the given code, using the "defaultHtmlEscape" setting.
292         * @param code the code of the message
293         * @return the message
294         * @throws org.springframework.context.NoSuchMessageException if not found
295         */
296        public String getMessage(String code) throws NoSuchMessageException {
297                return getMessage(code, null, isDefaultHtmlEscape());
298        }
299
300        /**
301         * Retrieve the message for the given code, using the "defaultHtmlEscape" setting.
302         * @param code the code of the message
303         * @param args arguments for the message, or {@code null} if none
304         * @return the message
305         * @throws org.springframework.context.NoSuchMessageException if not found
306         */
307        public String getMessage(String code, @Nullable Object[] args) throws NoSuchMessageException {
308                return getMessage(code, args, isDefaultHtmlEscape());
309        }
310
311        /**
312         * Retrieve the message for the given code, using the "defaultHtmlEscape" setting.
313         * @param code the code of the message
314         * @param args arguments for the message as a List, or {@code null} if none
315         * @return the message
316         * @throws org.springframework.context.NoSuchMessageException if not found
317         */
318        public String getMessage(String code, @Nullable List<?> args) throws NoSuchMessageException {
319                return getMessage(code, (args != null ? args.toArray() : null), isDefaultHtmlEscape());
320        }
321
322        /**
323         * Retrieve the message for the given code.
324         * @param code the code of the message
325         * @param args arguments for the message, or {@code null} if none
326         * @param htmlEscape if the message should be HTML-escaped
327         * @return the message
328         * @throws org.springframework.context.NoSuchMessageException if not found
329         */
330        public String getMessage(String code, @Nullable Object[] args, boolean htmlEscape) throws NoSuchMessageException {
331                String msg = this.messageSource.getMessage(code, args, this.locale);
332                return (htmlEscape ? HtmlUtils.htmlEscape(msg) : msg);
333        }
334
335        /**
336         * Retrieve the given MessageSourceResolvable (e.g. an ObjectError instance), using the "defaultHtmlEscape" setting.
337         * @param resolvable the MessageSourceResolvable
338         * @return the message
339         * @throws org.springframework.context.NoSuchMessageException if not found
340         */
341        public String getMessage(MessageSourceResolvable resolvable) throws NoSuchMessageException {
342                return getMessage(resolvable, isDefaultHtmlEscape());
343        }
344
345        /**
346         * Retrieve the given MessageSourceResolvable (e.g. an ObjectError instance).
347         * @param resolvable the MessageSourceResolvable
348         * @param htmlEscape if the message should be HTML-escaped
349         * @return the message
350         * @throws org.springframework.context.NoSuchMessageException if not found
351         */
352        public String getMessage(MessageSourceResolvable resolvable, boolean htmlEscape) throws NoSuchMessageException {
353                String msg = this.messageSource.getMessage(resolvable, this.locale);
354                return (htmlEscape ? HtmlUtils.htmlEscape(msg) : msg);
355        }
356
357        /**
358         * Retrieve the Errors instance for the given bind object, using the
359         * "defaultHtmlEscape" setting.
360         * @param name the name of the bind object
361         * @return the Errors instance, or {@code null} if not found
362         */
363        @Nullable
364        public Errors getErrors(String name) {
365                return getErrors(name, isDefaultHtmlEscape());
366        }
367
368        /**
369         * Retrieve the Errors instance for the given bind object.
370         * @param name the name of the bind object
371         * @param htmlEscape create an Errors instance with automatic HTML escaping?
372         * @return the Errors instance, or {@code null} if not found
373         */
374        @Nullable
375        public Errors getErrors(String name, boolean htmlEscape) {
376                if (this.errorsMap == null) {
377                        this.errorsMap = new HashMap<>();
378                }
379
380                Errors errors = this.errorsMap.get(name);
381                if (errors == null) {
382                        errors = getModelObject(BindingResult.MODEL_KEY_PREFIX + name);
383                        if (errors == null) {
384                                return null;
385                        }
386                }
387
388                if (errors instanceof BindException) {
389                        errors = ((BindException) errors).getBindingResult();
390                }
391
392                if (htmlEscape && !(errors instanceof EscapedErrors)) {
393                        errors = new EscapedErrors(errors);
394                }
395                else if (!htmlEscape && errors instanceof EscapedErrors) {
396                        errors = ((EscapedErrors) errors).getSource();
397                }
398
399                this.errorsMap.put(name, errors);
400                return errors;
401        }
402
403        /**
404         * Retrieve the model object for the given model name, either from the model
405         * or from the request attributes.
406         * @param modelName the name of the model object
407         * @return the model object
408         */
409        @SuppressWarnings("unchecked")
410        @Nullable
411        protected <T> T getModelObject(String modelName) {
412                T modelObject = (T) this.model.get(modelName);
413                if (modelObject == null) {
414                        modelObject = this.exchange.getAttribute(modelName);
415                }
416                return modelObject;
417        }
418
419        /**
420         * Create a BindStatus for the given bind object using the
421         * "defaultHtmlEscape" setting.
422         * @param path the bean and property path for which values and errors will
423         * be resolved (e.g. "person.age")
424         * @return the new BindStatus instance
425         * @throws IllegalStateException if no corresponding Errors object found
426         */
427        public BindStatus getBindStatus(String path) throws IllegalStateException {
428                return new BindStatus(this, path, isDefaultHtmlEscape());
429        }
430
431        /**
432         * Create a BindStatus for the given bind object, using the
433         * "defaultHtmlEscape" setting.
434         * @param path the bean and property path for which values and errors will
435         * be resolved (e.g. "person.age")
436         * @param htmlEscape create a BindStatus with automatic HTML escaping?
437         * @return the new BindStatus instance
438         * @throws IllegalStateException if no corresponding Errors object found
439         */
440        public BindStatus getBindStatus(String path, boolean htmlEscape) throws IllegalStateException {
441                return new BindStatus(this, path, htmlEscape);
442        }
443
444}