diff --git a/grails-codecs-core/src/main/groovy/org/grails/plugins/codecs/Base64CodecExtensionMethods.groovy b/grails-codecs-core/src/main/groovy/org/grails/plugins/codecs/Base64CodecExtensionMethods.groovy index 91e7b8a1a69..c421af4a888 100644 --- a/grails-codecs-core/src/main/groovy/org/grails/plugins/codecs/Base64CodecExtensionMethods.groovy +++ b/grails-codecs-core/src/main/groovy/org/grails/plugins/codecs/Base64CodecExtensionMethods.groovy @@ -18,38 +18,42 @@ */ package org.grails.plugins.codecs +import java.nio.charset.Charset import java.nio.charset.StandardCharsets import org.codehaus.groovy.runtime.NullObject import org.apache.commons.codec.binary.Base64 +import groovy.transform.CompileStatic + /** * A codec that encodes and decodes Objects using Base64 encoding. * * @author Drew Varner */ +@CompileStatic class Base64CodecExtensionMethods { - static encodeAsBase64(theTarget) { + static Object encodeAsBase64(Object theTarget, Charset charset = StandardCharsets.UTF_8) { if (theTarget == null || theTarget instanceof NullObject) { return null } if (theTarget instanceof Byte[] || theTarget instanceof byte[]) { - return new String(Base64.encodeBase64(theTarget)) + return new String(Base64.encodeBase64(DigestUtils.toByteArray(theTarget)), StandardCharsets.UTF_8) } - return new String(Base64.encodeBase64(theTarget.toString().getBytes(StandardCharsets.UTF_8))) + return new String(Base64.encodeBase64(theTarget.toString().getBytes(charset)), StandardCharsets.UTF_8) } - static decodeBase64(theTarget) { + static Object decodeBase64(Object theTarget, Charset charset = StandardCharsets.UTF_8) { if (theTarget == null || theTarget instanceof NullObject) { return null } if (theTarget instanceof Byte[] || theTarget instanceof byte[]) { - return Base64.decodeBase64(theTarget) + return Base64.decodeBase64(DigestUtils.toByteArray(theTarget)) } return Base64.decodeBase64(theTarget.toString().getBytes(StandardCharsets.UTF_8)) diff --git a/grails-codecs-core/src/main/groovy/org/grails/plugins/codecs/DigestUtils.groovy b/grails-codecs-core/src/main/groovy/org/grails/plugins/codecs/DigestUtils.groovy index fccb6489a10..7b8222d5c99 100644 --- a/grails-codecs-core/src/main/groovy/org/grails/plugins/codecs/DigestUtils.groovy +++ b/grails-codecs-core/src/main/groovy/org/grails/plugins/codecs/DigestUtils.groovy @@ -18,30 +18,89 @@ */ package org.grails.plugins.codecs +import java.lang.reflect.Array import java.nio.charset.StandardCharsets import java.security.MessageDigest +import groovy.transform.CompileStatic +import org.codehaus.groovy.runtime.typehandling.DefaultTypeTransformation + +@CompileStatic abstract class DigestUtils { // Digest byte[], any list/array or string into a byte[] - static digest(String algorithm, data) { + static Object digest(String algorithm, Object data) { if (data == null) { return null } - def md = MessageDigest.getInstance(algorithm) - def src - if (data instanceof Byte[] || data instanceof byte[]) { - src = data + MessageDigest md = MessageDigest.getInstance(algorithm) + byte[] src = toByteArray(data) + md.update(src) // This probably needs to use the thread's Locale encoding + return md.digest() + } + + protected static byte[] toByteArray(Object data) { + if (data instanceof byte[]) { + return (byte[]) data } - else if (data instanceof List || data.getClass().isArray()) { - src = new byte[data.size()] - data.eachWithIndex { v, i -> src[i] = v } + if (data instanceof Byte[]) { + return toByteArrayFromWrapper((Byte[]) data) } - else { - src = data.toString().getBytes(StandardCharsets.UTF_8) + if (data instanceof Iterable) { + return toByteArrayFromIterable((Iterable) data) } - md.update(src) // This probably needs to use the thread's Locale encoding - return md.digest() + if (data instanceof Iterator) { + return toByteArrayFromIterator((Iterator) data) + } + if (data.getClass().isArray()) { + return toByteArrayFromArray(data, Array.getLength(data)) + } + + return data.toString().getBytes(StandardCharsets.UTF_8) + } + + private static byte[] toByteArrayFromWrapper(Byte[] data) { + byte[] result = new byte[data.length] + for (int i = 0; i < data.length; i++) { + result[i] = data[i].byteValue() + } + return result + } + + private static byte[] toByteArrayFromIterable(Iterable data) { + List bytes = new ArrayList() + for (Object value : data) { + bytes.add(toByte(value)) + } + return toByteArrayFromList(bytes) + } + + private static byte[] toByteArrayFromIterator(Iterator data) { + List bytes = new ArrayList() + while (data.hasNext()) { + bytes.add(toByte(data.next())) + } + return toByteArrayFromList(bytes) + } + + private static byte[] toByteArrayFromList(List data) { + byte[] result = new byte[data.size()] + for (int i = 0; i < data.size(); i++) { + result[i] = data.get(i) + } + return result + } + + private static byte[] toByteArrayFromArray(Object data, int length) { + byte[] result = new byte[length] + for (int i = 0; i < length; i++) { + result[i] = toByte(Array.get(data, i)) + } + return result + } + + private static byte toByte(Object value) { + DefaultTypeTransformation.castToNumber(value).byteValue() } } diff --git a/grails-codecs-core/src/main/groovy/org/grails/plugins/codecs/HexCodecExtensionMethods.groovy b/grails-codecs-core/src/main/groovy/org/grails/plugins/codecs/HexCodecExtensionMethods.groovy index 97af8be4fdc..5770527132a 100644 --- a/grails-codecs-core/src/main/groovy/org/grails/plugins/codecs/HexCodecExtensionMethods.groovy +++ b/grails-codecs-core/src/main/groovy/org/grails/plugins/codecs/HexCodecExtensionMethods.groovy @@ -21,51 +21,48 @@ package org.grails.plugins.codecs import java.nio.charset.StandardCharsets import org.codehaus.groovy.runtime.NullObject +import org.codehaus.groovy.runtime.typehandling.DefaultTypeTransformation +import groovy.transform.CompileStatic + +@CompileStatic class HexCodecExtensionMethods { - static HEXDIGITS = '0123456789abcdef' + static Object HEXDIGITS = '0123456789abcdef' // Expects an array/list of numbers - static encodeAsHex(theTarget) { + static Object encodeAsHex(Object theTarget) { if (theTarget == null || theTarget instanceof NullObject) { return null } - def result = new StringBuilder() - if (theTarget instanceof String) { - theTarget = theTarget.getBytes(StandardCharsets.UTF_8) - } - theTarget.each() { - result << HexCodecExtensionMethods.HEXDIGITS[(it & 0xF0) >> 4] - result << HexCodecExtensionMethods.HEXDIGITS[it & 0x0F] + byte[] bytes = theTarget instanceof String ? ((String) theTarget).getBytes(StandardCharsets.UTF_8) : DigestUtils.toByteArray(theTarget) + StringBuilder result = new StringBuilder(bytes.length * 2) + String hexDigits = (String) HEXDIGITS + for (byte value : bytes) { + int unsignedValue = value & 0xFF + result.append(hexDigits.charAt((unsignedValue & 0xF0) >> 4)) + result.append(hexDigits.charAt(unsignedValue & 0x0F)) } return result.toString() } - static decodeHex(theTarget) { - if (!theTarget) return null + static Object decodeHex(Object theTarget) { + if (theTarget instanceof CharSequence && theTarget.length() == 0) return new byte[0] + if (!DefaultTypeTransformation.castToBoolean(theTarget)) return null - def output = [] - - def str = theTarget.toString().toLowerCase() - if (str.size() % 2) { + String str = theTarget.toString().toLowerCase() + if (str.length() % 2 != 0) { throw new UnsupportedOperationException('Decode of hex strings requires strings of even length') } - def currentByte - str.eachWithIndex { val, idx -> - if (!(idx % 2)) { - currentByte = HEXDIGITS.indexOf(val) << 4 - } - else { - output << (currentByte | HEXDIGITS.indexOf(val)) - currentByte = 0 - } + byte[] result = new byte[str.length() >> 1] + String hexDigits = (String) HEXDIGITS + for (int i = 0; i < str.length(); i += 2) { + int high = hexDigits.indexOf((int) str.charAt(i)) + int low = hexDigits.indexOf((int) str.charAt(i + 1)) + result[i >> 1] = (byte) ((high << 4) | low) } - - def result = new byte[output.size()] - output.eachWithIndex { v, i -> result[i] = v } return result } } diff --git a/grails-codecs-core/src/main/groovy/org/grails/plugins/codecs/MD5BytesCodecExtensionMethods.groovy b/grails-codecs-core/src/main/groovy/org/grails/plugins/codecs/MD5BytesCodecExtensionMethods.groovy index f6d601f208d..55f2e605832 100644 --- a/grails-codecs-core/src/main/groovy/org/grails/plugins/codecs/MD5BytesCodecExtensionMethods.groovy +++ b/grails-codecs-core/src/main/groovy/org/grails/plugins/codecs/MD5BytesCodecExtensionMethods.groovy @@ -20,18 +20,21 @@ package org.grails.plugins.codecs import org.codehaus.groovy.runtime.NullObject +import groovy.transform.CompileStatic + +@CompileStatic class MD5BytesCodecExtensionMethods { // Returns the byte[] of the digest, taken from UTF-8 of the string representation // or the raw data coerced to bytes - static encodeAsMD5Bytes(theTarget) { + static Object encodeAsMD5Bytes(Object theTarget) { if (theTarget == null || theTarget instanceof NullObject) { return null } DigestUtils.digest('MD5', theTarget) } - static decodeMD5Bytes(theTarget) { + static Object decodeMD5Bytes(Object theTarget) { throw new UnsupportedOperationException('Cannot decode MD5 hashes') } } diff --git a/grails-codecs-core/src/main/groovy/org/grails/plugins/codecs/MD5CodecExtensionMethods.groovy b/grails-codecs-core/src/main/groovy/org/grails/plugins/codecs/MD5CodecExtensionMethods.groovy index 06b503bc130..d26cfe5189a 100644 --- a/grails-codecs-core/src/main/groovy/org/grails/plugins/codecs/MD5CodecExtensionMethods.groovy +++ b/grails-codecs-core/src/main/groovy/org/grails/plugins/codecs/MD5CodecExtensionMethods.groovy @@ -18,15 +18,19 @@ */ package org.grails.plugins.codecs +import groovy.transform.CompileStatic + +@CompileStatic class MD5CodecExtensionMethods { // Returns the byte[] of the digest, taken from UTF-8 of the string representation // or the raw data coerced to bytes - static encodeAsMD5(theTarget) { - theTarget.encodeAsMD5Bytes()?.encodeAsHex() + static Object encodeAsMD5(Object theTarget) { + byte[] digest = (byte[]) MD5BytesCodecExtensionMethods.encodeAsMD5Bytes(theTarget) + return digest == null ? null : HexCodecExtensionMethods.encodeAsHex(digest) } - static decodeMD5(theTarget) { + static Object decodeMD5(Object theTarget) { throw new UnsupportedOperationException('Cannot decode MD5 hashes') } } diff --git a/grails-codecs-core/src/main/groovy/org/grails/plugins/codecs/SHA1BytesCodecExtensionMethods.groovy b/grails-codecs-core/src/main/groovy/org/grails/plugins/codecs/SHA1BytesCodecExtensionMethods.groovy index 3f5e79d8d51..f7feb37354c 100644 --- a/grails-codecs-core/src/main/groovy/org/grails/plugins/codecs/SHA1BytesCodecExtensionMethods.groovy +++ b/grails-codecs-core/src/main/groovy/org/grails/plugins/codecs/SHA1BytesCodecExtensionMethods.groovy @@ -20,17 +20,20 @@ package org.grails.plugins.codecs import org.codehaus.groovy.runtime.NullObject +import groovy.transform.CompileStatic + +@CompileStatic class SHA1BytesCodecExtensionMethods { // Returns the byte[] of the digest - static encodeAsSHA1Bytes(theTarget) { + static Object encodeAsSHA1Bytes(Object theTarget) { if (theTarget == null || theTarget instanceof NullObject) { return null } DigestUtils.digest('SHA-1', theTarget) } - static decodeSHA1Bytes(theTarget) { + static Object decodeSHA1Bytes(Object theTarget) { throw new UnsupportedOperationException('Cannot decode SHA-1 hashes') } } diff --git a/grails-codecs-core/src/main/groovy/org/grails/plugins/codecs/SHA1CodecExtensionMethods.groovy b/grails-codecs-core/src/main/groovy/org/grails/plugins/codecs/SHA1CodecExtensionMethods.groovy index 177e030256b..9a3d19f356f 100644 --- a/grails-codecs-core/src/main/groovy/org/grails/plugins/codecs/SHA1CodecExtensionMethods.groovy +++ b/grails-codecs-core/src/main/groovy/org/grails/plugins/codecs/SHA1CodecExtensionMethods.groovy @@ -20,17 +20,20 @@ package org.grails.plugins.codecs import org.codehaus.groovy.runtime.NullObject +import groovy.transform.CompileStatic + +@CompileStatic class SHA1CodecExtensionMethods { // Returns the byte[] of the digest - static encodeAsSHA1(theTarget) { + static Object encodeAsSHA1(Object theTarget) { if (theTarget == null || theTarget instanceof NullObject) { return null } - theTarget.encodeAsSHA1Bytes().encodeAsHex() + return HexCodecExtensionMethods.encodeAsHex(SHA1BytesCodecExtensionMethods.encodeAsSHA1Bytes(theTarget)) } - static decodeSHA1(theTarget) { + static Object decodeSHA1(Object theTarget) { throw new UnsupportedOperationException('Cannot decode SHA-1 hashes') } } diff --git a/grails-codecs-core/src/main/groovy/org/grails/plugins/codecs/SHA256BytesCodecExtensionMethods.groovy b/grails-codecs-core/src/main/groovy/org/grails/plugins/codecs/SHA256BytesCodecExtensionMethods.groovy index 335ca50df8a..a91a27014a0 100644 --- a/grails-codecs-core/src/main/groovy/org/grails/plugins/codecs/SHA256BytesCodecExtensionMethods.groovy +++ b/grails-codecs-core/src/main/groovy/org/grails/plugins/codecs/SHA256BytesCodecExtensionMethods.groovy @@ -20,17 +20,20 @@ package org.grails.plugins.codecs import org.codehaus.groovy.runtime.NullObject +import groovy.transform.CompileStatic + +@CompileStatic class SHA256BytesCodecExtensionMethods { // Returns the byte[] of the digest - static encodeAsSHA256Bytes(theTarget) { + static Object encodeAsSHA256Bytes(Object theTarget) { if (theTarget == null || theTarget instanceof NullObject) { return null } DigestUtils.digest('SHA-256', theTarget) } - static decodeSHA256Bytes(theTarget) { + static Object decodeSHA256Bytes(Object theTarget) { throw new UnsupportedOperationException('Cannot decode SHA-256 hashes') } } diff --git a/grails-codecs-core/src/main/groovy/org/grails/plugins/codecs/SHA256CodecExtensionMethods.groovy b/grails-codecs-core/src/main/groovy/org/grails/plugins/codecs/SHA256CodecExtensionMethods.groovy index ce231e0ce50..7067a4d4894 100644 --- a/grails-codecs-core/src/main/groovy/org/grails/plugins/codecs/SHA256CodecExtensionMethods.groovy +++ b/grails-codecs-core/src/main/groovy/org/grails/plugins/codecs/SHA256CodecExtensionMethods.groovy @@ -20,17 +20,20 @@ package org.grails.plugins.codecs import org.codehaus.groovy.runtime.NullObject +import groovy.transform.CompileStatic + +@CompileStatic class SHA256CodecExtensionMethods extends DigestUtils { // Returns the byte[] of the digest - static encodeAsSHA256(theTarget) { + static Object encodeAsSHA256(Object theTarget) { if (theTarget == null || theTarget instanceof NullObject) { return null } - theTarget.encodeAsSHA256Bytes()?.encodeAsHex() + return HexCodecExtensionMethods.encodeAsHex(SHA256BytesCodecExtensionMethods.encodeAsSHA256Bytes(theTarget)) } - static decodeSHA256(theTarget) { + static Object decodeSHA256(Object theTarget) { throw new UnsupportedOperationException('Cannot decode SHA-256 hashes') } } diff --git a/grails-codecs-core/src/test/groovy/org/grails/web/codecs/Base64CodecTests.groovy b/grails-codecs-core/src/test/groovy/org/grails/web/codecs/Base64CodecTests.groovy index 120c938a139..f3ec64ef6f3 100644 --- a/grails-codecs-core/src/test/groovy/org/grails/web/codecs/Base64CodecTests.groovy +++ b/grails-codecs-core/src/test/groovy/org/grails/web/codecs/Base64CodecTests.groovy @@ -19,6 +19,8 @@ package org.grails.web.codecs +import java.nio.charset.StandardCharsets + import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test @@ -79,4 +81,11 @@ class Base64CodecTests { assertEquals "dGVzdA==", "test".encodeAsBase64() assertEquals "test", new String("dGVzdA==".decodeBase64()) } + + @Test + void testEncodeDecodeAsBase64WithCharset() { + assertEquals '6Q==', 'é'.encodeAsBase64(StandardCharsets.ISO_8859_1) + assertEquals 'é', new String('6Q=='.decodeBase64(StandardCharsets.ISO_8859_1), StandardCharsets.ISO_8859_1) + assertEquals '/v///g==', '￾'.encodeAsBase64(StandardCharsets.UTF_16) + } } diff --git a/grails-codecs-core/src/test/groovy/org/grails/web/codecs/HexCodecTests.groovy b/grails-codecs-core/src/test/groovy/org/grails/web/codecs/HexCodecTests.groovy index 307383e1697..6f502d51612 100644 --- a/grails-codecs-core/src/test/groovy/org/grails/web/codecs/HexCodecTests.groovy +++ b/grails-codecs-core/src/test/groovy/org/grails/web/codecs/HexCodecTests.groovy @@ -48,10 +48,25 @@ class HexCodecTests { assertIterableEquals(new Byte[] {65, 32, 66, 32, 67, 32, 68, 32, 69}.toList(), result.toList()) //make sure decoding null returns null assertEquals(null.decodeHex(), null) + + //make sure decoding Groovy-falsy values returns null + assertIterableEquals([], ''.decodeHex().toList()) + assertEquals(null, 0.decodeHex()) + assertEquals(null, false.decodeHex()) + assertEquals(null, [].decodeHex()) + assertEquals(null, new byte[0].decodeHex()) } + @Test void testRoundtrip() { - assertIterableEquals([65, 32, 66, 32, 67, 32, 68, 32, 69], [65, 32, 66, 32, 67, 32, 68, 32, 69].encodeAsHex().decodeHex().toList()) - assertIterableEquals([65, 32, 66, 32, 67, 32, 68, 32, 69], 'A B C D E'.encodeAsHex().decodeHex().toList()) + List expected = [65, 32, 66, 32, 67, 32, 68, 32, 69].collect { ((Number) it).byteValue() } + assertIterableEquals(expected, [65, 32, 66, 32, 67, 32, 68, 32, 69].encodeAsHex().decodeHex().toList()) + assertIterableEquals(expected, 'A B C D E'.encodeAsHex().decodeHex().toList()) + } + + @Test + void testEncodeIterableCoercion() { + assertEquals('4142', ['A' as char, 'B' as char].encodeAsHex()) + assertEquals('4142', new LinkedHashSet([65, 66]).encodeAsHex()) } } diff --git a/grails-doc/src/en/guide/security/codecs.adoc b/grails-doc/src/en/guide/security/codecs.adoc index cdcc4985b94..b3e2aa28347 100644 --- a/grails-doc/src/en/guide/security/codecs.adoc +++ b/grails-doc/src/en/guide/security/codecs.adoc @@ -135,6 +135,8 @@ Performs Base64 encode/decode functions. Example of usage: Your registration code is: ${user.registrationCode.encodeAsBase64()} ---- +String values can be encoded with a specific source character set by passing a `java.nio.charset.Charset` to `encodeAsBase64`. The Base64 text itself is returned as UTF-8/ASCII-compatible text. + *JavaScriptCodec* Escapes Strings so they can be used as valid JavaScript strings. For example: diff --git a/grails-web-common/src/main/groovy/grails/web/servlet/mvc/GrailsHttpSession.java b/grails-web-common/src/main/groovy/grails/web/servlet/mvc/GrailsHttpSession.java index eefd76ce165..1519251c3dd 100644 --- a/grails-web-common/src/main/groovy/grails/web/servlet/mvc/GrailsHttpSession.java +++ b/grails-web-common/src/main/groovy/grails/web/servlet/mvc/GrailsHttpSession.java @@ -19,6 +19,8 @@ package grails.web.servlet.mvc; import java.util.Enumeration; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; import jakarta.servlet.ServletContext; import jakarta.servlet.http.HttpServletRequest; @@ -32,8 +34,9 @@ */ public class GrailsHttpSession implements HttpSession { + private final Lock sessionLock = new ReentrantLock(); private HttpSession adaptee; - private HttpServletRequest request; + private final HttpServletRequest request; public GrailsHttpSession(HttpServletRequest request) { this.request = request; @@ -43,14 +46,18 @@ public GrailsHttpSession(HttpServletRequest request) { * @see jakarta.servlet.http.HttpSession#getAttribute(java.lang.String) */ public Object getAttribute(String name) { - createSessionIfNecessary(); - synchronized (this) { - return adaptee.getAttribute(name); + sessionLock.lock(); + try { + return createSessionIfNecessary().getAttribute(name); + } + finally { + sessionLock.unlock(); } } - private void createSessionIfNecessary() { + private HttpSession createSessionIfNecessary() { if (adaptee == null) adaptee = request.getSession(true); + return adaptee; } /* (non-Javadoc) @@ -58,9 +65,12 @@ private void createSessionIfNecessary() { */ @SuppressWarnings({ "rawtypes", "unchecked" }) public Enumeration getAttributeNames() { - createSessionIfNecessary(); - synchronized (this) { - return adaptee.getAttributeNames(); + sessionLock.lock(); + try { + return createSessionIfNecessary().getAttributeNames(); + } + finally { + sessionLock.unlock(); } } @@ -68,9 +78,12 @@ public Enumeration getAttributeNames() { * @see jakarta.servlet.http.HttpSession#getCreationTime() */ public long getCreationTime() { - createSessionIfNecessary(); - synchronized (this) { - return adaptee.getCreationTime(); + sessionLock.lock(); + try { + return createSessionIfNecessary().getCreationTime(); + } + finally { + sessionLock.unlock(); } } @@ -78,9 +91,12 @@ public long getCreationTime() { * @see jakarta.servlet.http.HttpSession#getId() */ public String getId() { - createSessionIfNecessary(); - synchronized (this) { - return adaptee.getId(); + sessionLock.lock(); + try { + return createSessionIfNecessary().getId(); + } + finally { + sessionLock.unlock(); } } @@ -88,9 +104,12 @@ public String getId() { * @see jakarta.servlet.http.HttpSession#getLastAccessedTime() */ public long getLastAccessedTime() { - createSessionIfNecessary(); - synchronized (this) { - return adaptee.getLastAccessedTime(); + sessionLock.lock(); + try { + return createSessionIfNecessary().getLastAccessedTime(); + } + finally { + sessionLock.unlock(); } } @@ -98,9 +117,12 @@ public long getLastAccessedTime() { * @see jakarta.servlet.http.HttpSession#getMaxInactiveInterval() */ public int getMaxInactiveInterval() { - createSessionIfNecessary(); - synchronized (this) { - return adaptee.getMaxInactiveInterval(); + sessionLock.lock(); + try { + return createSessionIfNecessary().getMaxInactiveInterval(); + } + finally { + sessionLock.unlock(); } } @@ -108,9 +130,12 @@ public int getMaxInactiveInterval() { * @see jakarta.servlet.http.HttpSession#getServletContext() */ public ServletContext getServletContext() { - createSessionIfNecessary(); - synchronized (this) { - return adaptee.getServletContext(); + sessionLock.lock(); + try { + return createSessionIfNecessary().getServletContext(); + } + finally { + sessionLock.unlock(); } } @@ -189,7 +214,8 @@ public void removeValue(String name) { */ @Deprecated public void invalidate() { - synchronized (this) { + sessionLock.lock(); + try { HttpSession session = adaptee; if (session == null) session = request.getSession(false); if (session != null) { @@ -197,15 +223,21 @@ public void invalidate() { session.invalidate(); } } + finally { + sessionLock.unlock(); + } } /* (non-Javadoc) * @see jakarta.servlet.http.HttpSession#isNew() */ public boolean isNew() { - createSessionIfNecessary(); - synchronized (this) { - return adaptee.isNew(); + sessionLock.lock(); + try { + return createSessionIfNecessary().isNew(); + } + finally { + sessionLock.unlock(); } } @@ -213,9 +245,12 @@ public boolean isNew() { * @see jakarta.servlet.http.HttpSession#removeAttribute(java.lang.String) */ public void removeAttribute(String name) { - createSessionIfNecessary(); - synchronized (this) { - adaptee.removeAttribute(name); + sessionLock.lock(); + try { + createSessionIfNecessary().removeAttribute(name); + } + finally { + sessionLock.unlock(); } } @@ -223,9 +258,12 @@ public void removeAttribute(String name) { * @see jakarta.servlet.http.HttpSession#setAttribute(java.lang.String, java.lang.Object) */ public void setAttribute(String name, Object value) { - createSessionIfNecessary(); - synchronized (this) { - adaptee.setAttribute(name, value); + sessionLock.lock(); + try { + createSessionIfNecessary().setAttribute(name, value); + } + finally { + sessionLock.unlock(); } } @@ -233,26 +271,35 @@ public void setAttribute(String name, Object value) { * @see jakarta.servlet.http.HttpSession#setMaxInactiveInterval(int) */ public void setMaxInactiveInterval(int arg0) { - createSessionIfNecessary(); - synchronized (this) { - adaptee.setMaxInactiveInterval(arg0); + sessionLock.lock(); + try { + createSessionIfNecessary().setMaxInactiveInterval(arg0); + } + finally { + sessionLock.unlock(); } } @SuppressWarnings("rawtypes") @Override public String toString() { - createSessionIfNecessary(); - StringBuilder sb = new StringBuilder("Session Content:\n"); - Enumeration e = adaptee.getAttributeNames(); - while (e.hasMoreElements()) { - String name = (String) e.nextElement(); - sb.append(" "); - sb.append(name); - sb.append(" = "); - sb.append(adaptee.getAttribute(name)); - sb.append('\n'); + sessionLock.lock(); + try { + HttpSession session = createSessionIfNecessary(); + StringBuilder sb = new StringBuilder("Session Content:\n"); + Enumeration e = session.getAttributeNames(); + while (e.hasMoreElements()) { + String name = (String) e.nextElement(); + sb.append(" "); + sb.append(name); + sb.append(" = "); + sb.append(session.getAttribute(name)); + sb.append('\n'); + } + return sb.toString(); + } + finally { + sessionLock.unlock(); } - return sb.toString(); } } diff --git a/grails-web-common/src/test/groovy/grails/web/servlet/mvc/GrailsHttpSessionSpec.groovy b/grails-web-common/src/test/groovy/grails/web/servlet/mvc/GrailsHttpSessionSpec.groovy index e6f4a418daa..eb7aadfaee8 100644 --- a/grails-web-common/src/test/groovy/grails/web/servlet/mvc/GrailsHttpSessionSpec.groovy +++ b/grails-web-common/src/test/groovy/grails/web/servlet/mvc/GrailsHttpSessionSpec.groovy @@ -24,6 +24,30 @@ import spock.lang.Specification class GrailsHttpSessionSpec extends Specification { + void 'attribute access creates servlet session lazily and delegates to the adaptee'() { + given: + def request = new MockHttpServletRequest() + def session = new GrailsHttpSession(request) + + expect: + request.getSession(false) == null + + when: + session.setAttribute('name', 'value') + + then: + request.getSession(false).getAttribute('name') == 'value' + session.getAttribute('name') == 'value' + Collections.list(session.getAttributeNames()) == ['name'] + session.toString().contains('name = value') + + when: + session.removeAttribute('name') + + then: + session.getAttribute('name') == null + } + void 'invalidate does not create a servlet session when none exists'() { given: def request = new MockHttpServletRequest()