Open Source Repository

Home /spring/spring-web-servlet-3.0.5 | Repository Home


org/springframework/web/servlet/view/RedirectView.java
/*
 * Copyright 2002-2010 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.servlet.view;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.Array;
import java.net.URLEncoder;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.beans.BeanUtils;
import org.springframework.http.HttpStatus;
import org.springframework.util.ObjectUtils;
import org.springframework.web.servlet.View;
import org.springframework.web.util.WebUtils;

/**
 <p>View that redirects to an absolute, context relative, or current request
 * relative URL. By default all primitive model attributes (or collections
 * thereof) are exposed as HTTP query parameters, but this behavior can be changed
 * by overriding the {@link #isEligibleProperty(String, Object)} method.
 *
 <p>A URL for this view is supposed to be a HTTP redirect URL, i.e.
 * suitable for HttpServletResponse's <code>sendRedirect</code> 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.
 *
 <p>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.
 *
 <p><b>NOTE when using this redirect view in a Portlet environment:</b> Make sure
 * that your controller respects the Portlet <code>sendRedirect</code> constraints.
 * When e.g. using {@link org.springframework.web.portlet.mvc.SimpleFormController},
 * make sure to set your controller's
 {@link org.springframework.web.portlet.mvc.AbstractFormController#setRedirectAction "redirectAction"}
 * property to "true", in order to make the controller base class behave accordingly.
 *
 @author Rod Johnson
 @author Juergen Hoeller
 @author Colin Sampaleanu
 @author Sam Brannen
 @author Arjen Poutsma
 @see #setContextRelative
 @see #setHttp10Compatible
 @see #setExposeModelAttributes
 @see javax.servlet.http.HttpServletResponse#sendRedirect
 */
public class RedirectView extends AbstractUrlBasedView {

  private boolean contextRelative = false;

  private boolean http10Compatible = true;

  private boolean exposeModelAttributes = true;

  private String encodingScheme;

  private HttpStatus statusCode;


  /**
   * Constructor for use as a bean.
   */
  public RedirectView() {
  }

  /**
   * Create a new RedirectView with the given URL.
   <p>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);
  }

  /**
   * 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;
  }

  /**
   * 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;
  }

  /**
   * 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;
  }


  /**
   * 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.
   <p>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.
   <p>In the default implementation, this will enforce HTTP status code 302
   * in any case, i.e. delegate to <code>HttpServletResponse.sendRedirect</code>.
   * 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.
   <p>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</code> flag which denotes whether
   * or not model attributes should be exposed as HTTP query parameters.
   <p>Defaults to <code>true</code>.
   */
  public void setExposeModelAttributes(final boolean exposeModelAttributes) {
    this.exposeModelAttributes = exposeModelAttributes;
  }

  /**
   * Set the encoding scheme for this view.
   <p>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.
   <p>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;
  }

  /**
   * Convert model to request parameters and redirect to the given URL.
   @see #appendQueryProperties
   @see #sendRedirect
   */
  @Override
  protected void renderMergedOutputModel(
      Map<String, Object> model, HttpServletRequest request, HttpServletResponse response)
      throws IOException {

    // 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());
    if (this.exposeModelAttributes) {
      String enc = this.encodingScheme;
      if (enc == null) {
        enc = request.getCharacterEncoding();
      }
      if (enc == null) {
        enc = WebUtils.DEFAULT_CHARACTER_ENCODING;
      }
      appendQueryProperties(targetUrl, model, enc);
    }

    sendRedirect(request, response, targetUrl.toString()this.http10Compatible);
  }

  /**
   * 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 Map that contains model attributes
   @param encodingScheme the encoding scheme to use
   @throws UnsupportedEncodingException if string encoding failed
   @see #queryProperties
   */
  protected void appendQueryProperties(StringBuilder targetUrl, Map<String, Object> 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 = (getUrl().indexOf('?'0);
    for (Map.Entry<String, Object> entry : queryProperties(model).entrySet()) {
      Object rawValue = entry.getValue();
      Iterator valueIter;
      if (rawValue != null && rawValue.getClass().isArray()) {
        valueIter = Arrays.asList(ObjectUtils.toObjectArray(rawValue)).iterator();
      }
      else if (rawValue instanceof Collection) {
        valueIter = ((CollectionrawValue).iterator();
      }
      else {
        valueIter = Collections.singleton(rawValue).iterator();
      }
      while (valueIter.hasNext()) {
        Object value = valueIter.next();
        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}.
   <p>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<String, Object> queryProperties(Map<String, Object> model) {
    Map<String, Object> result = new LinkedHashMap<String, Object>();
    for (Map.Entry<String, Object> entry : model.entrySet()) {
      if (isEligibleProperty(entry.getKey(), entry.getValue())) {
        result.put(entry.getKey(), entry.getValue());
      }
    }
    return result;
  }

  /**
   * Determine whether the given model element should be exposed
   * as a query property.
   <p>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, 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 = (Collectionvalue;
      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.
   <p>The default implementation considers primitives, Strings, Numbers, Dates,
   * URIs, URLs and Locale objects as eligible. 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(Object value) {
    return (value != null && BeanUtils.isSimpleValueType(value.getClass()));
  }

  /**
   * URL-encode the given input String with the given encoding scheme.
   <p>The default implementation uses <code>URLEncoder.encode(input, enc)</code>.
   @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)
   @see java.net.URLEncoder#encode(String)
   */
  protected String urlEncode(String input, String encodingSchemethrows UnsupportedEncodingException {
    return (input != null ? URLEncoder.encode(input, encodingSchemenull);
  }

  /**
   * 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 {

    if (http10Compatible) {
      // Always send status code 302.
      response.sendRedirect(response.encodeRedirectURL(targetUrl));
    }
    else {
      HttpStatus statusCode = getHttp11StatusCode(request, response, targetUrl);
      response.setStatus(statusCode.value());
      response.setHeader("Location", response.encodeRedirectURL(targetUrl));
    }
  }

  /**
   * Determines the status code to use for HTTP 1.1 compatible requests.
   <p>The default implemenetation 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 = (HttpStatusrequest.getAttribute(View.RESPONSE_STATUS_ATTRIBUTE);
    if (attributeStatusCode != null) {
      return attributeStatusCode;
    }
    return HttpStatus.SEE_OTHER;
  }

}