All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.springframework.web.servlet.view.RedirectView Maven / Gradle / Ivy

There is a newer version: 6.1.6
Show newest version
/*
 * Copyright 2002-2020 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
 *
 *      https://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.servlet.view;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.Array;
import java.net.URLEncoder;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.beans.BeanUtils;
import org.springframework.http.HttpStatus;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.servlet.HandlerMapping;
import org.springframework.web.servlet.SmartView;
import org.springframework.web.servlet.View;
import org.springframework.web.servlet.support.RequestContextUtils;
import org.springframework.web.servlet.support.RequestDataValueProcessor;
import org.springframework.web.util.UriComponentsBuilder;
import org.springframework.web.util.UriUtils;
import org.springframework.web.util.WebUtils;

/**
 * View that redirects to an absolute, context relative, or current request
 * 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. By default
 * all primitive model attributes (or collections thereof) are exposed as HTTP
 * query parameters (assuming they've not been used as URI template variables),
 * but this behavior can be changed by overriding the
 * {@link #isEligibleProperty(String, Object)} method.
 *
 * 

A URL for this view is supposed to be an HTTP redirect URL, i.e. * suitable for HttpServletResponse's {@code sendRedirect} method, which * is what actually does the redirect if the HTTP 1.0 flag is on, or via sending * back an HTTP 303 code - if the HTTP 1.0 compatibility flag is off. * *

Note that while the default value for the "contextRelative" flag is off, * you will probably want to almost always set it to true. With the flag off, * URLs starting with "/" are considered relative to the web server root, while * with the flag on, they are considered relative to the web application root. * Since most web applications will never know or care what their context path * actually is, they are much better off setting this flag to true, and submitting * paths which are to be considered relative to the web application root. * *

NOTE when using this redirect view in a Portlet environment: Make sure * that your controller respects the Portlet {@code sendRedirect} constraints. * * @author Rod Johnson * @author Juergen Hoeller * @author Colin Sampaleanu * @author Sam Brannen * @author Arjen Poutsma * @author Rossen Stoyanchev * @see #setContextRelative * @see #setHttp10Compatible * @see #setExposeModelAttributes * @see javax.servlet.http.HttpServletResponse#sendRedirect */ public class RedirectView extends AbstractUrlBasedView implements SmartView { private static final Pattern URI_TEMPLATE_VARIABLE_PATTERN = Pattern.compile("\\{([^/]+?)\\}"); private boolean contextRelative = false; private boolean http10Compatible = true; private boolean exposeModelAttributes = true; @Nullable private String encodingScheme; @Nullable private HttpStatus statusCode; private boolean expandUriTemplateVariables = true; private boolean propagateQueryParams = false; @Nullable private String[] hosts; /** * Constructor for use as a bean. */ public RedirectView() { setExposePathVariables(false); } /** * Create a new RedirectView with the given URL. *

The given URL will be considered as relative to the web server, * not as relative to the current ServletContext. * @param url the URL to redirect to * @see #RedirectView(String, boolean) */ public RedirectView(String url) { super(url); setExposePathVariables(false); } /** * Create a new RedirectView with the given URL. * @param url the URL to redirect to * @param contextRelative whether to interpret the given URL as * relative to the current ServletContext */ public RedirectView(String url, boolean contextRelative) { super(url); this.contextRelative = contextRelative; setExposePathVariables(false); } /** * Create a new RedirectView with the given URL. * @param url the URL to redirect to * @param contextRelative whether to interpret the given URL as * relative to the current ServletContext * @param http10Compatible whether to stay compatible with HTTP 1.0 clients */ public RedirectView(String url, boolean contextRelative, boolean http10Compatible) { super(url); this.contextRelative = contextRelative; this.http10Compatible = http10Compatible; setExposePathVariables(false); } /** * Create a new RedirectView with the given URL. * @param url the URL to redirect to * @param contextRelative whether to interpret the given URL as * relative to the current ServletContext * @param http10Compatible whether to stay compatible with HTTP 1.0 clients * @param exposeModelAttributes whether or not model attributes should be * exposed as query parameters */ public RedirectView(String url, boolean contextRelative, boolean http10Compatible, boolean exposeModelAttributes) { super(url); this.contextRelative = contextRelative; this.http10Compatible = http10Compatible; this.exposeModelAttributes = exposeModelAttributes; setExposePathVariables(false); } /** * Set whether to interpret a given URL that starts with a slash ("/") * as relative to the current ServletContext, i.e. as relative to the * web application root. *

Default is "false": A URL that starts with a slash will be interpreted * as absolute, i.e. taken as-is. If "true", the context path will be * prepended to the URL in such a case. * @see javax.servlet.http.HttpServletRequest#getContextPath */ public void setContextRelative(boolean contextRelative) { this.contextRelative = contextRelative; } /** * Set whether to stay compatible with HTTP 1.0 clients. *

In the default implementation, this will enforce HTTP status code 302 * in any case, i.e. delegate to {@code HttpServletResponse.sendRedirect}. * Turning this off will send HTTP status code 303, which is the correct * code for HTTP 1.1 clients, but not understood by HTTP 1.0 clients. *

Many HTTP 1.1 clients treat 302 just like 303, not making any * difference. However, some clients depend on 303 when redirecting * after a POST request; turn this flag off in such a scenario. * @see javax.servlet.http.HttpServletResponse#sendRedirect */ public void setHttp10Compatible(boolean http10Compatible) { this.http10Compatible = http10Compatible; } /** * Set the {@code exposeModelAttributes} flag which denotes whether * or not model attributes should be exposed as HTTP query parameters. *

Defaults to {@code true}. */ public void setExposeModelAttributes(final boolean exposeModelAttributes) { this.exposeModelAttributes = exposeModelAttributes; } /** * Set the encoding scheme for this view. *

Default is the request's encoding scheme * (which is ISO-8859-1 if not specified otherwise). */ public void setEncodingScheme(String encodingScheme) { this.encodingScheme = encodingScheme; } /** * Set the status code for this view. *

Default is to send 302/303, depending on the value of the * {@link #setHttp10Compatible(boolean) http10Compatible} flag. */ public void setStatusCode(HttpStatus statusCode) { this.statusCode = statusCode; } /** * Whether to treat the redirect URL as a URI template. * Set this flag to {@code false} if the redirect URL contains open * and close curly braces "{", "}" and you don't want them interpreted * as URI variables. *

Defaults to {@code true}. */ public void setExpandUriTemplateVariables(boolean expandUriTemplateVariables) { this.expandUriTemplateVariables = expandUriTemplateVariables; } /** * 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}. * @since 4.1 */ public void setPropagateQueryParams(boolean propagateQueryParams) { this.propagateQueryParams = propagateQueryParams; } /** * Whether to propagate the query params of the current URL. * @since 4.1 */ public boolean isPropagateQueryProperties() { return this.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 HttpServletResponse#encodeRedirectURL} for URLs that have a * host and that host is not listed as a known host. *

If not set (the default) all URLs are encoded through the response. * @param hosts one or more application hosts * @since 4.3 */ public void setHosts(@Nullable String... hosts) { this.hosts = hosts; } /** * Return the configured application hosts. * @since 4.3 */ @Nullable public String[] getHosts() { return this.hosts; } /** * Returns "true" indicating this view performs a redirect. */ @Override public boolean isRedirectView() { return true; } /** * An ApplicationContext is not strictly required for RedirectView. */ @Override protected boolean isContextRequired() { return false; } /** * Convert model to request parameters and redirect to the given URL. * @see #appendQueryProperties * @see #sendRedirect */ @Override protected void renderMergedOutputModel(Map model, HttpServletRequest request, HttpServletResponse response) throws IOException { String targetUrl = createTargetUrl(model, request); targetUrl = updateTargetUrl(targetUrl, model, request, response); // Save flash attributes RequestContextUtils.saveOutputFlashMap(targetUrl, request, response); // Redirect sendRedirect(request, response, targetUrl, this.http10Compatible); } /** * 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, HttpServletRequest request) throws UnsupportedEncodingException { // Prepare target URL. StringBuilder targetUrl = new StringBuilder(); String url = getUrl(); Assert.state(url != null, "'url' not set"); if (this.contextRelative && getUrl().startsWith("/")) { // Do not apply context path to relative URLs. targetUrl.append(getContextPath(request)); } targetUrl.append(getUrl()); String enc = this.encodingScheme; if (enc == null) { enc = request.getCharacterEncoding(); } if (enc == null) { enc = WebUtils.DEFAULT_CHARACTER_ENCODING; } if (this.expandUriTemplateVariables && StringUtils.hasText(targetUrl)) { Map variables = getCurrentRequestUriVariables(request); targetUrl = replaceUriTemplateVariables(targetUrl.toString(), model, variables, enc); } if (isPropagateQueryProperties()) { appendCurrentQueryParams(targetUrl, request); } if (this.exposeModelAttributes) { appendQueryProperties(targetUrl, model, enc); } return targetUrl.toString(); } private String getContextPath(HttpServletRequest request) { String contextPath = request.getContextPath(); while (contextPath.startsWith("//")) { contextPath = contextPath.substring(1); } return contextPath; } /** * 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 a Map that contains model attributes * @param currentUriVariables current request URI variables to use * @param encodingScheme the encoding scheme to use * @throws UnsupportedEncodingException if string encoding failed */ protected StringBuilder replaceUriTemplateVariables( String targetUrl, Map model, Map currentUriVariables, String encodingScheme) throws UnsupportedEncodingException { 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, endLastMatch, matcher.start()); result.append(UriUtils.encodePathSegment(value.toString(), encodingScheme)); endLastMatch = matcher.end(); } result.append(targetUrl.substring(endLastMatch)); return result; } @SuppressWarnings("unchecked") private Map getCurrentRequestUriVariables(HttpServletRequest request) { String name = HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE; Map uriVars = (Map) request.getAttribute(name); return (uriVars != null) ? uriVars : 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 * @since 4.1 */ protected void appendCurrentQueryParams(StringBuilder targetUrl, HttpServletRequest request) { String query = request.getQueryString(); 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); } } } /** * Append query properties to the redirect URL. * Stringifies, URL-encodes and formats model attributes as query properties. * @param targetUrl the StringBuilder to append the properties to * @param model a Map that contains model attributes * @param encodingScheme the encoding scheme to use * @throws UnsupportedEncodingException if string encoding failed * @see #queryProperties */ @SuppressWarnings("unchecked") protected void appendQueryProperties(StringBuilder targetUrl, Map model, String encodingScheme) throws UnsupportedEncodingException { // 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 there aren't already some parameters, we need a "?". boolean first = (targetUrl.toString().indexOf('?') < 0); for (Map.Entry entry : queryProperties(model).entrySet()) { Object rawValue = entry.getValue(); Collection values; if (rawValue != null && rawValue.getClass().isArray()) { values = CollectionUtils.arrayToList(rawValue); } else if (rawValue instanceof Collection) { values = ((Collection) rawValue); } else { values = Collections.singleton(rawValue); } for (Object value : values) { if (first) { targetUrl.append('?'); first = false; } else { targetUrl.append('&'); } String encodedKey = urlEncode(entry.getKey(), encodingScheme); String encodedValue = (value != null ? urlEncode(value.toString(), encodingScheme) : ""); targetUrl.append(encodedKey).append('=').append(encodedValue); } } // Append anchor fragment, if any, to end of URL. if (fragment != null) { targetUrl.append(fragment); } } /** * Determine name-value pairs for query strings, which will be stringified, * URL-encoded and formatted by {@link #appendQueryProperties}. *

This implementation filters the model through checking * {@link #isEligibleProperty(String, Object)} for each element, * by default accepting Strings, primitives and primitive wrappers only. * @param model the original model Map * @return the filtered Map of eligible query properties * @see #isEligibleProperty(String, Object) */ protected Map queryProperties(Map model) { Map result = new LinkedHashMap<>(); model.forEach((name, value) -> { if (isEligibleProperty(name, value)) { result.put(name, value); } }); return result; } /** * Determine whether the given model element should be exposed * as a query property. *

The default implementation considers Strings and primitives * as eligible, and also arrays and Collections/Iterables with * corresponding elements. This can be overridden in subclasses. * @param key the key of the model element * @param value the value of the model element * @return whether the element is eligible as query property */ protected boolean isEligibleProperty(String key, @Nullable Object value) { if (value == null) { return false; } if (isEligibleValue(value)) { return true; } if (value.getClass().isArray()) { int length = Array.getLength(value); if (length == 0) { return false; } for (int i = 0; i < length; i++) { Object element = Array.get(value, i); if (!isEligibleValue(element)) { return false; } } return true; } if (value instanceof Collection) { Collection coll = (Collection) value; if (coll.isEmpty()) { return false; } for (Object element : coll) { if (!isEligibleValue(element)) { return false; } } return true; } return false; } /** * Determine whether the given model element value is eligible for exposure. *

The default implementation considers primitives, strings, numbers, dates, * URIs, URLs etc as eligible, according to {@link BeanUtils#isSimpleValueType}. * This can be overridden in subclasses. * @param value the model element value * @return whether the element value is eligible * @see BeanUtils#isSimpleValueType */ protected boolean isEligibleValue(@Nullable Object value) { return (value != null && BeanUtils.isSimpleValueType(value.getClass())); } /** * URL-encode the given input String with the given encoding scheme. *

The default implementation uses {@code URLEncoder.encode(input, enc)}. * @param input the unencoded input String * @param encodingScheme the encoding scheme * @return the encoded output String * @throws UnsupportedEncodingException if thrown by the JDK URLEncoder * @see java.net.URLEncoder#encode(String, String) */ protected String urlEncode(String input, String encodingScheme) throws UnsupportedEncodingException { return URLEncoder.encode(input, encodingScheme); } /** * Find the registered {@link RequestDataValueProcessor}, if any, and allow * it to update the redirect target URL. * @param targetUrl the given redirect URL * @return the updated URL or the same as URL as the one passed in */ protected String updateTargetUrl(String targetUrl, Map model, HttpServletRequest request, HttpServletResponse response) { WebApplicationContext wac = getWebApplicationContext(); if (wac == null) { wac = RequestContextUtils.findWebApplicationContext(request, getServletContext()); } if (wac != null && wac.containsBean(RequestContextUtils.REQUEST_DATA_VALUE_PROCESSOR_BEAN_NAME)) { RequestDataValueProcessor processor = wac.getBean( RequestContextUtils.REQUEST_DATA_VALUE_PROCESSOR_BEAN_NAME, RequestDataValueProcessor.class); return processor.processUrl(request, targetUrl); } return targetUrl; } /** * Send a redirect back to the HTTP client. * @param request current HTTP request (allows for reacting to request method) * @param response current HTTP response (for sending response headers) * @param targetUrl the target URL to redirect to * @param http10Compatible whether to stay compatible with HTTP 1.0 clients * @throws IOException if thrown by response methods */ protected void sendRedirect(HttpServletRequest request, HttpServletResponse response, String targetUrl, boolean http10Compatible) throws IOException { String encodedURL = (isRemoteHost(targetUrl) ? targetUrl : response.encodeRedirectURL(targetUrl)); if (http10Compatible) { HttpStatus attributeStatusCode = (HttpStatus) request.getAttribute(View.RESPONSE_STATUS_ATTRIBUTE); if (this.statusCode != null) { response.setStatus(this.statusCode.value()); response.setHeader("Location", encodedURL); } else if (attributeStatusCode != null) { response.setStatus(attributeStatusCode.value()); response.setHeader("Location", encodedURL); } else { // Send status code 302 by default. response.sendRedirect(encodedURL); } } else { HttpStatus statusCode = getHttp11StatusCode(request, response, targetUrl); response.setStatus(statusCode.value()); response.setHeader("Location", encodedURL); } } /** * Whether the given targetUrl has a host that is a "foreign" system in which * case {@link 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. * @since 4.3 */ protected boolean isRemoteHost(String targetUrl) { if (ObjectUtils.isEmpty(getHosts())) { return false; } String targetHost = UriComponentsBuilder.fromUriString(targetUrl).build().getHost(); if (!StringUtils.hasLength(targetHost)) { return false; } for (String host : getHosts()) { if (targetHost.equals(host)) { return false; } } return true; } /** * Determines the status code to use for HTTP 1.1 compatible requests. *

The default implementation returns the {@link #setStatusCode(HttpStatus) statusCode} * property if set, or the value of the {@link #RESPONSE_STATUS_ATTRIBUTE} attribute. * If neither are set, it defaults to {@link HttpStatus#SEE_OTHER} (303). * @param request the request to inspect * @param response the servlet response * @param targetUrl the target URL * @return the response status */ protected HttpStatus getHttp11StatusCode( HttpServletRequest request, HttpServletResponse response, String targetUrl) { if (this.statusCode != null) { return this.statusCode; } HttpStatus attributeStatusCode = (HttpStatus) request.getAttribute(View.RESPONSE_STATUS_ATTRIBUTE); if (attributeStatusCode != null) { return attributeStatusCode; } return HttpStatus.SEE_OTHER; } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy