diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/tags/Param.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/tags/Param.java
new file mode 100644
index 00000000000..b0adb36dddb
--- /dev/null
+++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/tags/Param.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2008 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;
+
+/**
+ * Bean used to pass name-value pair parameters from a {@link ParamTag} to a
+ * {@link ParamAware} tag.
+ *
+ * @author Scott Andrews
+ * @since 3.0
+ * @see ParamTag
+ */
+public class Param {
+
+ private String name;
+
+ private String value;
+
+ /**
+ * @return the non-encoded parameter name
+ */
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * Set the non-encoded name of the parameter
+ */
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ /**
+ * @return the non-encoded parameter value
+ */
+ public String getValue() {
+ return value;
+ }
+
+ /**
+ * Set the non-encoded value of the parameter
+ */
+ public void setValue(String value) {
+ this.value = value;
+ }
+
+ @Override
+ public String toString() {
+ return "JSP Tag Param: name '" + name + "', value '" + value + "'";
+ }
+
+}
diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/tags/ParamAware.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/tags/ParamAware.java
new file mode 100644
index 00000000000..b4c4fb505e1
--- /dev/null
+++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/tags/ParamAware.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2008 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;
+
+/**
+ * Allows implementing tag to utilize nested spring:param tags.
+ *
+ * @author Scott Andrews
+ * @since 3.0
+ * @see ParamTag
+ */
+public interface ParamAware {
+
+ /**
+ * Callback hook for nested spring:param tags to pass their value to the
+ * parent tag.
+ *
+ * @param param the result of the nested spring:param tag
+ */
+ public void addParam(Param param);
+
+}
diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/tags/ParamTag.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/tags/ParamTag.java
new file mode 100644
index 00000000000..055c437344d
--- /dev/null
+++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/tags/ParamTag.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright 2008 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 javax.servlet.jsp.JspException;
+import javax.servlet.jsp.tagext.BodyTagSupport;
+
+/**
+ * JSP tag for collecting name-value parameters and passing them to a
+ * {@link ParamAware} ancestor in the tag hierarchy.
+ *
+ *
+ * This tag must be nested under a param aware tag.
+ *
+ * @author Scott Andrews
+ * @since 3.0
+ * @see Param
+ * @see UrlTag
+ */
+public class ParamTag extends BodyTagSupport {
+
+ private String name;
+
+ private String value;
+
+ private Param param;
+
+ // tag lifecycle
+
+ @Override
+ public int doEndTag() throws JspException {
+ param = new Param();
+ param.setName(name);
+ if (value != null) {
+ param.setValue(value);
+ }
+ else if (getBodyContent() != null) {
+ // get the value from the tag body
+ param.setValue(getBodyContent().getString().trim());
+ }
+
+ // find a param aware ancestor
+ ParamAware paramAwareTag = (ParamAware) findAncestorWithClass(this,
+ ParamAware.class);
+ if (paramAwareTag == null) {
+ throw new JspException(
+ "The param tag must be a descendant of a tag that supports parameters");
+ }
+
+ paramAwareTag.addParam(param);
+
+ return EVAL_PAGE;
+ }
+
+ // tag attribute accessors
+
+ /**
+ * Sets the name of the parameter
+ *
+ *
+ * Required
+ *
+ * @param name the parameter name
+ */
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ /**
+ * Sets the value of the parameter
+ *
+ *
+ * Optional. If not set, the tag's body content is evaluated
+ *
+ * @param value the parameter value
+ */
+ public void setValue(String value) {
+ this.value = value;
+ }
+
+}
diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/tags/UrlTag.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/tags/UrlTag.java
new file mode 100644
index 00000000000..fb60775f038
--- /dev/null
+++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/tags/UrlTag.java
@@ -0,0 +1,371 @@
+/*
+ * Copyright 2008 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.net.URLEncoder;
+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 javax.servlet.jsp.tagext.TagSupport;
+
+import org.springframework.util.StringUtils;
+import org.springframework.web.util.TagUtils;
+
+/**
+ * JSP tag for creating URLs. Modeled after the JSTL c:url tag with backwards
+ * compatibility in mind.
+ *
+ *
+ * Enhancements to the JSTL functionality include:
+ *
+ * - URL encoded template URI variables
+ * - Server and servlet relative URLs in addition to the standard context
+ * relative.
+ * - XML escaping of URLs
+ *
+ *
+ *
+ * Template URI variables are indicated in the {@link #setValue(String) 'value'}
+ * attribute and marked by braces '{}'. The braces and attribute name are
+ * replaced by the URL encoded value of a parameter. If no parameter is
+ * available the literal value is passed through. Params matched to template
+ * variables will not be added to the query string.
+ *
+ *
+ * URLs can be XML escaped by setting the {@link #setEscapeXml(String)
+ * 'escapeXml'} attribute to 'true', the default is 'false'.
+ *
+ * @author Scott Andrews
+ * @since 3.0
+ * @see ParamTag
+ */
+public class UrlTag extends TagSupport 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 enum UrlType {
+ CONTEXT_RELATIVE, RELATIVE, ABSOLUTE
+ };
+
+ private static final char[] XML_CHARS = { '&', '<', '>', '"', '\'' };
+
+ private List params;
+
+ private Set templateParams;
+
+ private UrlType type;
+
+ private String value;
+
+ private String context;
+
+ private String var;
+
+ private int scope = PageContext.PAGE_SCOPE;
+
+ private boolean escapeXml = false;
+
+ // tag lifecycle
+
+ @Override
+ public int doStartTag() throws JspException {
+ params = new LinkedList();
+ templateParams = new HashSet();
+
+ return EVAL_BODY_INCLUDE;
+ }
+
+ @Override
+ public int doEndTag() throws JspException {
+ String url = createUrl();
+
+ if (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;
+ }
+
+ // from ParamAware
+
+ public void addParam(Param param) {
+ params.add(param);
+ }
+
+ // support methods
+
+ /**
+ * 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 = (HttpServletRequest) pageContext
+ .getRequest();
+ HttpServletResponse response = (HttpServletResponse) pageContext
+ .getResponse();
+ StringBuilder url = new StringBuilder();
+
+ if (type == UrlType.CONTEXT_RELATIVE) {
+ // add application context to url
+ if (context == null) {
+ url.append(request.getContextPath());
+ }
+ else {
+ url.append(context);
+ }
+ }
+
+ if (type != UrlType.RELATIVE && type != UrlType.ABSOLUTE
+ && !value.startsWith("/")) {
+ url.append("/");
+ }
+
+ url.append(replaceUriTemplateParams(value, params, templateParams));
+ url.append(createQueryString(params, templateParams,
+ (url.indexOf("?") == -1)));
+
+ String urlStr;
+ if (type != UrlType.ABSOLUTE) {
+ // add the session identifier if needed
+ urlStr = response.encodeURL(url.toString());
+ }
+ else {
+ // do not embed the session identifier in a remote link
+ urlStr = url.toString();
+ }
+
+ if (escapeXml) {
+ urlStr = escapeXml(urlStr);
+ }
+
+ return urlStr;
+ }
+
+ /**
+ * Builds the query string from available parameters that have not already
+ * been applied as template params.
+ *
+ *
+ * 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
+ * @throws JspException
+ */
+ protected String createQueryString(List params,
+ Set usedParams, boolean includeQueryStringDelimiter)
+ throws JspException {
+ StringBuilder qs = new StringBuilder();
+ for (Param param : params) {
+ if (!usedParams.contains(param.getName())
+ && param.getName() != null && !"".equals(param.getName())) {
+ if (includeQueryStringDelimiter && qs.length() == 0) {
+ qs.append("?");
+ }
+ else {
+ qs.append("&");
+ }
+ qs.append(urlEncode(param.getName()));
+ if (param.getValue() != null) {
+ qs.append("=");
+ qs.append(urlEncode(param.getValue()));
+ }
+ }
+ }
+
+ return qs.toString();
+ }
+
+ /**
+ * Replaces template markers in the URL matching available parameters. The
+ * name of matched parameters are added to the used parameters set.
+ *
+ *
+ * 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
+ * @throws JspException
+ */
+ protected String replaceUriTemplateParams(String uri, List params,
+ Set usedParams) throws JspException {
+ for (Param param : params) {
+ String template = URL_TEMPLATE_DELIMITER_PREFIX + param.getName()
+ + URL_TEMPLATE_DELIMITER_SUFFIX;
+ if (uri.contains(template)) {
+ usedParams.add(param.getName());
+ uri = uri.replace(template, urlEncode(param.getValue()));
+ }
+ }
+ return uri;
+ }
+
+ /**
+ * URL encode the provided string using the character encoding for the
+ * response.
+ *
+ * @param value the value to encode
+ * @return the URL encoded value
+ * @throws JspException if the character encoding is invalid
+ */
+ protected String urlEncode(String value) throws JspException {
+ if (value == null) {
+ return null;
+ }
+
+ try {
+ return URLEncoder.encode(value, pageContext.getResponse()
+ .getCharacterEncoding());
+ }
+ catch (UnsupportedEncodingException e) {
+ throw new JspException(e);
+ }
+ }
+
+ /**
+ * XML entity encode the provided string. &, <, >, ' and
+ * " are encoded to their entity values.
+ *
+ * @param xml the value to escape
+ * @return the escaped value
+ */
+ protected String escapeXml(String xml) {
+ if (xml == null) {
+ return null;
+ }
+
+ String escapedXml = xml;
+ for (char xmlChar : XML_CHARS) {
+ escapedXml = StringUtils.replace(escapedXml, String
+ .valueOf(xmlChar), entityValue(xmlChar));
+ }
+
+ return escapedXml;
+ }
+
+ /**
+ * Convert a character value to a XML entity value. The decimal value of the
+ * character is used.
+ *
+ *
+ * For example, 'A' is converted to "A".
+ *
+ * @param xmlChar the character to encode
+ * @return the entity value
+ */
+ protected String entityValue(char xmlChar) {
+ return new StringBuilder().append("").append(
+ Integer.toString(xmlChar)).append(";").toString();
+ }
+
+ // tag attribute accessors
+
+ /**
+ * Sets the value of the URL
+ */
+ public void setValue(String value) {
+ if (value.contains(URL_TYPE_ABSOLUTE)) {
+ type = UrlType.ABSOLUTE;
+ this.value = value;
+ }
+ else if (value.startsWith("/")) {
+ type = UrlType.CONTEXT_RELATIVE;
+ this.value = value;
+ }
+ else {
+ type = UrlType.RELATIVE;
+ this.value = value;
+ }
+ }
+
+ /**
+ * Sets 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;
+ }
+ }
+
+ /**
+ * Sets 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;
+ }
+
+ /**
+ * Sets the scope to export the URL variable to. This attribute has no
+ * meaning unless var is also defined.
+ *
+ * @param scope the string name of the scope
+ * @see TagUtils#getScope(String)
+ */
+ public void setScope(String scope) {
+ this.scope = TagUtils.getScope(scope);
+ }
+
+ /**
+ * Instructs the tag to XML entity encode the resulting URL.
+ *
+ * Defaults to false to maintain compatibility with the JSTL c:url tag.
+ *
+ * NOTE: Strongly recommended to set as 'true' when rendering
+ * directly to the JspWriter in an XML or HTML based file.
+ *
+ * @param escapeXml string representation of a boolean
+ * @see Boolean#valueOf(String)
+ */
+ public void setEscapeXml(String escapeXml) {
+ this.escapeXml = Boolean.valueOf(escapeXml);
+ }
+
+}
diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/tags/spring.tld b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/tags/spring.tld
index 8c8ad97daad..a0bb87c7c8a 100644
--- a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/tags/spring.tld
+++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/tags/spring.tld
@@ -342,4 +342,71 @@
+
+ url
+ org.springframework.web.servlet.tags.UrlTag
+ JSP
+ URL tag based on the JSTL c:url tag. This variant is fully
+ backwards compatible with the standard tag. Enhancements include support
+ for URL template parameters.
+
+ value
+ true
+ true
+ The URL to build. This value can include template place holders
+ that are replaced with the URL encoded value of the named parameter. Parameters
+ must be defined using the param tag inside the body of this tag.
+
+
+ context
+ false
+ true
+ Specifies a remote application context. The default is the
+ current application context.
+
+
+ var
+ false
+ true
+ The name of the variable to export the URL value to.
+
+
+ scope
+ false
+ true
+ The scope for the var. 'application', 'session', 'request' and
+ 'page' scopes are supported. Defaults to page scope. This attribute has no
+ effect unless the var attribute is also defined.
+
+
+ escapeXml
+ false
+ true
+ Escape XML special characters in the resulting URL. 'true' and
+ 'false' are supported. Defaults to 'false' to maintain compatibility with
+ the JSTL c:url tag. Strongly recommended to set as 'true' when rendering
+ directly to the JspWriter in an XML or HTML based file.
+
+
+
+
+ param
+ org.springframework.web.servlet.tags.ParamTag
+ JSP
+ Parameter tag based on the JSTL c:param tag. The sole purpose is to
+ support params inside the spring:url tag.
+
+ name
+ true
+ true
+ The name of the parameter.
+
+
+ value
+ false
+ true
+ The value of the parameter.
+
+
+
diff --git a/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/tags/ParamTagTests.java b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/tags/ParamTagTests.java
new file mode 100644
index 00000000000..1066b9596d3
--- /dev/null
+++ b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/tags/ParamTagTests.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright 2008 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 javax.servlet.jsp.JspException;
+import javax.servlet.jsp.PageContext;
+import javax.servlet.jsp.tagext.Tag;
+import javax.servlet.jsp.tagext.TagSupport;
+
+import org.springframework.mock.web.MockBodyContent;
+import org.springframework.mock.web.MockHttpServletResponse;
+
+/**
+ * Unit tests for ParamTag
+ *
+ * @author Scott Andrews
+ */
+public class ParamTagTests extends AbstractTagTests {
+
+ private ParamTag tag;
+
+ private MockParamSupportTag parent;
+
+ @Override
+ protected void setUp() throws Exception {
+ PageContext context = createPageContext();
+ parent = new MockParamSupportTag();
+ tag = new ParamTag();
+ tag.setPageContext(context);
+ tag.setParent(parent);
+ }
+
+ public void testParamWithNameAndValue() throws JspException {
+ tag.setName("name");
+ tag.setValue("value");
+
+ int action = tag.doEndTag();
+
+ assertEquals(Tag.EVAL_PAGE, action);
+ assertEquals("name", parent.getParam().getName());
+ assertEquals("value", parent.getParam().getValue());
+ }
+
+ public void testParamWithBodyValue() throws JspException {
+ tag.setName("name");
+ tag.setBodyContent(new MockBodyContent("value",
+ new MockHttpServletResponse()));
+
+ int action = tag.doEndTag();
+
+ assertEquals(Tag.EVAL_PAGE, action);
+ assertEquals("name", parent.getParam().getName());
+ assertEquals("value", parent.getParam().getValue());
+ }
+
+ public void testParamWithNullValue() throws JspException {
+ tag.setName("name");
+
+ int action = tag.doEndTag();
+
+ assertEquals(Tag.EVAL_PAGE, action);
+ assertEquals("name", parent.getParam().getName());
+ assertNull(parent.getParam().getValue());
+ }
+
+ public void testParamWithNoParent() {
+ tag.setName("name");
+ tag.setValue("value");
+
+ tag.setParent(null);
+
+ try {
+ tag.doEndTag();
+ fail("expected JspException");
+ }
+ catch (JspException e) {
+ // we want this
+ }
+ }
+
+ private class MockParamSupportTag extends TagSupport implements ParamAware {
+
+ private Param param;
+
+ public void addParam(Param param) {
+ this.param = param;
+ }
+
+ public Param getParam() {
+ return param;
+ }
+
+ }
+
+}
diff --git a/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/tags/ParamTests.java b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/tags/ParamTests.java
new file mode 100644
index 00000000000..4cd6406a55f
--- /dev/null
+++ b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/tags/ParamTests.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2008 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 junit.framework.TestCase;
+
+/**
+ * Unit tests for Param
+ *
+ * @author Scott Andrews
+ */
+public class ParamTests extends TestCase {
+
+ private Param param;
+
+ @Override
+ protected void setUp() throws Exception {
+ param = new Param();
+ }
+
+ public void testName() {
+ param.setName("name");
+ assertEquals("name", param.getName());
+ }
+
+ public void testValue() {
+ param.setValue("value");
+ assertEquals("value", param.getValue());
+ }
+
+ public void testNullDefaults() {
+ assertNull(param.getName());
+ assertNull(param.getValue());
+ }
+}
diff --git a/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/tags/UrlTagTests.java b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/tags/UrlTagTests.java
new file mode 100644
index 00000000000..9182a30a50a
--- /dev/null
+++ b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/tags/UrlTagTests.java
@@ -0,0 +1,571 @@
+/*
+ * Copyright 2008 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.lang.reflect.Method;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Set;
+
+import javax.servlet.jsp.JspException;
+import javax.servlet.jsp.PageContext;
+import javax.servlet.jsp.tagext.Tag;
+
+import org.springframework.mock.web.MockHttpServletRequest;
+import org.springframework.mock.web.MockPageContext;
+import org.springframework.util.ReflectionUtils;
+
+/**
+ * Unit tests for UrlTag
+ *
+ * @author Scott Andrews
+ */
+public class UrlTagTests extends AbstractTagTests {
+
+ private UrlTag tag;
+
+ private MockPageContext context;
+
+ @Override
+ protected void setUp() throws Exception {
+ context = createPageContext();
+ tag = new UrlTag();
+ tag.setPageContext(context);
+ }
+
+ public void testParamSupport() {
+ assertTrue(tag instanceof ParamAware);
+ }
+
+ public void testDoStartTag() throws JspException {
+ int action = tag.doStartTag();
+
+ assertEquals(Tag.EVAL_BODY_INCLUDE, action);
+ }
+
+ public void testDoEndTag() throws JspException {
+ tag.setValue("url/path");
+
+ tag.doStartTag();
+ int action = tag.doEndTag();
+
+ assertEquals(Tag.EVAL_PAGE, action);
+ }
+
+ public void testVarDefaultScope() throws JspException {
+ tag.setValue("url/path");
+ tag.setVar("var");
+
+ tag.doStartTag();
+ tag.doEndTag();
+
+ assertEquals("url/path", context.getAttribute("var",
+ PageContext.PAGE_SCOPE));
+ }
+
+ public void testVarExplicitScope() throws JspException {
+ tag.setValue("url/path");
+ tag.setVar("var");
+ tag.setScope("request");
+
+ tag.doStartTag();
+ tag.doEndTag();
+
+ assertEquals("url/path", context.getAttribute("var",
+ PageContext.REQUEST_SCOPE));
+ }
+
+ public void testSetEscapeXmlDefault() throws JspException {
+ tag.setValue("url/path");
+ tag.setVar("var");
+
+ tag.doStartTag();
+
+ Param param = new Param();
+ param.setName("n me");
+ param.setValue("v&l=e");
+ tag.addParam(param);
+
+ param = new Param();
+ param.setName("name");
+ param.setValue("value2");
+ tag.addParam(param);
+
+ tag.doEndTag();
+
+ assertEquals("url/path?n+me=v%26l%3De&name=value2", context
+ .getAttribute("var"));
+ }
+
+ public void testSetEscapeXmlFalse() throws JspException {
+ tag.setValue("url/path");
+ tag.setVar("var");
+ tag.setEscapeXml("false");
+
+ tag.doStartTag();
+
+ Param param = new Param();
+ param.setName("n me");
+ param.setValue("v&l=e");
+ tag.addParam(param);
+
+ param = new Param();
+ param.setName("name");
+ param.setValue("value2");
+ tag.addParam(param);
+
+ tag.doEndTag();
+
+ assertEquals("url/path?n+me=v%26l%3De&name=value2", context
+ .getAttribute("var"));
+ }
+
+ public void testSetEscapeXmlTrue() throws JspException {
+ tag.setValue("url/path");
+ tag.setVar("var");
+ tag.setEscapeXml("true");
+
+ tag.doStartTag();
+
+ Param param = new Param();
+ param.setName("n me");
+ param.setValue("v&l=e");
+ tag.addParam(param);
+
+ param = new Param();
+ param.setName("name");
+ param.setValue("value2");
+ tag.addParam(param);
+
+ tag.doEndTag();
+
+ assertEquals("url/path?n+me=v%26l%3De&name=value2", context
+ .getAttribute("var"));
+ }
+
+ public void testCreateQueryStringNoParams() throws JspException {
+ List params = new LinkedList();
+ Set usedParams = new HashSet();
+
+ String queryString = tag.createQueryString(params, usedParams, true);
+
+ assertEquals("", queryString);
+ }
+
+ public void testCreateQueryStringOneParam() throws JspException {
+ List params = new LinkedList();
+ Set usedParams = new HashSet();
+
+ Param param = new Param();
+ param.setName("name");
+ param.setValue("value");
+ params.add(param);
+
+ String queryString = tag.createQueryString(params, usedParams, true);
+
+ assertEquals("?name=value", queryString);
+ }
+
+ public void testCreateQueryStringOneParamForExsistingQueryString()
+ throws JspException {
+ List params = new LinkedList();
+ Set usedParams = new HashSet();
+
+ Param param = new Param();
+ param.setName("name");
+ param.setValue("value");
+ params.add(param);
+
+ String queryString = tag.createQueryString(params, usedParams, false);
+
+ assertEquals("&name=value", queryString);
+ }
+
+ public void testCreateQueryStringOneParamEmptyValue() throws JspException {
+ List params = new LinkedList();
+ Set usedParams = new HashSet();
+
+ Param param = new Param();
+ param.setName("name");
+ param.setValue("");
+ params.add(param);
+
+ String queryString = tag.createQueryString(params, usedParams, true);
+
+ assertEquals("?name=", queryString);
+ }
+
+ public void testCreateQueryStringOneParamNullValue() throws JspException {
+ List params = new LinkedList();
+ Set usedParams = new HashSet();
+
+ Param param = new Param();
+ param.setName("name");
+ param.setValue(null);
+ params.add(param);
+
+ String queryString = tag.createQueryString(params, usedParams, true);
+
+ assertEquals("?name", queryString);
+ }
+
+ public void testCreateQueryStringOneParamAlreadyUsed() throws JspException {
+ List params = new LinkedList();
+ Set usedParams = new HashSet();
+
+ Param param = new Param();
+ param.setName("name");
+ param.setValue("value");
+ params.add(param);
+
+ usedParams.add("name");
+
+ String queryString = tag.createQueryString(params, usedParams, true);
+
+ assertEquals("", queryString);
+ }
+
+ public void testCreateQueryStringTwoParams() throws JspException {
+ List params = new LinkedList();
+ Set usedParams = new HashSet();
+
+ Param param = new Param();
+ param.setName("name");
+ param.setValue("value");
+ params.add(param);
+
+ param = new Param();
+ param.setName("name");
+ param.setValue("value2");
+ params.add(param);
+
+ String queryString = tag.createQueryString(params, usedParams, true);
+
+ assertEquals("?name=value&name=value2", queryString);
+ }
+
+ public void testCreateQueryStringUrlEncoding() throws JspException {
+ List params = new LinkedList();
+ Set usedParams = new HashSet();
+
+ Param param = new Param();
+ param.setName("n me");
+ param.setValue("v&l=e");
+ params.add(param);
+
+ param = new Param();
+ param.setName("name");
+ param.setValue("value2");
+ params.add(param);
+
+ String queryString = tag.createQueryString(params, usedParams, true);
+
+ assertEquals("?n+me=v%26l%3De&name=value2", queryString);
+ }
+
+ public void testCreateQueryStringParamNullName() throws JspException {
+ List params = new LinkedList();
+ Set usedParams = new HashSet();
+
+ Param param = new Param();
+ param.setName(null);
+ param.setValue("value");
+ params.add(param);
+
+ String queryString = tag.createQueryString(params, usedParams, true);
+
+ assertEquals("", queryString);
+ }
+
+ public void testCreateQueryStringParamEmptyName() throws JspException {
+ List params = new LinkedList();
+ Set usedParams = new HashSet();
+
+ Param param = new Param();
+ param.setName("");
+ param.setValue("value");
+ params.add(param);
+
+ String queryString = tag.createQueryString(params, usedParams, true);
+
+ assertEquals("", queryString);
+ }
+
+ public void testReplaceUriTemplateParamsNoParams() throws JspException {
+ List params = new LinkedList();
+ Set usedParams = new HashSet();
+
+ String uri = tag.replaceUriTemplateParams("url/path", params,
+ usedParams);
+
+ assertEquals("url/path", uri);
+ assertEquals(0, usedParams.size());
+ }
+
+ public void testReplaceUriTemplateParamsTemplateWithoutParamMatch()
+ throws JspException {
+ List params = new LinkedList();
+ Set usedParams = new HashSet();
+
+ String uri = tag.replaceUriTemplateParams("url/{path}", params,
+ usedParams);
+
+ assertEquals("url/{path}", uri);
+ assertEquals(0, usedParams.size());
+ }
+
+ public void testReplaceUriTemplateParamsTemplateWithParamMatch()
+ throws JspException {
+ List params = new LinkedList();
+ Set usedParams = new HashSet();
+
+ Param param = new Param();
+ param.setName("name");
+ param.setValue("value");
+ params.add(param);
+
+ String uri = tag.replaceUriTemplateParams("url/{name}", params,
+ usedParams);
+
+ assertEquals("url/value", uri);
+ assertEquals(1, usedParams.size());
+ assertTrue(usedParams.contains("name"));
+ }
+
+ public void testReplaceUriTemplateParamsTemplateWithParamMatchNamePreEncoding()
+ throws JspException {
+ List params = new LinkedList();
+ Set usedParams = new HashSet();
+
+ Param param = new Param();
+ param.setName("n me");
+ param.setValue("value");
+ params.add(param);
+
+ String uri = tag.replaceUriTemplateParams("url/{n me}", params,
+ usedParams);
+
+ assertEquals("url/value", uri);
+ assertEquals(1, usedParams.size());
+ assertTrue(usedParams.contains("n me"));
+ }
+
+ public void testReplaceUriTemplateParamsTemplateWithParamMatchValueEncoded()
+ throws JspException {
+ List params = new LinkedList();
+ Set usedParams = new HashSet();
+
+ Param param = new Param();
+ param.setName("name");
+ param.setValue("v lue");
+ params.add(param);
+
+ String uri = tag.replaceUriTemplateParams("url/{name}", params,
+ usedParams);
+
+ assertEquals("url/v+lue", uri);
+ assertEquals(1, usedParams.size());
+ assertTrue(usedParams.contains("name"));
+ }
+
+ public void testCreateUrlRemoteServer() throws JspException {
+ tag.setValue("http://www.springframework.org/");
+
+ tag.doStartTag();
+
+ // String uri = tag.createUrl();
+ String uri = invokeCreateUrl(tag);
+
+ assertEquals("http://www.springframework.org/", uri);
+ }
+
+ public void testCreateUrlRelative() throws JspException {
+ tag.setValue("url/path");
+
+ tag.doStartTag();
+
+ String uri = invokeCreateUrl(tag);
+
+ assertEquals("url/path", uri);
+ }
+
+ public void testCreateUrlLocalContext() throws JspException {
+ ((MockHttpServletRequest) context.getRequest())
+ .setContextPath("/app-context");
+
+ tag.setValue("/url/path");
+
+ tag.doStartTag();
+
+ String uri = invokeCreateUrl(tag);
+
+ assertEquals("/app-context/url/path", uri);
+ }
+
+ public void testCreateUrlRemoteContext() throws JspException {
+ ((MockHttpServletRequest) context.getRequest())
+ .setContextPath("/app-context");
+
+ tag.setValue("/url/path");
+ tag.setContext("some-other-context");
+
+ tag.doStartTag();
+
+ String uri = invokeCreateUrl(tag);
+
+ assertEquals("/some-other-context/url/path", uri);
+ }
+
+ public void testCreateUrlRemoteContextWithSlash() throws JspException {
+ ((MockHttpServletRequest) context.getRequest())
+ .setContextPath("/app-context");
+
+ tag.setValue("/url/path");
+ tag.setContext("/some-other-context");
+
+ tag.doStartTag();
+
+ String uri = invokeCreateUrl(tag);
+
+ assertEquals("/some-other-context/url/path", uri);
+ }
+
+ public void testCreateUrlWithParams() throws JspException {
+ tag.setValue("url/path");
+
+ tag.doStartTag();
+
+ Param param = new Param();
+ param.setName("name");
+ param.setValue("value");
+ tag.addParam(param);
+
+ param = new Param();
+ param.setName("n me");
+ param.setValue("v lue");
+ tag.addParam(param);
+
+ String uri = invokeCreateUrl(tag);
+
+ assertEquals("url/path?name=value&n+me=v+lue", uri);
+ }
+
+ public void testCreateUrlWithTemplateParams() throws JspException {
+ tag.setValue("url/{name}");
+
+ tag.doStartTag();
+
+ Param param = new Param();
+ param.setName("name");
+ param.setValue("value");
+ tag.addParam(param);
+
+ param = new Param();
+ param.setName("n me");
+ param.setValue("v lue");
+ tag.addParam(param);
+
+ String uri = invokeCreateUrl(tag);
+
+ assertEquals("url/value?n+me=v+lue", uri);
+ }
+
+ public void testCreateUrlWithParamAndExsistingQueryString()
+ throws JspException {
+ tag.setValue("url/path?foo=bar");
+
+ tag.doStartTag();
+
+ Param param = new Param();
+ param.setName("name");
+ param.setValue("value");
+ tag.addParam(param);
+
+ String uri = invokeCreateUrl(tag);
+
+ assertEquals("url/path?foo=bar&name=value", uri);
+ }
+
+ public void testUrlEncode() throws JspException {
+ assertEquals("my+name", tag.urlEncode("my name"));
+ }
+
+ public void testUrlEncodeNull() throws JspException {
+ assertNull(tag.urlEncode(null));
+ }
+
+ public void testUrlEncodeBadEncoding() {
+ context.getResponse().setCharacterEncoding("bad encoding");
+
+ try {
+ tag.urlEncode("my name");
+ fail("expected JspException");
+ }
+ catch (JspException e) {
+ // we want this
+ }
+ }
+
+ public void testEscapeXml() {
+ assertEquals("<script type="text/javascript">", tag
+ .escapeXml("