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}