From 8ad95b09e82f9f2f2b833cb22193d40ed6f6d0f3 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Wed, 30 Nov 2016 14:58:12 -0500 Subject: [PATCH] ReactiveContext and BindStatus in spring-web-reactive Issue: SPR-14533 --- .../reactive/result/view/AbstractView.java | 37 ++ .../web/reactive/result/view/BindStatus.java | 342 +++++++++++++++ .../reactive/result/view/RequestContext.java | 414 ++++++++++++++++++ .../result/view/UrlBasedViewResolver.java | 21 + .../result/view/RequestContextTests.java | 86 ++++ 5 files changed, 900 insertions(+) create mode 100644 spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/BindStatus.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/RequestContext.java create mode 100644 spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/RequestContextTests.java diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/AbstractView.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/AbstractView.java index e286bf14be..a3bfeb7ded 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/AbstractView.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/AbstractView.java @@ -49,6 +49,8 @@ public abstract class AbstractView implements View, ApplicationContextAware { private Charset defaultCharset = StandardCharsets.UTF_8; + private String requestContextAttribute; + private ApplicationContext applicationContext; @@ -95,6 +97,21 @@ public abstract class AbstractView implements View, ApplicationContextAware { return this.defaultCharset; } + /** + * Set the name of the RequestContext attribute for this view. + * Default is none. + */ + public void setRequestContextAttribute(String requestContextAttribute) { + this.requestContextAttribute = requestContextAttribute; + } + + /** + * Return the name of the RequestContext attribute, if any. + */ + public String getRequestContextAttribute() { + return this.requestContextAttribute; + } + @Override public void setApplicationContext(ApplicationContext applicationContext) { this.applicationContext = applicationContext; @@ -126,6 +143,12 @@ public abstract class AbstractView implements View, ApplicationContextAware { } Map mergedModel = getModelAttributes(model, exchange); + + // Expose RequestContext? + if (this.requestContextAttribute != null) { + mergedModel.put(this.requestContextAttribute, createRequestContext(exchange, mergedModel)); + } + return renderInternal(mergedModel, contentType, exchange); } @@ -145,6 +168,20 @@ public abstract class AbstractView implements View, ApplicationContextAware { return attributes; } + /** + * Create a RequestContext to expose under the specified attribute name. + *

The default implementation creates a standard RequestContext instance for the + * given request and model. Can be overridden in subclasses for custom instances. + * @param exchange current exchange + * @param model combined output Map (never {@code null}), + * with dynamic values taking precedence over static attributes + * @return the RequestContext instance + * @see #setRequestContextAttribute + */ + protected RequestContext createRequestContext(ServerWebExchange exchange, Map model) { + return new RequestContext(exchange, model, this.applicationContext); + } + /** * Subclasses must implement this method to actually render the view. * @param renderAttributes combined output Map (never {@code null}), diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/BindStatus.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/BindStatus.java new file mode 100644 index 0000000000..34cbcfee64 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/BindStatus.java @@ -0,0 +1,342 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.web.reactive.result.view; + +import java.beans.PropertyEditor; +import java.util.Arrays; +import java.util.List; + +import org.springframework.beans.BeanWrapper; +import org.springframework.beans.PropertyAccessorFactory; +import org.springframework.context.NoSuchMessageException; +import org.springframework.util.StringUtils; +import org.springframework.validation.BindingResult; +import org.springframework.validation.Errors; +import org.springframework.validation.ObjectError; +import org.springframework.web.util.HtmlUtils; + +/** + * Simple adapter to expose the bind status of a field or object. + * Set as a variable by FreeMarker macros and other tag libraries. + * + *

Obviously, object status representations (i.e. errors at the object level + * rather than the field level) do not have an expression and a value but only + * error codes and messages. For simplicity's sake and to be able to use the same + * tags and macros, the same status class is used for both scenarios. + * + * @author Rossen Stoyanchev + * @since 5.0 + * @see RequestContext#getBindStatus + */ +public class BindStatus { + + private final RequestContext requestContext; + + private final String path; + + private final boolean htmlEscape; + + private final String expression; + + private final Errors errors; + + + private BindingResult bindingResult; + + private Object value; + + private Class valueType; + + private Object actualValue; + + private PropertyEditor editor; + + private List objectErrors; + + private String[] errorCodes; + + private String[] errorMessages; + + + /** + * Create a new BindStatus instance, representing a field or object status. + * @param requestContext the current RequestContext + * @param path the bean and property path for which values and errors + * will be resolved (e.g. "customer.address.street") + * @param htmlEscape whether to HTML-escape error messages and string values + * @throws IllegalStateException if no corresponding Errors object found + */ + public BindStatus(RequestContext requestContext, String path, boolean htmlEscape) + throws IllegalStateException { + + this.requestContext = requestContext; + this.path = path; + this.htmlEscape = htmlEscape; + + // determine name of the object and property + String beanName; + int dotPos = path.indexOf('.'); + if (dotPos == -1) { + // property not set, only the object itself + beanName = path; + this.expression = null; + } + else { + beanName = path.substring(0, dotPos); + this.expression = path.substring(dotPos + 1); + } + + this.errors = requestContext.getErrors(beanName, false).orElse(null); + + if (this.errors != null) { + // Usual case: A BindingResult is available as request attribute. + // Can determine error codes and messages for the given expression. + // Can use a custom PropertyEditor, as registered by a form controller. + if (this.expression != null) { + if ("*".equals(this.expression)) { + this.objectErrors = this.errors.getAllErrors(); + } + else if (this.expression.endsWith("*")) { + this.objectErrors = this.errors.getFieldErrors(this.expression); + } + else { + this.objectErrors = this.errors.getFieldErrors(this.expression); + this.value = this.errors.getFieldValue(this.expression); + this.valueType = this.errors.getFieldType(this.expression); + if (this.errors instanceof BindingResult) { + this.bindingResult = (BindingResult) this.errors; + this.actualValue = this.bindingResult.getRawFieldValue(this.expression); + this.editor = this.bindingResult.findEditor(this.expression, null); + } + else { + this.actualValue = this.value; + } + } + } + else { + this.objectErrors = this.errors.getGlobalErrors(); + } + this.errorCodes = initErrorCodes(this.objectErrors); + } + + else { + // No BindingResult available as request attribute: + // Probably forwarded directly to a form view. + // Let's do the best we can: extract a plain target if appropriate. + Object target = requestContext.getModelObject(beanName); + if (target == null) { + throw new IllegalStateException( + "Neither BindingResult nor plain target object for bean name '" + + beanName + "' available as request attribute"); + } + if (this.expression != null && !"*".equals(this.expression) && !this.expression.endsWith("*")) { + BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(target); + this.value = bw.getPropertyValue(this.expression); + this.valueType = bw.getPropertyType(this.expression); + this.actualValue = this.value; + } + this.errorCodes = new String[0]; + this.errorMessages = new String[0]; + } + + if (htmlEscape && this.value instanceof String) { + this.value = HtmlUtils.htmlEscape((String) this.value); + } + } + + /** + * Extract the error codes from the ObjectError list. + */ + private static String[] initErrorCodes(List objectErrors) { + String[] errorCodes = new String[objectErrors.size()]; + for (int i = 0; i < objectErrors.size(); i++) { + ObjectError error = objectErrors.get(i); + errorCodes[i] = error.getCode(); + } + return errorCodes; + } + + + /** + * Return the bean and property path for which values and errors + * will be resolved (e.g. "customer.address.street"). + */ + public String getPath() { + return this.path; + } + + /** + * Return a bind expression that can be used in HTML forms as input name + * for the respective field, or {@code null} if not field-specific. + *

Returns a bind path appropriate for resubmission, e.g. "address.street". + * Note that the complete bind path as required by the bind tag is + * "customer.address.street", if bound to a "customer" bean. + */ + public String getExpression() { + return this.expression; + } + + /** + * Return the current value of the field, i.e. either the property value + * or a rejected update, or {@code null} if not field-specific. + *

This value will be an HTML-escaped String if the original value + * already was a String. + */ + public Object getValue() { + return this.value; + } + + /** + * Get the '{@code Class}' type of the field. Favor this instead of + * '{@code getValue().getClass()}' since '{@code getValue()}' may + * return '{@code null}'. + */ + public Class getValueType() { + return this.valueType; + } + + /** + * Return the actual value of the field, i.e. the raw property value, + * or {@code null} if not available. + */ + public Object getActualValue() { + return this.actualValue; + } + + /** + * Return a suitable display value for the field, i.e. the stringified + * value if not null, and an empty string in case of a null value. + *

This value will be an HTML-escaped String if the original value + * was non-null: the {@code toString} result of the original value + * will get HTML-escaped. + */ + public String getDisplayValue() { + if (this.value instanceof String) { + return (String) this.value; + } + if (this.value != null) { + return (this.htmlEscape ? + HtmlUtils.htmlEscape(this.value.toString()) : this.value.toString()); + } + return ""; + } + + /** + * Return if this status represents a field or object error. + */ + public boolean isError() { + return (this.errorCodes != null && this.errorCodes.length > 0); + } + + /** + * Return the error codes for the field or object, if any. + * Returns an empty array instead of null if none. + */ + public String[] getErrorCodes() { + return this.errorCodes; + } + + /** + * Return the first error codes for the field or object, if any. + */ + public String getErrorCode() { + return (this.errorCodes.length > 0 ? this.errorCodes[0] : ""); + } + + /** + * Return the resolved error messages for the field or object, + * if any. Returns an empty array instead of null if none. + */ + public String[] getErrorMessages() { + initErrorMessages(); + return this.errorMessages; + } + + /** + * Return the first error message for the field or object, if any. + */ + public String getErrorMessage() { + initErrorMessages(); + return (this.errorMessages.length > 0 ? this.errorMessages[0] : ""); + } + + /** + * Return an error message string, concatenating all messages + * separated by the given delimiter. + * @param delimiter separator string, e.g. ", " or "
" + * @return the error message string + */ + public String getErrorMessagesAsString(String delimiter) { + initErrorMessages(); + return StringUtils.arrayToDelimitedString(this.errorMessages, delimiter); + } + + /** + * Extract the error messages from the ObjectError list. + */ + private void initErrorMessages() throws NoSuchMessageException { + if (this.errorMessages == null) { + this.errorMessages = new String[this.objectErrors.size()]; + for (int i = 0; i < this.objectErrors.size(); i++) { + ObjectError error = this.objectErrors.get(i); + this.errorMessages[i] = this.requestContext.getMessage(error, this.htmlEscape); + } + } + } + + /** + * Return the Errors instance (typically a BindingResult) that this + * bind status is currently associated with. + * @return the current Errors instance, or {@code null} if none + * @see org.springframework.validation.BindingResult + */ + public Errors getErrors() { + return this.errors; + } + + /** + * Return the PropertyEditor for the property that this bind status + * is currently bound to. + * @return the current PropertyEditor, or {@code null} if none + */ + public PropertyEditor getEditor() { + return this.editor; + } + + /** + * Find a PropertyEditor for the given value class, associated with + * the property that this bound status is currently bound to. + * @param valueClass the value class that an editor is needed for + * @return the associated PropertyEditor, or {@code null} if none + */ + public PropertyEditor findEditor(Class valueClass) { + return (this.bindingResult != null ? + this.bindingResult.findEditor(this.expression, valueClass) : null); + } + + + @Override + public String toString() { + StringBuilder sb = new StringBuilder("BindStatus: "); + sb.append("expression=[").append(this.expression).append("]; "); + sb.append("value=[").append(this.value).append("]"); + if (isError()) { + sb.append("; errorCodes=").append(Arrays.asList(this.errorCodes)); + } + return sb.toString(); + } + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/RequestContext.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/RequestContext.java new file mode 100644 index 0000000000..6ff4daf22c --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/RequestContext.java @@ -0,0 +1,414 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.web.reactive.result.view; + +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Optional; +import java.util.TimeZone; + +import org.springframework.context.MessageSource; +import org.springframework.context.MessageSourceResolvable; +import org.springframework.context.NoSuchMessageException; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.util.Assert; +import org.springframework.validation.BindException; +import org.springframework.validation.BindingResult; +import org.springframework.validation.Errors; +import org.springframework.web.bind.EscapedErrors; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.util.HtmlUtils; +import org.springframework.web.util.UriTemplate; + +/** + * Context holder for request-specific state, like the {@link MessageSource} to + * use, current locale, binding errors, etc. Provides easy access to localized + * messages and Errors instances. + * + *

Suitable for exposition to views, and usage within FreeMarker templates, + * and tag libraries. + * + *

Can be instantiated manually, or automatically exposed to views as model + * attribute via AbstractView's "requestContextAttribute" property. + * + * @author Rossen Stoyanchev + * @since 5.0 + */ +public class RequestContext { + + private final ServerWebExchange exchange; + + private final Map model; + + private final MessageSource messageSource; + + private Boolean defaultHtmlEscape; + + private Map errorsMap; + + private Locale locale; + + private TimeZone timeZone; + + + public RequestContext(ServerWebExchange exchange, Map model, + MessageSource messageSource) { + + Assert.notNull(exchange, "'exchange' is required"); + Assert.notNull(model, "'model' is required"); + Assert.notNull(messageSource, "'messageSource' is required"); + this.exchange = exchange; + this.model = model; + this.messageSource = messageSource; + this.defaultHtmlEscape = null; // TODO + this.locale = Locale.getDefault(); // TODO + this.timeZone = TimeZone.getDefault(); // TODO + } + + + protected final ServerWebExchange getExchange() { + return this.exchange; + } + + /** + * Return the MessageSource in use with this request. + */ + public MessageSource getMessageSource() { + return this.messageSource; + } + + /** + * Return the model Map that this RequestContext encapsulates, if any. + * @return the populated model Map, or {@code null} if none available + */ + public Map getModel() { + return this.model; + } + + /** + * Return the current Locale. + * TODO: currently this is Locale.getDefault() + */ + public final Locale getLocale() { + return this.locale; + } + + /** + * Return the current TimeZone. + * TODO: currently this is the Timezone.getDefault() + */ + public TimeZone getTimeZone() { + return this.timeZone; + } + + /** + * Change the current locale to the specified one. + * TODO: currently simply change the internal field + */ + public void changeLocale(Locale locale) { + this.locale = locale; + } + + /** + * Change the current locale to the specified locale and time zone context. + * TODO: currently simply change the internal fields + */ + public void changeLocale(Locale locale, TimeZone timeZone) { + this.locale = locale; + this.timeZone = timeZone; + } + + /** + * (De)activate default HTML escaping for messages and errors, for the scope + * of this RequestContext. + *

TODO: currently no application-wide setting ... + */ + public void setDefaultHtmlEscape(boolean defaultHtmlEscape) { + this.defaultHtmlEscape = defaultHtmlEscape; + } + + /** + * Is default HTML escaping active? Falls back to {@code false} in case of + * no explicit default given. + */ + public boolean isDefaultHtmlEscape() { + return (this.defaultHtmlEscape != null && this.defaultHtmlEscape.booleanValue()); + } + + /** + * Return the default HTML escape setting, differentiating between no default + * specified and an explicit value. + * @return whether default HTML escaping is enabled (null = no explicit default) + */ + public Boolean getDefaultHtmlEscape() { + return this.defaultHtmlEscape; + } + + /** + * Return the context path of the the current web application. This is + * useful for building links to other resources within the application. + *

Delegates to {@link ServerHttpRequest#getContextPath()}. + */ + public String getContextPath() { + return this.exchange.getRequest().getContextPath(); + } + + /** + * Return a context-aware URl for the given relative URL. + * @param relativeUrl the relative URL part + * @return a URL that points back to the current web application with an + * absolute path also URL-encoded accordingly + */ + public String getContextUrl(String relativeUrl) { + String url = getContextPath() + relativeUrl; + // TODO: this.response.encodeURL(url) + return url; + } + + /** + * Return a context-aware URl for the given relative URL with placeholders -- + * named keys with braces {@code {}}. For example, send in a relative URL + * {@code foo/{bar}?spam={spam}} and a parameter map {@code {bar=baz,spam=nuts}} + * and the result will be {@code [contextpath]/foo/baz?spam=nuts}. + * @param relativeUrl the relative URL part + * @param params a map of parameters to insert as placeholders in the url + * @return a URL that points back to the current web application with an + * absolute path also URL-encoded accordingly + */ + public String getContextUrl(String relativeUrl, Map params) { + String url = getContextPath() + relativeUrl; + UriTemplate template = new UriTemplate(url); + url = template.expand(params).toASCIIString(); + // TODO: this.response.encodeURL(url) + return url; + } + + /** + * Return the request path of the request. This is useful as HTML form + * action target, also in combination with the original query string. + */ + public String getRequestPath() { + return this.exchange.getRequest().getURI().getPath(); + } + + /** + * Return the query string of the current request. This is useful for + * building an HTML form action target in combination with the original + * request path. + */ + public String getQueryString() { + return this.exchange.getRequest().getURI().getQuery(); + } + + /** + * Retrieve the message for the given code, using the "defaultHtmlEscape" setting. + * @param code code of the message + * @param defaultMessage String to return if the lookup fails + * @return the message + */ + public String getMessage(String code, String defaultMessage) { + return getMessage(code, null, defaultMessage, isDefaultHtmlEscape()); + } + + /** + * Retrieve the message for the given code, using the "defaultHtmlEscape" setting. + * @param code code of the message + * @param args arguments for the message, or {@code null} if none + * @param defaultMessage String to return if the lookup fails + * @return the message + */ + public String getMessage(String code, Object[] args, String defaultMessage) { + return getMessage(code, args, defaultMessage, isDefaultHtmlEscape()); + } + + /** + * Retrieve the message for the given code, using the "defaultHtmlEscape" setting. + * @param code code of the message + * @param args arguments for the message as a List, or {@code null} if none + * @param defaultMessage String to return if the lookup fails + * @return the message + */ + public String getMessage(String code, List args, String defaultMessage) { + return getMessage(code, (args != null ? args.toArray() : null), defaultMessage, isDefaultHtmlEscape()); + } + + /** + * Retrieve the message for the given code. + * @param code code of the message + * @param args arguments for the message, or {@code null} if none + * @param defaultMessage String to return if the lookup fails + * @param htmlEscape HTML escape the message? + * @return the message + */ + public String getMessage(String code, Object[] args, String defaultMessage, boolean htmlEscape) { + String msg = this.messageSource.getMessage(code, args, defaultMessage, this.locale); + return (htmlEscape ? HtmlUtils.htmlEscape(msg) : msg); + } + + /** + * Retrieve the message for the given code, using the "defaultHtmlEscape" setting. + * @param code code of the message + * @return the message + * @throws org.springframework.context.NoSuchMessageException if not found + */ + public String getMessage(String code) throws NoSuchMessageException { + return getMessage(code, null, isDefaultHtmlEscape()); + } + + /** + * Retrieve the message for the given code, using the "defaultHtmlEscape" setting. + * @param code code of the message + * @param args arguments for the message, or {@code null} if none + * @return the message + * @throws org.springframework.context.NoSuchMessageException if not found + */ + public String getMessage(String code, Object[] args) throws NoSuchMessageException { + return getMessage(code, args, isDefaultHtmlEscape()); + } + + /** + * Retrieve the message for the given code, using the "defaultHtmlEscape" setting. + * @param code code of the message + * @param args arguments for the message as a List, or {@code null} if none + * @return the message + * @throws org.springframework.context.NoSuchMessageException if not found + */ + public String getMessage(String code, List args) throws NoSuchMessageException { + return getMessage(code, (args != null ? args.toArray() : null), isDefaultHtmlEscape()); + } + + /** + * Retrieve the message for the given code. + * @param code code of the message + * @param args arguments for the message, or {@code null} if none + * @param htmlEscape HTML escape the message? + * @return the message + * @throws org.springframework.context.NoSuchMessageException if not found + */ + public String getMessage(String code, Object[] args, boolean htmlEscape) throws NoSuchMessageException { + String msg = this.messageSource.getMessage(code, args, this.locale); + return (htmlEscape ? HtmlUtils.htmlEscape(msg) : msg); + } + + /** + * Retrieve the given MessageSourceResolvable (e.g. an ObjectError instance), using the "defaultHtmlEscape" setting. + * @param resolvable the MessageSourceResolvable + * @return the message + * @throws org.springframework.context.NoSuchMessageException if not found + */ + public String getMessage(MessageSourceResolvable resolvable) throws NoSuchMessageException { + return getMessage(resolvable, isDefaultHtmlEscape()); + } + + /** + * Retrieve the given MessageSourceResolvable (e.g. an ObjectError instance). + * @param resolvable the MessageSourceResolvable + * @param htmlEscape HTML escape the message? + * @return the message + * @throws org.springframework.context.NoSuchMessageException if not found + */ + public String getMessage(MessageSourceResolvable resolvable, boolean htmlEscape) throws NoSuchMessageException { + String msg = this.messageSource.getMessage(resolvable, this.locale); + return (htmlEscape ? HtmlUtils.htmlEscape(msg) : msg); + } + + /** + * Retrieve the Errors instance for the given bind object, using the + * "defaultHtmlEscape" setting. + * @param name name of the bind object + * @return the Errors instance, or {@code null} if not found + */ + public Optional getErrors(String name) { + return getErrors(name, isDefaultHtmlEscape()); + } + + /** + * Retrieve the Errors instance for the given bind object. + * @param name name of the bind object + * @param htmlEscape create an Errors instance with automatic HTML escaping? + * @return the Errors instance, or {@code null} if not found + */ + public Optional getErrors(String name, boolean htmlEscape) { + if (this.errorsMap == null) { + this.errorsMap = new HashMap<>(); + } + + // Since there is no Optional orElse + flatMap... + Optional optional = Optional.ofNullable(this.errorsMap.get(name)); + optional = optional.isPresent() ? optional : getModelObject(BindingResult.MODEL_KEY_PREFIX + name); + + return optional + .map(errors -> { + if (errors instanceof BindException) { + return ((BindException) errors).getBindingResult(); + } + else { + return errors; + } + }) + .map(errors -> { + if (htmlEscape && !(errors instanceof EscapedErrors)) { + errors = new EscapedErrors(errors); + } + else if (!htmlEscape && errors instanceof EscapedErrors) { + errors = ((EscapedErrors) errors).getSource(); + } + this.errorsMap.put(name, errors); + return errors; + }); + } + + /** + * Retrieve the model object for the given model name, either from the model + * or from the request attributes. + * @param modelName the name of the model object + * @return the model object + */ + @SuppressWarnings("unchecked") + protected Optional getModelObject(String modelName) { + return Optional.ofNullable(this.model) + .map(model -> Optional.ofNullable((T) model.get(modelName))) + .orElse(this.exchange.getAttribute(modelName)); + } + + /** + * Create a BindStatus for the given bind object using the + * "defaultHtmlEscape" setting. + * @param path the bean and property path for which values and errors will + * be resolved (e.g. "person.age") + * @return the new BindStatus instance + * @throws IllegalStateException if no corresponding Errors object found + */ + public BindStatus getBindStatus(String path) throws IllegalStateException { + return new BindStatus(this, path, isDefaultHtmlEscape()); + } + + /** + * Create a BindStatus for the given bind object, using the + * "defaultHtmlEscape" setting. + * @param path the bean and property path for which values and errors will + * be resolved (e.g. "person.age") + * @param htmlEscape create a BindStatus with automatic HTML escaping? + * @return the new BindStatus instance + * @throws IllegalStateException if no corresponding Errors object found + */ + public BindStatus getBindStatus(String path, boolean htmlEscape) throws IllegalStateException { + return new BindStatus(this, path, htmlEscape); + } + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/UrlBasedViewResolver.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/UrlBasedViewResolver.java index dac13e0269..b357738bb4 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/UrlBasedViewResolver.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/UrlBasedViewResolver.java @@ -77,6 +77,8 @@ public class UrlBasedViewResolver extends ViewResolverSupport implements ViewRes private Function redirectViewProvider = url -> new RedirectView(url); + private String requestContextAttribute; + /** * Set the view class to instantiate through {@link #createUrlBasedView(String)}. @@ -162,6 +164,23 @@ public class UrlBasedViewResolver extends ViewResolverSupport implements ViewRes this.redirectViewProvider = redirectViewProvider; } + /** + * Set the name of the RequestContext attribute for all views. + * @param requestContextAttribute name of the RequestContext attribute + * @see AbstractView#setRequestContextAttribute + */ + public void setRequestContextAttribute(String requestContextAttribute) { + this.requestContextAttribute = requestContextAttribute; + } + + /** + * Return the name of the RequestContext attribute for all views, if any. + */ + protected String getRequestContextAttribute() { + return this.requestContextAttribute; + } + + @Override public void afterPropertiesSet() throws Exception { if (getViewClass() == null) { @@ -169,6 +188,7 @@ public class UrlBasedViewResolver extends ViewResolverSupport implements ViewRes } } + @Override public Mono resolveViewName(String viewName, Locale locale) { if (!canHandle(viewName, locale)) { @@ -221,6 +241,7 @@ public class UrlBasedViewResolver extends ViewResolverSupport implements ViewRes protected AbstractUrlBasedView createUrlBasedView(String viewName) { AbstractUrlBasedView view = (AbstractUrlBasedView) BeanUtils.instantiateClass(getViewClass()); view.setSupportedMediaTypes(getSupportedMediaTypes()); + view.setRequestContextAttribute(getRequestContextAttribute()); view.setDefaultCharset(getDefaultCharset()); view.setUrl(getPrefix() + viewName + getSuffix()); return view; diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/RequestContextTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/RequestContextTests.java new file mode 100644 index 0000000000..171d0db7a5 --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/RequestContextTests.java @@ -0,0 +1,86 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.web.reactive.result.view; + +import java.util.HashMap; +import java.util.Map; + +import org.junit.Before; +import org.junit.Test; + +import org.springframework.context.support.GenericApplicationContext; +import org.springframework.mock.http.server.reactive.test.MockServerHttpRequest; +import org.springframework.mock.http.server.reactive.test.MockServerHttpResponse; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.adapter.DefaultServerWebExchange; +import org.springframework.web.server.session.DefaultWebSessionManager; + +import static org.junit.Assert.assertEquals; + +/** + * Unit tests for {@link RequestContext}. + * @author Rossen Stoyanchev + */ +public class RequestContextTests { + + private ServerWebExchange exchange; + + private MockServerHttpRequest request; + + private GenericApplicationContext applicationContext; + + private Map model = new HashMap<>(); + + + @Before + public void init() { + this.request = new MockServerHttpRequest(); + MockServerHttpResponse response = new MockServerHttpResponse(); + DefaultWebSessionManager sessionManager = new DefaultWebSessionManager(); + this.exchange = new DefaultServerWebExchange(this.request, response, sessionManager); + this.applicationContext = new GenericApplicationContext(); + this.applicationContext.refresh(); + } + + @Test + public void testGetContextUrl() throws Exception { + this.request.setContextPath("foo/"); + RequestContext context = new RequestContext(this.exchange, this.model, this.applicationContext); + assertEquals("foo/bar", context.getContextUrl("bar")); + } + + @Test + public void testGetContextUrlWithMap() throws Exception { + this.request.setContextPath("foo/"); + RequestContext context = new RequestContext(this.exchange, this.model, this.applicationContext); + Map map = new HashMap<>(); + map.put("foo", "bar"); + map.put("spam", "bucket"); + assertEquals("foo/bar?spam=bucket", context.getContextUrl("{foo}?spam={spam}", map)); + } + + @Test + public void testGetContextUrlWithMapEscaping() throws Exception { + this.request.setContextPath("foo/"); + RequestContext context = new RequestContext(this.exchange, this.model, this.applicationContext); + Map map = new HashMap<>(); + map.put("foo", "bar baz"); + map.put("spam", "&bucket="); + assertEquals("foo/bar%20baz?spam=%26bucket%3D", context.getContextUrl("{foo}?spam={spam}", map)); + } + +} -- GitLab