/*
* 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.io.UnsupportedEncodingException;
import java.net.URI;
import java.nio.charset.Charset;
import java.util.Collections;
import java.util.Locale;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import reactor.core.publisher.Mono;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.reactive.HandlerMapping;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.util.UriComponentsBuilder;
import org.springframework.web.util.UriUtils;
/**
* View that redirects to an absolute or context relative URL. The URL may be a URI
* template in which case the URI template variables will be replaced with values
* available in the model.
*
*
A URL for this view is supposed to be a HTTP redirect which does the redirect via
* sending an {@link HttpStatus#SEE_OTHER} code. If HTTP 1.0 compatibility is needed,
* {@link HttpStatus#FOUND} code can be set via {@link #setStatusCode(HttpStatus)}.
*
*
Note that the default value for the "contextRelative" flag is true.
* With the flag on, URLs starting with "/" are considered relative to the web application
* context path, while with this flag off they are considered relative to the web server
* root.
*
* @author Sebastien Deleuze
* @see #setContextRelative
* @since 5.0
*/
public class RedirectView extends AbstractUrlBasedView {
private static final Pattern URI_TEMPLATE_VARIABLE_PATTERN = Pattern.compile("\\{([^/]+?)\\}");
private boolean contextRelative = true;
private HttpStatus statusCode = HttpStatus.SEE_OTHER;
private boolean propagateQueryParams = false;
private String[] hosts;
/**
* Create a new {@code RedirectView} with the given redirect URL.
*
* @see #builder(String)
*/
public RedirectView(String redirectUrl) {
super(redirectUrl);
}
/**
* Return a builder for a {@code RedirectView}.
*/
public static Builder builder(String redirectUrl) {
return new BuilderImpl(redirectUrl);
}
/**
* Set whether to interpret a given URL that starts with a slash ("/")
* as relative to the current context path.
*
Default is "true": the context path will be
* prepended to the URL in such a case. If "false", an URL that starts
* with a slash will be interpreted as absolute, i.e. taken as-is.
*/
public void setContextRelative(boolean contextRelative) {
this.contextRelative = contextRelative;
}
/**
* Set a customized redirect status code to be used for a redirect. Default is
* {@link HttpStatus#SEE_OTHER} which is the correct code for HTTP 1.1
* clients. This setter can be used to configure {@link HttpStatus#FOUND}
* if HTTP 1.0 clients need to be supported, or any other {@literal 3xx}
* status code.
*/
public void setStatusCode(HttpStatus statusCode) {
Assert.notNull(statusCode);
this.statusCode = statusCode;
}
/**
* Get the redirect status code.
*/
public HttpStatus getStatusCode() {
return statusCode;
}
/**
* When set to {@code true} the query string of the current URL is appended
* and thus propagated through to the redirected URL.
*
Defaults to {@code false}.
*/
public void setPropagateQueryParams(boolean propagateQueryParams) {
this.propagateQueryParams = propagateQueryParams;
}
/**
* Configure one or more hosts associated with the application.
* All other hosts will be considered external hosts.
*
In effect, this property provides a way turn off encoding via
* {@link javax.servlet.http.HttpServletResponse#encodeRedirectURL} for URLs that have a
* host and that host is not listed as a known host when using a Servlet based engine.
*
If not set (the default) all URLs are encoded through the response.
* @param hosts one or more application hosts
*/
public void setHosts(String... hosts) {
this.hosts = hosts;
}
/**
* Convert model to request parameters and redirect to the given URL.
* @see #sendRedirect
*/
@Override
protected Mono renderInternal(Map model, MediaType contentType,
ServerWebExchange exchange) {
String targetUrl = createTargetUrl(model, exchange);
return sendRedirect(exchange, targetUrl);
}
/**
* Create the target URL by checking if the redirect string is a URI template first,
* expanding it with the given model, and then optionally appending simple type model
* attributes as query String parameters.
*/
protected final String createTargetUrl(Map model, ServerWebExchange exchange) {
ServerHttpRequest request = exchange.getRequest();
// Prepare target URL.
StringBuilder targetUrl = new StringBuilder();
if (this.contextRelative && getUrl().startsWith("/")) {
// Do not apply context path to relative URLs.
targetUrl.append(request.getContextPath());
}
targetUrl.append(getUrl());
Charset charset = this.getDefaultCharset();
if (StringUtils.hasText(targetUrl)) {
Map variables = getCurrentRequestUriVariables(exchange);
targetUrl = replaceUriTemplateVariables(targetUrl.toString(), model, variables, charset);
}
if (this.propagateQueryParams) {
appendCurrentQueryParams(targetUrl, request);
}
return targetUrl.toString();
}
/**
* Replace URI template variables in the target URL with encoded model
* attributes or URI variables from the current request. Model attributes
* referenced in the URL are removed from the model.
* @param targetUrl the redirect URL
* @param model Map that contains model attributes
* @param currentUriVariables current request URI variables to use
* @param charset the charset to use
*/
protected StringBuilder replaceUriTemplateVariables(String targetUrl,
Map model, Map currentUriVariables, Charset charset) {
StringBuilder result = new StringBuilder();
Matcher matcher = URI_TEMPLATE_VARIABLE_PATTERN.matcher(targetUrl);
int endLastMatch = 0;
while (matcher.find()) {
String name = matcher.group(1);
Object value = (model.containsKey(name) ? model.remove(name) : currentUriVariables.get(name));
if (value == null) {
throw new IllegalArgumentException("Model has no value for key '" + name + "'");
}
result.append(targetUrl.substring(endLastMatch, matcher.start()));
try {
result.append(UriUtils.encodePathSegment(value.toString(), charset.name()));
}
catch (UnsupportedEncodingException ex) {
throw new IllegalStateException(ex);
}
endLastMatch = matcher.end();
}
result.append(targetUrl.substring(endLastMatch, targetUrl.length()));
return result;
}
@SuppressWarnings("unchecked")
private Map getCurrentRequestUriVariables(ServerWebExchange exchange) {
String name = HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE;
return (Map) exchange.getAttribute(name).orElse(Collections.emptyMap());
}
/**
* Append the query string of the current request to the target redirect URL.
* @param targetUrl the StringBuilder to append the properties to
* @param request the current request
*/
protected void appendCurrentQueryParams(StringBuilder targetUrl, ServerHttpRequest request) {
String query = request.getURI().getQuery();
if (StringUtils.hasText(query)) {
// Extract anchor fragment, if any.
String fragment = null;
int anchorIndex = targetUrl.indexOf("#");
if (anchorIndex > -1) {
fragment = targetUrl.substring(anchorIndex);
targetUrl.delete(anchorIndex, targetUrl.length());
}
if (targetUrl.toString().indexOf('?') < 0) {
targetUrl.append('?').append(query);
}
else {
targetUrl.append('&').append(query);
}
// Append anchor fragment, if any, to end of URL.
if (fragment != null) {
targetUrl.append(fragment);
}
}
}
/**
* Send a redirect back to the HTTP client
* @param exchange current HTTP exchange
* @param targetUrl the target URL to redirect to
*/
protected Mono sendRedirect(ServerWebExchange exchange, String targetUrl) {
ServerHttpResponse response = exchange.getResponse();
// TODO Support encoding redirect URL as ServerHttpResponse level when SPR-14529 will be fixed
response.getHeaders().setLocation(URI.create(targetUrl));
response.setStatusCode(this.statusCode);
return Mono.empty();
}
/**
* Whether the given targetUrl has a host that is a "foreign" system in which
* case {@link javax.servlet.http.HttpServletResponse#encodeRedirectURL} will not be applied.
* This method returns {@code true} if the {@link #setHosts(String[])}
* property is configured and the target URL has a host that does not match.
* @param targetUrl the target redirect URL
* @return {@code true} the target URL has a remote host, {@code false} if it
* the URL does not have a host or the "host" property is not configured.
*/
protected boolean isRemoteHost(String targetUrl) {
if (ObjectUtils.isEmpty(this.hosts)) {
return false;
}
String targetHost = UriComponentsBuilder.fromUriString(targetUrl).build().getHost();
if (StringUtils.isEmpty(targetHost)) {
return false;
}
for (String host : this.hosts) {
if (targetHost.equals(host)) {
return false;
}
}
return true;
}
@Override
public boolean checkResourceExists(Locale locale) throws Exception {
return true;
}
public interface Builder {
/**
* @see RedirectView#setContextRelative(boolean)
*/
Builder contextRelative(boolean contextRelative);
/**
* @see RedirectView#setStatusCode(HttpStatus)
*/
Builder statusCode(HttpStatus statusCode);
/**
* @see RedirectView#setPropagateQueryParams(boolean)
*/
Builder propagateQueryParams(boolean propagateQueryParams);
/**
* @see RedirectView#setHosts(String...)
*/
Builder hosts(String... hosts);
/**
* Build the redirect view.
*/
RedirectView build();
}
private static class BuilderImpl implements Builder {
private final RedirectView view;
public BuilderImpl(String redirectUrl) {
this.view = new RedirectView(redirectUrl);
}
@Override
public Builder contextRelative(boolean contextRelative) {
this.view.setContextRelative(contextRelative);
return this;
}
@Override
public Builder statusCode(HttpStatus statusCode) {
this.view.setStatusCode(statusCode);
return this;
}
@Override
public Builder propagateQueryParams(boolean propagateQueryParams) {
this.view.setPropagateQueryParams(propagateQueryParams);
return this;
}
@Override
public Builder hosts(String... hosts) {
this.view.setHosts(hosts);
return this;
}
@Override
public RedirectView build() {
return this.view;
}
}
}