This is an automated email from the ASF dual-hosted git repository. markt pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/tomcat.git
The following commit(s) were added to refs/heads/main by this push: new 83fdd8b Implement new generic attribute methods for Cookies 83fdd8b is described below commit 83fdd8b9662e9a58ba83dc99ce64c3f317e2643b Author: Mark Thomas <ma...@apache.org> AuthorDate: Mon May 24 16:31:55 2021 +0100 Implement new generic attribute methods for Cookies --- java/jakarta/servlet/http/Cookie.java | 140 +++++++++++++++++----- java/jakarta/servlet/http/LocalStrings.properties | 3 + test/jakarta/servlet/http/TestCookie.java | 46 +++++++ webapps/docs/changelog.xml | 5 + 4 files changed, 164 insertions(+), 30 deletions(-) diff --git a/java/jakarta/servlet/http/Cookie.java b/java/jakarta/servlet/http/Cookie.java index 084e1e1..8422c65 100644 --- a/java/jakarta/servlet/http/Cookie.java +++ b/java/jakarta/servlet/http/Cookie.java @@ -21,9 +21,11 @@ import java.security.AccessController; import java.security.PrivilegedAction; import java.text.MessageFormat; import java.util.BitSet; +import java.util.Collections; import java.util.Locale; import java.util.Map; import java.util.ResourceBundle; +import java.util.TreeMap; /** * Creates a cookie, a small amount of information sent by a servlet to a Web @@ -56,6 +58,9 @@ import java.util.ResourceBundle; */ public class Cookie implements Cloneable, Serializable { + private static final String LSTRING_FILE = "jakarta.servlet.http.LocalStrings"; + private static final ResourceBundle LSTRINGS = ResourceBundle.getBundle(LSTRING_FILE); + private static final CookieNameValidator validation; static { @@ -103,22 +108,22 @@ public class Cookie implements Cloneable, Serializable { } } - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = 2L; private final String name; private String value; private int version = 0; // ;Version=1 ... means RFC 2109 style - // // Attributes encoded in the header's cookie fields. - // - private String comment; // ;Comment=VALUE ... describes cookie's use - private String domain; // ;Domain=VALUE ... domain that sees cookie - private int maxAge = -1; // ;Max-Age=VALUE ... cookies auto-expire - private String path; // ;Path=VALUE ... URLs that see the cookie - private boolean secure; // ;Secure ... e.g. use SSL - private boolean httpOnly; // Not in cookie specs, but supported by browsers + private volatile Map<String,String> attributes; + + private static final String COMMENT = "Comment"; + private static final String DOMAIN = "Domain"; + private static final String MAX_AGE = "Max-Age"; + private static final String PATH = "Path"; + private static final String SECURE = "Secure"; + private static final String HTTP_ONLY = "HttpOnly"; /** * Constructs a cookie with a specified name and value. @@ -153,6 +158,7 @@ public class Cookie implements Cloneable, Serializable { this.value = value; } + /** * Specifies a comment that describes a cookie's purpose. The comment is * useful if the browser presents the cookie to the user. Comments are not @@ -164,9 +170,10 @@ public class Cookie implements Cloneable, Serializable { * @see #getComment */ public void setComment(String purpose) { - comment = purpose; + setAttributeInternal(COMMENT, purpose); } + /** * Returns the comment describing the purpose of this cookie, or * <code>null</code> if the cookie has no comment. @@ -176,9 +183,10 @@ public class Cookie implements Cloneable, Serializable { * @see #setComment */ public String getComment() { - return comment; + return getAttribute(COMMENT); } + /** * Specifies the domain within which this cookie should be presented. * <p> @@ -194,9 +202,15 @@ public class Cookie implements Cloneable, Serializable { * @see #getDomain */ public void setDomain(String pattern) { - domain = pattern.toLowerCase(Locale.ENGLISH); // IE allegedly needs this + if (pattern == null) { + setAttributeInternal(DOMAIN, null); + } else { + // IE requires the domain to be lower case (unconfirmed) + setAttributeInternal(DOMAIN, pattern.toLowerCase(Locale.ENGLISH)); + } } + /** * Returns the domain name set for this cookie. The form of the domain name * is set by RFC 2109. @@ -205,9 +219,10 @@ public class Cookie implements Cloneable, Serializable { * @see #setDomain */ public String getDomain() { - return domain; + return getAttribute(DOMAIN); } + /** * Sets the maximum age of the cookie in seconds. * <p> @@ -226,9 +241,10 @@ public class Cookie implements Cloneable, Serializable { * @see #getMaxAge */ public void setMaxAge(int expiry) { - maxAge = expiry; + setAttributeInternal(MAX_AGE, Integer.toString(expiry)); } + /** * Returns the maximum age of the cookie, specified in seconds, By default, * <code>-1</code> indicating the cookie will persist until browser @@ -239,9 +255,15 @@ public class Cookie implements Cloneable, Serializable { * @see #setMaxAge */ public int getMaxAge() { - return maxAge; + String maxAge = getAttribute(MAX_AGE); + if (maxAge == null) { + return -1; + } else { + return Integer.parseInt(maxAge); + } } + /** * Specifies a path for the cookie to which the client should return the * cookie. @@ -260,9 +282,10 @@ public class Cookie implements Cloneable, Serializable { * @see #getPath */ public void setPath(String uri) { - path = uri; + setAttributeInternal(PATH, uri); } + /** * Returns the path on the server to which the browser returns this cookie. * The cookie is visible to all subpaths on the server. @@ -272,9 +295,10 @@ public class Cookie implements Cloneable, Serializable { * @see #setPath */ public String getPath() { - return path; + return getAttribute(PATH); } + /** * Indicates to the browser whether the cookie should only be sent using a * secure protocol, such as HTTPS or SSL. @@ -288,9 +312,10 @@ public class Cookie implements Cloneable, Serializable { * @see #getSecure */ public void setSecure(boolean flag) { - secure = flag; + setAttributeInternal(SECURE, Boolean.toString(flag)); } + /** * Returns <code>true</code> if the browser is sending cookies only over a * secure protocol, or <code>false</code> if the browser can send cookies @@ -301,9 +326,10 @@ public class Cookie implements Cloneable, Serializable { * @see #setSecure */ public boolean getSecure() { - return secure; + return Boolean.parseBoolean(getAttribute(SECURE)); } + /** * Returns the name of the cookie. The name cannot be changed after * creation. @@ -314,6 +340,7 @@ public class Cookie implements Cloneable, Serializable { return name; } + /** * Assigns a new value to a cookie after the cookie is created. If you use a * binary value, you may want to use BASE64 encoding. @@ -332,6 +359,7 @@ public class Cookie implements Cloneable, Serializable { value = newValue; } + /** * Returns the value of the cookie. * @@ -343,6 +371,7 @@ public class Cookie implements Cloneable, Serializable { return value; } + /** * Returns the version of the protocol this cookie complies with. Version 1 * complies with RFC 2109, and version 0 complies with the original cookie @@ -357,6 +386,7 @@ public class Cookie implements Cloneable, Serializable { return version; } + /** * Sets the version of the cookie protocol this cookie complies with. * Version 0 complies with the original Netscape cookie specification. @@ -374,6 +404,7 @@ public class Cookie implements Cloneable, Serializable { version = v; } + /** * Overrides the standard <code>java.lang.Object.clone</code> method to * return a copy of this cookie. @@ -387,6 +418,7 @@ public class Cookie implements Cloneable, Serializable { } } + /** * Sets the flag that controls if this cookie will be hidden from scripts on * the client side. @@ -396,9 +428,10 @@ public class Cookie implements Cloneable, Serializable { * @since Servlet 3.0 */ public void setHttpOnly(boolean httpOnly) { - this.httpOnly = httpOnly; + setAttributeInternal(HTTP_ONLY, Boolean.toString(httpOnly)); } + /** * Gets the flag that controls if this cookie will be hidden from scripts on * the client side. @@ -408,9 +441,10 @@ public class Cookie implements Cloneable, Serializable { * @since Servlet 3.0 */ public boolean isHttpOnly() { - return httpOnly; + return Boolean.parseBoolean(getAttribute(HTTP_ONLY)); } + /** * Sets the value for the given cookie attribute. When a value is set via * this method, the value returned by the attribute specific getter (if any) @@ -419,9 +453,8 @@ public class Cookie implements Cloneable, Serializable { * @param name Name of attribute to set * @param value Value of attribute * - * @throws IllegalArgumentException If the attribute name is null, contains - * any characters not permitted foe use in Cookie names or matches a - * name reserved by the cookie specification. + * @throws IllegalArgumentException If the attribute name is null or + * contains any characters not permitted for use in Cookie names. * * @throws NumberFormatException If the attribute is known to be numerical * but the provided value cannot be parsed to a number. @@ -429,9 +462,41 @@ public class Cookie implements Cloneable, Serializable { * @since Servlet 5.1 */ public void setAttribute(String name, String value) { - // TODO - Servlet 5.1 + if (name == null) { + throw new IllegalArgumentException(LSTRINGS.getString("cookie.attribute.invalidName.null")); + } + if (!validation.isToken(name)) { + String msg = LSTRINGS.getString("cookie.attribute.invalidName.notToken"); + throw new IllegalArgumentException(MessageFormat.format(msg, name)); + } + + if (name.equalsIgnoreCase(MAX_AGE)) { + if (value == null) { + setAttributeInternal(MAX_AGE, null); + } else { + // Integer.parseInt throws NFE if required + setMaxAge(Integer.parseInt(value)); + } + } else { + setAttributeInternal(name, value); + } + } + + + private void setAttributeInternal(String name, String value) { + if (attributes == null) { + if (value == null) { + return; + } else { + // Case insensitive keys but retain case used + attributes = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); + } + } + + attributes.put(name, value); } + /** * Obtain the value for a given attribute. Values returned from this method * must be consistent with the values set and returned by the attribute @@ -444,13 +509,28 @@ public class Cookie implements Cloneable, Serializable { * @since Servlet 5.1 */ public String getAttribute(String name) { - // TODO - Servlet 5.1 - return null; + if (attributes == null) { + return null; + } else { + return attributes.get(name); + } } + + /** + * Obtain the Map of attributes and values (excluding version) for this + * cookie. + * + * @return A read-only Map of attributes to values, excluding version. + * + * @since Servlet 5.1 + */ public Map<String,String> getAttributes() { - // TODO - Servlet 5.1 - return null; + if (attributes == null) { + return Collections.emptyMap(); + } else { + return Collections.unmodifiableMap(attributes); + } } } @@ -480,7 +560,7 @@ class CookieNameValidator { } } - private boolean isToken(String possibleToken) { + boolean isToken(String possibleToken) { int len = possibleToken.length(); for (int i = 0; i < len; i++) { diff --git a/java/jakarta/servlet/http/LocalStrings.properties b/java/jakarta/servlet/http/LocalStrings.properties index 3ea5922..4d0e8d8 100644 --- a/java/jakarta/servlet/http/LocalStrings.properties +++ b/java/jakarta/servlet/http/LocalStrings.properties @@ -13,6 +13,9 @@ # See the License for the specific language governing permissions and # limitations under the License. +cookie.attribute.invalidName.notToken=Cookie attribute name [{0}] is not valid as it is not a token +cookie.attribute.invalidName.null=Cookie attribute names may not be null + err.cookie_name_blank=Cookie name may not be null or zero length err.cookie_name_is_token=Cookie name [{0}] is a reserved token err.io.indexOutOfBounds=Invalid offset [{0}] and / or length [{1}] specified for array of size [{2}] diff --git a/test/jakarta/servlet/http/TestCookie.java b/test/jakarta/servlet/http/TestCookie.java index 353004e..7885de5 100644 --- a/test/jakarta/servlet/http/TestCookie.java +++ b/test/jakarta/servlet/http/TestCookie.java @@ -134,6 +134,52 @@ public class TestCookie { Cookie cookie = new Cookie("$Foo", null); } + @Test + public void testGetAttributes01() { + Cookie cookie = new Cookie("name", "value"); + Assert.assertEquals(0, cookie.getAttributes().size()); + } + + @Test + public void testMaxAge01() { + Cookie cookie = new Cookie("name", "value"); + Assert.assertEquals(-1, cookie.getMaxAge()); + + for (int value : new int[] { Integer.MIN_VALUE, -2, -1, 0, 1, 2, Integer.MAX_VALUE}) { + cookie.setMaxAge(value); + Assert.assertEquals(value, cookie.getMaxAge()); + } + } + + @Test + public void testAttribute01() { + Cookie cookie = new Cookie("name", "value"); + cookie.setAttribute("aaa", "bbb"); + Assert.assertEquals("bbb", cookie.getAttribute("aAa")); + cookie.setAttribute("aaa", ""); + Assert.assertEquals("", cookie.getAttribute("aAa")); + cookie.setAttribute("aaa", null); + Assert.assertNull(cookie.getAttribute("aAa")); + } + + @Test(expected = IllegalArgumentException.class) + public void testAttributeInvalid01() { + Cookie cookie = new Cookie("name", "value"); + cookie.setAttribute("a<aa", "bbb"); + } + + @Test(expected = IllegalArgumentException.class) + public void testAttributeInvalid02() { + Cookie cookie = new Cookie("name", "value"); + cookie.setAttribute(null, "bbb"); + } + + @Test(expected = NumberFormatException.class) + public void testAttributeInvalid03() { + Cookie cookie = new Cookie("name", "value"); + cookie.setAttribute("Max-Age", "bbb"); + } + public static void checkCharInName(CookieNameValidator validator, BitSet allowed) { for (char ch = 0; ch < allowed.size(); ch++) { boolean expected = allowed.get(ch); diff --git a/webapps/docs/changelog.xml b/webapps/docs/changelog.xml index 2b2a716..1eda6eb 100644 --- a/webapps/docs/changelog.xml +++ b/webapps/docs/changelog.xml @@ -147,6 +147,11 @@ before attempting conversion to String. Pull request provided by tianshuang. (markt) </fix> + <add> + Implement the new <code>Cookie</code> methods + <code>setAttribute()</code>, <code>getAttribute()</code> and + <code>getAttributes()</code> introduced in Servlet 5.1. (markt) + </add> </changelog> </subsection> <subsection name="Coyote"> --------------------------------------------------------------------- To unsubscribe, e-mail: dev-unsubscr...@tomcat.apache.org For additional commands, e-mail: dev-h...@tomcat.apache.org