Open Source Repository

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


org/springframework/web/servlet/tags/UrlTag.java
/*
 * Copyright 2002-2009 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.tags;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.jsp.JspException;
import javax.servlet.jsp.PageContext;

import org.springframework.util.StringUtils;
import org.springframework.web.util.ExpressionEvaluationUtils;
import org.springframework.web.util.HtmlUtils;
import org.springframework.web.util.JavaScriptUtils;
import org.springframework.web.util.TagUtils;
import org.springframework.web.util.UriUtils;

/**
 * JSP tag for creating URLs. Modeled after the JSTL c:url tag with backwards
 * compatibility in mind.
 
 <p>Enhancements to the JSTL functionality include:
 <ul>
 <li>URL encoded template URI variables</li>
 <li>HTML/XML escaping of URLs</li>
 <li>JavaScript escaping of URLs</li>
 </ul>
 
 <p>Template URI variables are indicated in the {@link #setValue(String) 'value'}
 * attribute and marked by braces '{variableName}'. The braces and attribute name are
 * replaced by the URL encoded value of a parameter defined with the spring:param tag 
 * in the body of the url tag. If no parameter is available the literal value is 
 * passed through. Params matched to template variables will not be added to the query 
 * string.
 
 <p>Use of the spring:param tag for URI template variables is strongly recommended 
 * over direct EL substitution as the values are URL encoded.  Failure to properly 
 * encode URL can leave an application vulnerable to XSS and other injection attacks.
 
 <p>URLs can be HTML/XML escaped by setting the {@link #setHtmlEscape(String) 
 * 'htmlEscape'} attribute to 'true'.  Detects an HTML escaping setting, either on 
 * this tag instance, the page level, or the <code>web.xml</code> level. The default 
 * is 'false'.  When setting the URL value into a variable, escaping is not recommended.
 
 <p>Example usage:
 <pre>&lt;spring:url value="/url/path/{variableName}"&gt;
 *   &lt;spring:param name="variableName" value="more than JSTL c:url" /&gt;
 * &lt;/spring:url&gt;</pre>
 * Results in:
 <code>/currentApplicationContext/url/path/more%20than%20JSTL%20c%3Aurl</code>
 
 @author Scott Andrews
 @since 3.0
 @see ParamTag
 */
public class UrlTag extends HtmlEscapingAwareTag implements ParamAware {

  private static final String URL_TEMPLATE_DELIMITER_PREFIX = "{";

  private static final String URL_TEMPLATE_DELIMITER_SUFFIX = "}";

  private static final String URL_TYPE_ABSOLUTE = "://";


  private List<Param> params;

  private Set<String> templateParams;

  private UrlType type;

  private String value;

  private String context;

  private String var;

  private int scope = PageContext.PAGE_SCOPE;

  private boolean javaScriptEscape = false;


  /**
   * Sets the value of the URL
   */
  public void setValue(String value) {
    if (value.contains(URL_TYPE_ABSOLUTE)) {
      this.type = UrlType.ABSOLUTE;
      this.value = value;
    }
    else if (value.startsWith("/")) {
      this.type = UrlType.CONTEXT_RELATIVE;
      this.value = value;
    }
    else {
      this.type = UrlType.RELATIVE;
      this.value = value;
    }
  }

  /**
   * Set the context path for the URL. Defaults to the current context
   */
  public void setContext(String context) {
    if (context.startsWith("/")) {
      this.context = context;
    }
    else {
      this.context = "/" + context;
    }
  }

  /**
   * Set the variable name to expose the URL under. Defaults to rendering the
   * URL to the current JspWriter
   */
  public void setVar(String var) {
    this.var = var;
  }

  /**
   * Set the scope to export the URL variable to. This attribute has no
   * meaning unless var is also defined.
   */
  public void setScope(String scope) {
    this.scope = TagUtils.getScope(scope);
  }

  /**
   * Set JavaScript escaping for this tag, as boolean value.
   * Default is "false".
   */
  public void setJavaScriptEscape(String javaScriptEscapethrows JspException {
    this.javaScriptEscape =
        ExpressionEvaluationUtils.evaluateBoolean("javaScriptEscape", javaScriptEscape, pageContext);
  }

  public void addParam(Param param) {
    this.params.add(param);
  }


  @Override
  public int doStartTagInternal() throws JspException {
    this.params = new LinkedList<Param>();
    this.templateParams = new HashSet<String>();
    return EVAL_BODY_INCLUDE;
  }

  @Override
  public int doEndTag() throws JspException {
    String url = createUrl();
    if (this.var == null) {
      // print the url to the writer
      try {
        pageContext.getOut().print(url);
      }
      catch (IOException e) {
        throw new JspException(e);
      }
    }
    else {
      // store the url as a variable
      pageContext.setAttribute(var, url, scope);
    }
    return EVAL_PAGE;
  }


  /**
   * Build the URL for the tag from the tag attributes and parameters.
   @return the URL value as a String
   @throws JspException
   */
  private String createUrl() throws JspException {
    HttpServletRequest request = (HttpServletRequestpageContext.getRequest();
    HttpServletResponse response = (HttpServletResponsepageContext.getResponse();
    StringBuilder url = new StringBuilder();
    if (this.type == UrlType.CONTEXT_RELATIVE) {
      // add application context to url
      if (this.context == null) {
        url.append(request.getContextPath());
      }
      else {
        url.append(this.context);
      }
    }
    if (this.type != UrlType.RELATIVE && this.type != UrlType.ABSOLUTE && !this.value.startsWith("/")) {
      url.append("/");
    }
    url.append(replaceUriTemplateParams(this.value, this.params, this.templateParams));
    url.append(createQueryString(this.params, this.templateParams, (url.indexOf("?"== -1)));

    String urlStr = url.toString();
    if (this.type != UrlType.ABSOLUTE) {
      // Add the session identifier if needed
      // (Do not embed the session identifier in a remote link!)
      urlStr = response.encodeURL(urlStr);
    }

    // HTML and/or JavaScript escape, if demanded.
    urlStr = isHtmlEscape() ? HtmlUtils.htmlEscape(urlStr: urlStr;
    urlStr = this.javaScriptEscape ? JavaScriptUtils.javaScriptEscape(urlStr: urlStr;
    
    return urlStr;
  }

  /**
   * Build the query string from available parameters that have not already
   * been applied as template params.
   <p>The names and values of parameters are URL encoded.
   @param params the parameters to build the query string from
   @param usedParams set of parameter names that have been applied as
   * template params
   @param includeQueryStringDelimiter true if the query string should start
   * with a '?' instead of '&'
   @return the query string
   */
  protected String createQueryString(List<Param> params, Set<String> usedParams, boolean includeQueryStringDelimiter)
      throws JspException {

    String encoding = pageContext.getResponse().getCharacterEncoding();
    StringBuilder qs = new StringBuilder();
    for (Param param : params) {
      if (!usedParams.contains(param.getName()) && StringUtils.hasLength(param.getName())) {
        if (includeQueryStringDelimiter && qs.length() == 0) {
          qs.append("?");
        }
        else {
          qs.append("&");
        }
        try {
          qs.append(UriUtils.encodeQueryParam(param.getName(), encoding));
          if (param.getValue() != null) {
            qs.append("=");
            qs.append(UriUtils.encodeQueryParam(param.getValue(), encoding));
          }
        }
        catch (UnsupportedEncodingException ex) {
          throw new JspException(ex);
        }
      }
    }
    return qs.toString();
  }

  /**
   * Replace template markers in the URL matching available parameters. The
   * name of matched parameters are added to the used parameters set.
   <p>Parameter values are URL encoded.
   @param uri the URL with template parameters to replace
   @param params parameters used to replace template markers
   @param usedParams set of template parameter names that have been replaced
   @return the URL with template parameters replaced
   */
  protected String replaceUriTemplateParams(String uri, List<Param> params, Set<String> usedParams)
      throws JspException {

    String encoding = pageContext.getResponse().getCharacterEncoding();
    for (Param param : params) {
      String template = URL_TEMPLATE_DELIMITER_PREFIX + param.getName() + URL_TEMPLATE_DELIMITER_SUFFIX;
      if (uri.contains(template)) {
        usedParams.add(param.getName());
        try {
          uri = uri.replace(template, UriUtils.encodePath(param.getValue(), encoding));
        }
        catch (UnsupportedEncodingException ex) {
          throw new JspException(ex);
        }
      }
    }
    return uri;
  }

  /**
   * Internal enum that classifies URLs by type.
   */
  private enum UrlType {
    CONTEXT_RELATIVE, RELATIVE, ABSOLUTE
  }

}