diff --git a/grails-core/src/main/groovy/grails/util/AbstractTypeConvertingMap.java b/grails-core/src/main/groovy/grails/util/AbstractTypeConvertingMap.java index 37e486b51f2..b7daf52ba52 100644 --- a/grails-core/src/main/groovy/grails/util/AbstractTypeConvertingMap.java +++ b/grails-core/src/main/groovy/grails/util/AbstractTypeConvertingMap.java @@ -18,12 +18,7 @@ */ package grails.util; -import java.text.ParseException; -import java.text.SimpleDateFormat; -import java.util.ArrayList; -import java.util.Arrays; import java.util.Collection; -import java.util.Collections; import java.util.Date; import java.util.Iterator; import java.util.LinkedHashMap; @@ -35,6 +30,8 @@ import org.codehaus.groovy.runtime.DefaultGroovyMethods; import org.codehaus.groovy.util.HashCodeHelper; +import org.apache.grails.core.internal.util.TypeConverters; + /** * AbstractTypeConvertingMap is a Map with type conversion capabilities. * @@ -47,7 +44,6 @@ */ @SuppressWarnings({ "rawtypes", "unchecked" }) public abstract class AbstractTypeConvertingMap extends GroovyObjectSupport implements Map, Cloneable { - private static final String DEFAULT_DATE_FORMAT = "yyyy-MM-dd HH:mm:ss.S"; protected Map wrappedMap; public AbstractTypeConvertingMap() { @@ -120,29 +116,11 @@ public int hashCode() { * @return The integer value or null if there isn't one */ public Byte getByte(String name) { - Object o = get(name); - if (o instanceof Number) { - return ((Number) o).byteValue(); - } - - if (o != null) { - try { - String string = o.toString(); - if (string != null && string.length() > 0) { - return Byte.parseByte(string); - } - } - catch (NumberFormatException e) {} - } - return null; + return TypeConverters.toByte(get(name)); } public Byte getByte(String name, Integer defaultValue) { - Byte value = getByte(name); - if (value == null && defaultValue != null) { - value = (byte) defaultValue.intValue(); - } - return value; + return TypeConverters.toByte(get(name), defaultValue); } /** @@ -151,26 +129,11 @@ public Byte getByte(String name, Integer defaultValue) { * @return The Character value or null if there isn't one */ public Character getChar(String name) { - Object o = get(name); - if (o instanceof Character) { - return (Character) o; - } - - if (o != null) { - String string = o.toString(); - if (string != null && string.length() == 1) { - return string.charAt(0); - } - } - return null; + return TypeConverters.toCharacter(get(name)); } public Character getChar(String name, Integer defaultValue) { - Character value = getChar(name); - if (value == null && defaultValue != null) { - value = (char) defaultValue.intValue(); - } - return value; + return TypeConverters.toCharacter(get(name), defaultValue); } /** @@ -179,29 +142,11 @@ public Character getChar(String name, Integer defaultValue) { * @return The integer value or null if there isn't one */ public Integer getInt(String name) { - Object o = get(name); - if (o instanceof Number) { - return ((Number) o).intValue(); - } - - if (o != null) { - try { - String string = o.toString(); - if (string != null) { - return Integer.parseInt(string); - } - } - catch (NumberFormatException e) {} - } - return null; + return TypeConverters.toInteger(get(name)); } public Integer getInt(String name, Integer defaultValue) { - Integer value = getInt(name); - if (value == null) { - value = defaultValue; - } - return value; + return TypeConverters.toInteger(get(name), defaultValue); } /** @@ -210,26 +155,11 @@ public Integer getInt(String name, Integer defaultValue) { * @return The long value or null if there isn't one */ public Long getLong(String name) { - Object o = get(name); - if (o instanceof Number) { - return ((Number) o).longValue(); - } - - if (o != null) { - try { - return Long.parseLong(o.toString()); - } - catch (NumberFormatException e) {} - } - return null; + return TypeConverters.toLong(get(name)); } public Long getLong(String name, Long defaultValue) { - Long value = getLong(name); - if (value == null) { - value = defaultValue; - } - return value; + return TypeConverters.toLong(get(name), defaultValue); } /** @@ -238,29 +168,11 @@ public Long getLong(String name, Long defaultValue) { * @return The short value or null if there isn't one */ public Short getShort(String name) { - Object o = get(name); - if (o instanceof Number) { - return ((Number) o).shortValue(); - } - - if (o != null) { - try { - String string = o.toString(); - if (string != null) { - return Short.parseShort(string); - } - } - catch (NumberFormatException e) {} - } - return null; + return TypeConverters.toShort(get(name)); } public Short getShort(String name, Integer defaultValue) { - Short value = getShort(name); - if (value == null && defaultValue != null) { - value = defaultValue.shortValue(); - } - return value; + return TypeConverters.toShort(get(name), defaultValue); } /** @@ -269,29 +181,11 @@ public Short getShort(String name, Integer defaultValue) { * @return The double value or null if there isn't one */ public Double getDouble(String name) { - Object o = get(name); - if (o instanceof Number) { - return ((Number) o).doubleValue(); - } - - if (o != null) { - try { - String string = o.toString(); - if (string != null) { - return Double.parseDouble(string); - } - } - catch (NumberFormatException e) {} - } - return null; + return TypeConverters.toDouble(get(name)); } public Double getDouble(String name, Double defaultValue) { - Double value = getDouble(name); - if (value == null) { - value = defaultValue; - } - return value; + return TypeConverters.toDouble(get(name), defaultValue); } /** @@ -300,29 +194,11 @@ public Double getDouble(String name, Double defaultValue) { * @return The double value or null if there isn't one */ public Float getFloat(String name) { - Object o = get(name); - if (o instanceof Number) { - return ((Number) o).floatValue(); - } - - if (o != null) { - try { - String string = o.toString(); - if (string != null) { - return Float.parseFloat(string); - } - } - catch (NumberFormatException e) {} - } - return null; + return TypeConverters.toFloat(get(name)); } public Float getFloat(String name, Float defaultValue) { - Float value = getFloat(name); - if (value == null) { - value = defaultValue; - } - return value; + return TypeConverters.toFloat(get(name), defaultValue); } /** @@ -331,21 +207,7 @@ public Float getFloat(String name, Float defaultValue) { * @return The boolean value or null if there isn't one */ public Boolean getBoolean(String name) { - Object o = get(name); - if (o instanceof Boolean) { - return (Boolean) o; - } - - if (o != null) { - try { - String string = o.toString(); - if (string != null) { - return GrailsStringUtils.toBoolean(string); - } - } - catch (Exception e) {} - } - return null; + return TypeConverters.toBoolean(get(name)); } public Boolean getBoolean(String name, Boolean defaultValue) { @@ -358,13 +220,26 @@ public Boolean getBoolean(String name, Boolean defaultValue) { return value; } + /** + * Helper method for obtaining a String value from a parameter + * @param name The name of the parameter + * @return The String value or null if there isn't one + */ + public String getString(String name) { + return TypeConverters.toStringValue(get(name)); + } + + public String getString(String name, String defaultValue) { + return TypeConverters.toStringValue(get(name), defaultValue); + } + /** * Obtains a date for the parameter name using the default format * @param name - * @return The date (in the {@link DEFAULT_DATE_FORMAT}) or null + * @return The date or null */ public Date getDate(String name) { - return getDate(name, DEFAULT_DATE_FORMAT); + return TypeConverters.toDate(get(name)); } /** @@ -374,19 +249,7 @@ public Date getDate(String name) { * @return The date or null */ public Date getDate(String name, String format) { - Object value = get(name); - if (value instanceof Date) { - return (Date) value; - } - - if (value != null) { - try { - return new SimpleDateFormat(format).parse(value.toString()); - } catch (ParseException e) { - // ignore - } - } - return null; + return TypeConverters.toDate(get(name), format); } /** @@ -422,11 +285,7 @@ public Date date(String name, Collection formats) { } private Date getDate(String name, Collection formats) { - for (String format : formats) { - Date date = getDate(name, format); - if (date != null) return date; - } - return null; + return TypeConverters.toDate(get(name), formats); } /** @@ -435,17 +294,7 @@ private Date getDate(String name, Collection formats) { * @return A list of values */ public List getList(String name) { - Object paramValues = get(name); - if (paramValues == null) { - return Collections.emptyList(); - } - if (paramValues.getClass().isArray()) { - return Arrays.asList((Object[]) paramValues); - } - if (paramValues instanceof Collection) { - return new ArrayList((Collection) paramValues); - } - return Collections.singletonList(paramValues); + return TypeConverters.toList(get(name)); } public List list(String name) { diff --git a/grails-core/src/main/groovy/grails/util/TypeConvertingMap.groovy b/grails-core/src/main/groovy/grails/util/TypeConvertingMap.groovy index 7a503e48a9b..2156b6fac49 100644 --- a/grails-core/src/main/groovy/grails/util/TypeConvertingMap.groovy +++ b/grails-core/src/main/groovy/grails/util/TypeConvertingMap.groovy @@ -111,4 +111,12 @@ class TypeConvertingMap extends AbstractTypeConvertingMap { Boolean 'boolean'(String name, Boolean defaultValue) { return getBoolean(name, defaultValue) } + + String string(String name) { + return getString(name) + } + + String string(String name, String defaultValue) { + return getString(name, defaultValue) + } } diff --git a/grails-core/src/main/groovy/org/apache/grails/core/internal/util/TypeConverters.java b/grails-core/src/main/groovy/org/apache/grails/core/internal/util/TypeConverters.java new file mode 100644 index 00000000000..693cea1153b --- /dev/null +++ b/grails-core/src/main/groovy/org/apache/grails/core/internal/util/TypeConverters.java @@ -0,0 +1,274 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 + * + * https://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.apache.grails.core.internal.util; + +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Date; +import java.util.List; + +import grails.util.GrailsStringUtils; + +/** + * Internal helpers that convert a raw {@code Object} value to a target type without allocating + * an intermediate map. These power the type conversion methods on {@code TypeConvertingMap} and + * the servlet attribute extensions, keeping the conversion logic in a single place. + * + *

This is an internal class and is not part of the public Grails API. + * + * @since 8.0 + */ +public final class TypeConverters { + + public static final String DEFAULT_DATE_FORMAT = "yyyy-MM-dd HH:mm:ss.S"; + + private TypeConverters() { + } + + public static Byte toByte(Object o) { + if (o instanceof Number) { + return ((Number) o).byteValue(); + } + if (o != null) { + try { + String string = o.toString(); + if (string != null && string.length() > 0) { + return Byte.parseByte(string); + } + } + catch (NumberFormatException ignored) {} + } + return null; + } + + public static Character toCharacter(Object o) { + if (o instanceof Character) { + return (Character) o; + } + if (o != null) { + String string = o.toString(); + if (string != null && string.length() == 1) { + return string.charAt(0); + } + } + return null; + } + + public static Integer toInteger(Object o) { + if (o instanceof Number) { + return ((Number) o).intValue(); + } + if (o != null) { + try { + String string = o.toString(); + if (string != null) { + return Integer.parseInt(string); + } + } + catch (NumberFormatException ignored) {} + } + return null; + } + + public static Long toLong(Object o) { + if (o instanceof Number) { + return ((Number) o).longValue(); + } + if (o != null) { + try { + return Long.parseLong(o.toString()); + } + catch (NumberFormatException ignored) {} + } + return null; + } + + public static Short toShort(Object o) { + if (o instanceof Number) { + return ((Number) o).shortValue(); + } + if (o != null) { + try { + String string = o.toString(); + if (string != null) { + return Short.parseShort(string); + } + } + catch (NumberFormatException ignored) {} + } + return null; + } + + public static Double toDouble(Object o) { + if (o instanceof Number) { + return ((Number) o).doubleValue(); + } + if (o != null) { + try { + String string = o.toString(); + if (string != null) { + return Double.parseDouble(string); + } + } + catch (NumberFormatException ignored) {} + } + return null; + } + + public static Float toFloat(Object o) { + if (o instanceof Number) { + return ((Number) o).floatValue(); + } + if (o != null) { + try { + String string = o.toString(); + if (string != null) { + return Float.parseFloat(string); + } + } + catch (NumberFormatException ignored) {} + } + return null; + } + + public static Boolean toBoolean(Object o) { + if (o instanceof Boolean) { + return (Boolean) o; + } + if (o != null) { + try { + String string = o.toString(); + if (string != null) { + return GrailsStringUtils.toBoolean(string); + } + } + catch (Exception ignored) {} + } + return null; + } + + public static String toStringValue(Object o) { + if (o == null) { + return null; + } + if (o instanceof Object[]) { + Object[] array = (Object[]) o; + return array.length > 0 && array[0] != null ? array[0].toString() : null; + } + return o.toString(); + } + + public static Date toDate(Object value) { + return toDate(value, DEFAULT_DATE_FORMAT); + } + + public static Date toDate(Object value, String format) { + if (value instanceof Date) { + return (Date) value; + } + if (value != null) { + String string = value.toString(); + if (string != null) { + try { + return new SimpleDateFormat(format).parse(string); + } catch (ParseException ignored) {} + } + } + return null; + } + + public static Date toDate(Object value, Collection formats) { + for (String format : formats) { + Date date = toDate(value, format); + if (date != null) return date; + } + return null; + } + + public static List toList(Object paramValues) { + if (paramValues == null) { + return Collections.emptyList(); + } + if (paramValues.getClass().isArray()) { + return Arrays.asList((Object[]) paramValues); + } + if (paramValues instanceof Collection) { + return new ArrayList<>((Collection) paramValues); + } + return Collections.singletonList(paramValues); + } + + public static Byte toByte(Object o, Integer defaultValue) { + Byte value = toByte(o); + if (value == null && defaultValue != null) { + return (byte) defaultValue.intValue(); + } + return value; + } + + public static Character toCharacter(Object o, Integer defaultValue) { + Character value = toCharacter(o); + if (value == null && defaultValue != null) { + return (char) defaultValue.intValue(); + } + return value; + } + + public static Character toCharacter(Object o, Character defaultValue) { + Character value = toCharacter(o); + return value != null ? value : defaultValue; + } + + public static Short toShort(Object o, Integer defaultValue) { + Short value = toShort(o); + if (value == null && defaultValue != null) { + return defaultValue.shortValue(); + } + return value; + } + + public static Integer toInteger(Object o, Integer defaultValue) { + Integer value = toInteger(o); + return value != null ? value : defaultValue; + } + + public static Long toLong(Object o, Long defaultValue) { + Long value = toLong(o); + return value != null ? value : defaultValue; + } + + public static Double toDouble(Object o, Double defaultValue) { + Double value = toDouble(o); + return value != null ? value : defaultValue; + } + + public static Float toFloat(Object o, Float defaultValue) { + Float value = toFloat(o); + return value != null ? value : defaultValue; + } + + public static String toStringValue(Object o, String defaultValue) { + String value = toStringValue(o); + return value != null ? value : defaultValue; + } +} diff --git a/grails-core/src/test/groovy/org/grails/util/TypeConvertingMapTests.groovy b/grails-core/src/test/groovy/org/grails/util/TypeConvertingMapTests.groovy index 956bea51d41..cff8d062446 100644 --- a/grails-core/src/test/groovy/org/grails/util/TypeConvertingMapTests.groovy +++ b/grails-core/src/test/groovy/org/grails/util/TypeConvertingMapTests.groovy @@ -20,6 +20,7 @@ package org.grails.util import grails.util.TypeConvertingMap import groovy.transform.CompileStatic +import org.apache.grails.core.internal.util.TypeConverters import org.junit.jupiter.api.Test /** @@ -54,6 +55,97 @@ class TypeConvertingMapTests { assert !toTypeConverting(a: 1, b: 2).equals(toTypeConverting(b: 2, a: null)) } + @Test + void testGetString() { + def map = toTypeConverting(name: 'Bob', count: 5, missing: null) + + assert map.getString('name') == 'Bob' + assert map.getString('count') == '5' + assert map.getString('missing') == null + assert map.getString('absent') == null + } + + @Test + void testGetStringWithDefault() { + def map = toTypeConverting(name: 'Bob') + + assert map.getString('name', 'fallback') == 'Bob' + assert map.getString('absent', 'fallback') == 'fallback' + } + + @Test + void testGetStringWithArrayValueReturnsFirstElement() { + def map = toTypeConverting(names: ['Bob', 'Judy'] as String[], empty: new String[0]) + + assert map.getString('names') == 'Bob' + assert map.getString('empty') == null + } + + @Test + void testStringFacade() { + def map = toTypeConverting(name: 'Bob') + + assert map.string('name') == 'Bob' + assert map.string('absent', 'fallback') == 'fallback' + } + + @Test + @CompileStatic + void testStringIsStaticallyTyped() { + TypeConvertingMap map = new TypeConvertingMap(name: 'Bob') + String name = map.string('name') + + assert name == 'Bob' + } + + @Test + void testTypeConverters() { + assert TypeConverters.toInteger('42') == 42 + assert TypeConverters.toInteger(42L) == 42 + assert TypeConverters.toInteger('not a number') == null + assert TypeConverters.toInteger(null) == null + assert TypeConverters.toLong('42') == 42L + assert TypeConverters.toBoolean('true') == true + assert TypeConverters.toStringValue(['a', 'b'] as String[]) == 'a' + assert TypeConverters.toList('one') == ['one'] + assert TypeConverters.toList(['a', 'b'] as String[]) == ['a', 'b'] + } + + @Test + void testTypeConvertersWithDefaults() { + assert TypeConverters.toInteger('42', 7) == 42 + assert TypeConverters.toInteger(null, 7) == 7 + assert TypeConverters.toInteger('not a number', 7) == 7 + assert TypeConverters.toStringValue('present', 'fallback') == 'present' + assert TypeConverters.toStringValue(null, 'fallback') == 'fallback' + assert TypeConverters.toByte(null, 5) == (byte) 5 + assert TypeConverters.toShort(null, 5) == (short) 5 + assert TypeConverters.toCharacter(null, (int) 'A') == 'A' as char + } + + @Test + void testConvertersWithValueWhoseToStringReturnsNull() { + def value = new NullToString() + + assert TypeConverters.toByte(value) == null + assert TypeConverters.toCharacter(value) == null + assert TypeConverters.toInteger(value) == null + assert TypeConverters.toLong(value) == null + assert TypeConverters.toShort(value) == null + assert TypeConverters.toDouble(value) == null + assert TypeConverters.toFloat(value) == null + assert TypeConverters.toBoolean(value) == null + assert TypeConverters.toStringValue(value) == null + assert TypeConverters.toDate(value) == null + assert TypeConverters.toInteger(value, 7) == 7 + assert TypeConverters.toStringValue(value, 'fallback') == 'fallback' + } + + static class NullToString { + @Override + String toString() { null } + } + @Test void testHashCode() { assert toTypeConverting(a: 1, b: 2).hashCode() == toTypeConverting(a: 1, b: 2).hashCode() diff --git a/grails-doc/src/en/guide/theWebLayer/controllers/typeConverters.adoc b/grails-doc/src/en/guide/theWebLayer/controllers/typeConverters.adoc index d461705e090..ba5ef58327e 100644 --- a/grails-doc/src/en/guide/theWebLayer/controllers/typeConverters.adoc +++ b/grails-doc/src/en/guide/theWebLayer/controllers/typeConverters.adoc @@ -53,3 +53,30 @@ for (name in params.list('name')) { println name } ---- + + +=== Type Conversion of Attributes + + +The same convenience methods are also available on the `session`, `request`, `flash` and `servletContext` objects for converting attribute values. This is especially useful under static compilation (for example in a class annotated with `@CompileStatic` or `@GrailsCompileStatic`), where dynamic attribute access such as `session.userTimeZoneId` does not compile and the explicit alternative requires a cast: + +[source,groovy] +---- +// returns an Integer, or null if the attribute is absent or not convertible +def page = request.int('page') + +// returns the attribute as an Integer, or the supplied default if absent +def page = request.int('page', 1) + +// String, Boolean, Long, Date and the other converters are available too +String timeZoneId = session.string('userTimeZoneId', 'America/Los_Angeles') +Boolean active = session.boolean('active', false) + +// flash and servletContext attributes work the same way +String notice = flash.string('notice') +int maxUploads = servletContext.int('maxUploads', 10) +---- + +As with `params`, each method is null-safe and safe from parsing errors, and each accepts an optional default value as a second argument that is returned when the attribute is absent or cannot be converted. + +NOTE: The `string` method returns the first element when the underlying attribute is an array, mirroring the single-value semantics of the other converters. Use the `list` method to obtain every value. diff --git a/grails-web-core/src/main/groovy/org/grails/web/servlet/FlashScopeExtension.groovy b/grails-web-core/src/main/groovy/org/grails/web/servlet/FlashScopeExtension.groovy new file mode 100644 index 00000000000..22ab7dc331d --- /dev/null +++ b/grails-web-core/src/main/groovy/org/grails/web/servlet/FlashScopeExtension.groovy @@ -0,0 +1,127 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 + * + * https://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.grails.web.servlet + +import groovy.transform.CompileStatic + +import org.apache.grails.core.internal.util.TypeConverters + +import grails.web.mvc.FlashScope + +/** + * An extension that adds type conversion methods to the {@link FlashScope} interface + * + * @since 8.0 + */ +@CompileStatic +class FlashScopeExtension { + + static Byte 'byte'(FlashScope flash, String name) { + TypeConverters.toByte(flash.get(name)) + } + + static Byte 'byte'(FlashScope flash, String name, Integer defaultValue) { + TypeConverters.toByte(flash.get(name), defaultValue) + } + + static Character 'char'(FlashScope flash, String name) { + TypeConverters.toCharacter(flash.get(name)) + } + + static Character 'char'(FlashScope flash, String name, Character defaultValue) { + TypeConverters.toCharacter(flash.get(name), defaultValue) + } + + static Character 'char'(FlashScope flash, String name, Integer defaultValue) { + TypeConverters.toCharacter(flash.get(name), defaultValue) + } + + static Short 'short'(FlashScope flash, String name) { + TypeConverters.toShort(flash.get(name)) + } + + static Short 'short'(FlashScope flash, String name, Integer defaultValue) { + TypeConverters.toShort(flash.get(name), defaultValue) + } + + static Integer 'int'(FlashScope flash, String name) { + TypeConverters.toInteger(flash.get(name)) + } + + static Integer 'int'(FlashScope flash, String name, Integer defaultValue) { + TypeConverters.toInteger(flash.get(name), defaultValue) + } + + static Long 'long'(FlashScope flash, String name) { + TypeConverters.toLong(flash.get(name)) + } + + static Long 'long'(FlashScope flash, String name, Long defaultValue) { + TypeConverters.toLong(flash.get(name), defaultValue) + } + + static Double 'double'(FlashScope flash, String name) { + TypeConverters.toDouble(flash.get(name)) + } + + static Double 'double'(FlashScope flash, String name, Double defaultValue) { + TypeConverters.toDouble(flash.get(name), defaultValue) + } + + static Float 'float'(FlashScope flash, String name) { + TypeConverters.toFloat(flash.get(name)) + } + + static Float 'float'(FlashScope flash, String name, Float defaultValue) { + TypeConverters.toFloat(flash.get(name), defaultValue) + } + + static Boolean 'boolean'(FlashScope flash, String name) { + TypeConverters.toBoolean(flash.get(name)) + } + + // boolean default is presence-based (key present), which cannot be expressed from the value alone + static Boolean 'boolean'(FlashScope flash, String name, Boolean defaultValue) { + flash.containsKey(name) ? TypeConverters.toBoolean(flash.get(name)) : defaultValue + } + + static String string(FlashScope flash, String name) { + TypeConverters.toStringValue(flash.get(name)) + } + + static String string(FlashScope flash, String name, String defaultValue) { + TypeConverters.toStringValue(flash.get(name), defaultValue) + } + + static List list(FlashScope flash, String name) { + TypeConverters.toList(flash.get(name)) + } + + static Date date(FlashScope flash, String name) { + TypeConverters.toDate(flash.get(name)) + } + + static Date date(FlashScope flash, String name, String format) { + TypeConverters.toDate(flash.get(name), format) + } + + static Date date(FlashScope flash, String name, Collection formats) { + TypeConverters.toDate(flash.get(name), formats) + } +} diff --git a/grails-web-core/src/main/groovy/org/grails/web/servlet/HttpServletRequestExtension.groovy b/grails-web-core/src/main/groovy/org/grails/web/servlet/HttpServletRequestExtension.groovy index 7e1beb85717..c8d2ac532bf 100644 --- a/grails-web-core/src/main/groovy/org/grails/web/servlet/HttpServletRequestExtension.groovy +++ b/grails-web-core/src/main/groovy/org/grails/web/servlet/HttpServletRequestExtension.groovy @@ -20,6 +20,8 @@ package org.grails.web.servlet import groovy.transform.CompileStatic +import org.apache.grails.core.internal.util.TypeConverters + import jakarta.servlet.http.HttpServletRequest import org.grails.web.util.WebUtils @@ -27,11 +29,11 @@ import org.grails.web.util.WebUtils /** * An extension that adds methods to the {@link HttpServletRequest} object * - * + * * @author Jeff Brown * @author Graeme Rocher * @since 3.0 - * + * */ @CompileStatic class HttpServletRequestExtension { @@ -155,4 +157,98 @@ class HttpServletRequestExtension { static boolean isPost(HttpServletRequest request) { request.method == 'POST' } + + static Byte 'byte'(HttpServletRequest request, String name) { + TypeConverters.toByte(request.getAttribute(name)) + } + + static Byte 'byte'(HttpServletRequest request, String name, Integer defaultValue) { + TypeConverters.toByte(request.getAttribute(name), defaultValue) + } + + static Character 'char'(HttpServletRequest request, String name) { + TypeConverters.toCharacter(request.getAttribute(name)) + } + + static Character 'char'(HttpServletRequest request, String name, Character defaultValue) { + TypeConverters.toCharacter(request.getAttribute(name), defaultValue) + } + + static Character 'char'(HttpServletRequest request, String name, Integer defaultValue) { + TypeConverters.toCharacter(request.getAttribute(name), defaultValue) + } + + static Short 'short'(HttpServletRequest request, String name) { + TypeConverters.toShort(request.getAttribute(name)) + } + + static Short 'short'(HttpServletRequest request, String name, Integer defaultValue) { + TypeConverters.toShort(request.getAttribute(name), defaultValue) + } + + static Integer 'int'(HttpServletRequest request, String name) { + TypeConverters.toInteger(request.getAttribute(name)) + } + + static Integer 'int'(HttpServletRequest request, String name, Integer defaultValue) { + TypeConverters.toInteger(request.getAttribute(name), defaultValue) + } + + static Long 'long'(HttpServletRequest request, String name) { + TypeConverters.toLong(request.getAttribute(name)) + } + + static Long 'long'(HttpServletRequest request, String name, Long defaultValue) { + TypeConverters.toLong(request.getAttribute(name), defaultValue) + } + + static Double 'double'(HttpServletRequest request, String name) { + TypeConverters.toDouble(request.getAttribute(name)) + } + + static Double 'double'(HttpServletRequest request, String name, Double defaultValue) { + TypeConverters.toDouble(request.getAttribute(name), defaultValue) + } + + static Float 'float'(HttpServletRequest request, String name) { + TypeConverters.toFloat(request.getAttribute(name)) + } + + static Float 'float'(HttpServletRequest request, String name, Float defaultValue) { + TypeConverters.toFloat(request.getAttribute(name), defaultValue) + } + + static Boolean 'boolean'(HttpServletRequest request, String name) { + TypeConverters.toBoolean(request.getAttribute(name)) + } + + // boolean default is presence-based (attribute set), which cannot be expressed from the value alone + static Boolean 'boolean'(HttpServletRequest request, String name, Boolean defaultValue) { + Object value = request.getAttribute(name) + value != null ? TypeConverters.toBoolean(value) : defaultValue + } + + static String string(HttpServletRequest request, String name) { + TypeConverters.toStringValue(request.getAttribute(name)) + } + + static String string(HttpServletRequest request, String name, String defaultValue) { + TypeConverters.toStringValue(request.getAttribute(name), defaultValue) + } + + static List list(HttpServletRequest request, String name) { + TypeConverters.toList(request.getAttribute(name)) + } + + static Date date(HttpServletRequest request, String name) { + TypeConverters.toDate(request.getAttribute(name)) + } + + static Date date(HttpServletRequest request, String name, String format) { + TypeConverters.toDate(request.getAttribute(name), format) + } + + static Date date(HttpServletRequest request, String name, Collection formats) { + TypeConverters.toDate(request.getAttribute(name), formats) + } } diff --git a/grails-web-core/src/main/groovy/org/grails/web/servlet/HttpSessionExtension.groovy b/grails-web-core/src/main/groovy/org/grails/web/servlet/HttpSessionExtension.groovy index c2d5dc6e361..663e514ce45 100644 --- a/grails-web-core/src/main/groovy/org/grails/web/servlet/HttpSessionExtension.groovy +++ b/grails-web-core/src/main/groovy/org/grails/web/servlet/HttpSessionExtension.groovy @@ -20,6 +20,8 @@ package org.grails.web.servlet import groovy.transform.CompileStatic +import org.apache.grails.core.internal.util.TypeConverters + import jakarta.servlet.http.HttpSession /** @@ -30,25 +32,119 @@ import jakarta.servlet.http.HttpSession * @author Graeme Rocher * * @since 3.0 - * + * */ @CompileStatic class HttpSessionExtension { - + static getProperty(HttpSession session, String name) { def mp = session.class.metaClass.getMetaProperty(name) return mp ? mp.getProperty(session) : session.getAttribute(name) } - + static propertyMissing(HttpSession session, String name, value) { session.setAttribute(name, value) } - + static getAt(HttpSession session, String name) { getProperty(session, name) } - + static propertyMissing(HttpSession session, String name) { getProperty(session, name) } + + static Byte 'byte'(HttpSession session, String name) { + TypeConverters.toByte(session.getAttribute(name)) + } + + static Byte 'byte'(HttpSession session, String name, Integer defaultValue) { + TypeConverters.toByte(session.getAttribute(name), defaultValue) + } + + static Character 'char'(HttpSession session, String name) { + TypeConverters.toCharacter(session.getAttribute(name)) + } + + static Character 'char'(HttpSession session, String name, Character defaultValue) { + TypeConverters.toCharacter(session.getAttribute(name), defaultValue) + } + + static Character 'char'(HttpSession session, String name, Integer defaultValue) { + TypeConverters.toCharacter(session.getAttribute(name), defaultValue) + } + + static Short 'short'(HttpSession session, String name) { + TypeConverters.toShort(session.getAttribute(name)) + } + + static Short 'short'(HttpSession session, String name, Integer defaultValue) { + TypeConverters.toShort(session.getAttribute(name), defaultValue) + } + + static Integer 'int'(HttpSession session, String name) { + TypeConverters.toInteger(session.getAttribute(name)) + } + + static Integer 'int'(HttpSession session, String name, Integer defaultValue) { + TypeConverters.toInteger(session.getAttribute(name), defaultValue) + } + + static Long 'long'(HttpSession session, String name) { + TypeConverters.toLong(session.getAttribute(name)) + } + + static Long 'long'(HttpSession session, String name, Long defaultValue) { + TypeConverters.toLong(session.getAttribute(name), defaultValue) + } + + static Double 'double'(HttpSession session, String name) { + TypeConverters.toDouble(session.getAttribute(name)) + } + + static Double 'double'(HttpSession session, String name, Double defaultValue) { + TypeConverters.toDouble(session.getAttribute(name), defaultValue) + } + + static Float 'float'(HttpSession session, String name) { + TypeConverters.toFloat(session.getAttribute(name)) + } + + static Float 'float'(HttpSession session, String name, Float defaultValue) { + TypeConverters.toFloat(session.getAttribute(name), defaultValue) + } + + static Boolean 'boolean'(HttpSession session, String name) { + TypeConverters.toBoolean(session.getAttribute(name)) + } + + // boolean default is presence-based (attribute set), which cannot be expressed from the value alone + static Boolean 'boolean'(HttpSession session, String name, Boolean defaultValue) { + Object value = session.getAttribute(name) + value != null ? TypeConverters.toBoolean(value) : defaultValue + } + + static String string(HttpSession session, String name) { + TypeConverters.toStringValue(session.getAttribute(name)) + } + + static String string(HttpSession session, String name, String defaultValue) { + TypeConverters.toStringValue(session.getAttribute(name), defaultValue) + } + + static List list(HttpSession session, String name) { + TypeConverters.toList(session.getAttribute(name)) + } + + static Date date(HttpSession session, String name) { + TypeConverters.toDate(session.getAttribute(name)) + } + + static Date date(HttpSession session, String name, String format) { + TypeConverters.toDate(session.getAttribute(name), format) + } + + static Date date(HttpSession session, String name, Collection formats) { + TypeConverters.toDate(session.getAttribute(name), formats) + } } diff --git a/grails-web-core/src/main/groovy/org/grails/web/servlet/ServletContextExtension.groovy b/grails-web-core/src/main/groovy/org/grails/web/servlet/ServletContextExtension.groovy index 3ea9540cf52..67a2dd5d635 100644 --- a/grails-web-core/src/main/groovy/org/grails/web/servlet/ServletContextExtension.groovy +++ b/grails-web-core/src/main/groovy/org/grails/web/servlet/ServletContextExtension.groovy @@ -18,6 +18,10 @@ */ package org.grails.web.servlet +import groovy.transform.CompileStatic + +import org.apache.grails.core.internal.util.TypeConverters + import jakarta.servlet.ServletContext /** @@ -26,13 +30,108 @@ import jakarta.servlet.ServletContext * @author Jeff Brown * @since 3.0 */ +@CompileStatic class ServletContextExtension { static propertyMissing(ServletContext context, String name, value) { context.setAttribute(name, value) } - + static propertyMissing(ServletContext context, String name) { context.getAttribute(name) } + + static Byte 'byte'(ServletContext context, String name) { + TypeConverters.toByte(context.getAttribute(name)) + } + + static Byte 'byte'(ServletContext context, String name, Integer defaultValue) { + TypeConverters.toByte(context.getAttribute(name), defaultValue) + } + + static Character 'char'(ServletContext context, String name) { + TypeConverters.toCharacter(context.getAttribute(name)) + } + + static Character 'char'(ServletContext context, String name, Character defaultValue) { + TypeConverters.toCharacter(context.getAttribute(name), defaultValue) + } + + static Character 'char'(ServletContext context, String name, Integer defaultValue) { + TypeConverters.toCharacter(context.getAttribute(name), defaultValue) + } + + static Short 'short'(ServletContext context, String name) { + TypeConverters.toShort(context.getAttribute(name)) + } + + static Short 'short'(ServletContext context, String name, Integer defaultValue) { + TypeConverters.toShort(context.getAttribute(name), defaultValue) + } + + static Integer 'int'(ServletContext context, String name) { + TypeConverters.toInteger(context.getAttribute(name)) + } + + static Integer 'int'(ServletContext context, String name, Integer defaultValue) { + TypeConverters.toInteger(context.getAttribute(name), defaultValue) + } + + static Long 'long'(ServletContext context, String name) { + TypeConverters.toLong(context.getAttribute(name)) + } + + static Long 'long'(ServletContext context, String name, Long defaultValue) { + TypeConverters.toLong(context.getAttribute(name), defaultValue) + } + + static Double 'double'(ServletContext context, String name) { + TypeConverters.toDouble(context.getAttribute(name)) + } + + static Double 'double'(ServletContext context, String name, Double defaultValue) { + TypeConverters.toDouble(context.getAttribute(name), defaultValue) + } + + static Float 'float'(ServletContext context, String name) { + TypeConverters.toFloat(context.getAttribute(name)) + } + + static Float 'float'(ServletContext context, String name, Float defaultValue) { + TypeConverters.toFloat(context.getAttribute(name), defaultValue) + } + + static Boolean 'boolean'(ServletContext context, String name) { + TypeConverters.toBoolean(context.getAttribute(name)) + } + + // boolean default is presence-based (attribute set), which cannot be expressed from the value alone + static Boolean 'boolean'(ServletContext context, String name, Boolean defaultValue) { + Object value = context.getAttribute(name) + value != null ? TypeConverters.toBoolean(value) : defaultValue + } + + static String string(ServletContext context, String name) { + TypeConverters.toStringValue(context.getAttribute(name)) + } + + static String string(ServletContext context, String name, String defaultValue) { + TypeConverters.toStringValue(context.getAttribute(name), defaultValue) + } + + static List list(ServletContext context, String name) { + TypeConverters.toList(context.getAttribute(name)) + } + + static Date date(ServletContext context, String name) { + TypeConverters.toDate(context.getAttribute(name)) + } + + static Date date(ServletContext context, String name, String format) { + TypeConverters.toDate(context.getAttribute(name), format) + } + + static Date date(ServletContext context, String name, Collection formats) { + TypeConverters.toDate(context.getAttribute(name), formats) + } } diff --git a/grails-web-core/src/main/resources/META-INF/services/org.codehaus.groovy.runtime.ExtensionModule b/grails-web-core/src/main/resources/META-INF/services/org.codehaus.groovy.runtime.ExtensionModule index 556957478b9..04d863f5fe5 100644 --- a/grails-web-core/src/main/resources/META-INF/services/org.codehaus.groovy.runtime.ExtensionModule +++ b/grails-web-core/src/main/resources/META-INF/services/org.codehaus.groovy.runtime.ExtensionModule @@ -1,3 +1,3 @@ moduleName=grails-web-servlets-module moduleVersion=1.0 -extensionClasses=org.grails.web.servlet.HttpServletRequestExtension,org.grails.web.servlet.HttpServletResponseExtension,org.grails.web.servlet.HttpSessionExtension,org.grails.web.servlet.ServletContextExtension +extensionClasses=org.grails.web.servlet.HttpServletRequestExtension,org.grails.web.servlet.HttpServletResponseExtension,org.grails.web.servlet.HttpSessionExtension,org.grails.web.servlet.ServletContextExtension,org.grails.web.servlet.FlashScopeExtension diff --git a/grails-web-core/src/test/groovy/org/grails/web/servlet/FlashScopeExtensionSpec.groovy b/grails-web-core/src/test/groovy/org/grails/web/servlet/FlashScopeExtensionSpec.groovy new file mode 100644 index 00000000000..c694267097c --- /dev/null +++ b/grails-web-core/src/test/groovy/org/grails/web/servlet/FlashScopeExtensionSpec.groovy @@ -0,0 +1,82 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 + * + * https://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.grails.web.servlet + +import groovy.transform.CompileStatic + +import grails.web.mvc.FlashScope + +import spock.lang.Specification + +class FlashScopeExtensionSpec extends Specification { + + private FlashScope newFlash() { + // false => do not register with the session, so put/get work without a bound request + new GrailsFlashScope(false) + } + + void "Test typed converters read and convert flash attributes"() { + given: + FlashScope flash = newFlash() + flash.put('age', '42') + flash.put('active', 'true') + flash.put('tz', 'America/Los_Angeles') + + expect: + flash.int('age') == 42 + flash.long('age') == 42L + flash.boolean('active') == true + flash.string('tz') == 'America/Los_Angeles' + } + + void "Test typed converters are null-safe and honor defaults"() { + given: + FlashScope flash = newFlash() + + expect: + flash.int('missing') == null + flash.int('missing', 7) == 7 + flash.boolean('missing') == null + flash.boolean('missing', true) == true + flash.string('missing') == null + flash.string('missing', 'fallback') == 'fallback' + flash.list('missing') == [] + } + + void "Test converters resolve under static compilation"() { + given: + FlashScope flash = newFlash() + flash.put('age', '21') + + expect: + StaticCaller.readAge(flash) == 21 + StaticCaller.readTimeZone(flash) == 'America/Los_Angeles' + } + + @CompileStatic + static class StaticCaller { + static Integer readAge(FlashScope flash) { + flash.int('age') + } + + static String readTimeZone(FlashScope flash) { + flash.string('userTimeZoneId', 'America/Los_Angeles') + } + } +} diff --git a/grails-web-core/src/test/groovy/org/grails/web/servlet/HttpServletRequestExtensionSpec.groovy b/grails-web-core/src/test/groovy/org/grails/web/servlet/HttpServletRequestExtensionSpec.groovy new file mode 100644 index 00000000000..ac8d922997e --- /dev/null +++ b/grails-web-core/src/test/groovy/org/grails/web/servlet/HttpServletRequestExtensionSpec.groovy @@ -0,0 +1,76 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 + * + * https://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.grails.web.servlet + +import groovy.transform.CompileStatic + +import jakarta.servlet.http.HttpServletRequest + +import org.springframework.mock.web.MockHttpServletRequest + +import spock.lang.Specification + +class HttpServletRequestExtensionSpec extends Specification { + + void "Test typed converters read and convert request attributes"() { + given: + HttpServletRequest request = new MockHttpServletRequest() + request.setAttribute('page', '3') + request.setAttribute('total', 99L) + request.setAttribute('name', 'Bob') + + expect: + request.int('page') == 3 + request.long('total') == 99L + request.string('name') == 'Bob' + } + + void "Test typed converters are null-safe and honor defaults"() { + given: + HttpServletRequest request = new MockHttpServletRequest() + + expect: + request.int('missing') == null + request.int('missing', 1) == 1 + request.boolean('missing', true) == true + request.string('missing', 'fallback') == 'fallback' + request.list('missing') == [] + } + + void "Test converters resolve under static compilation"() { + given: + HttpServletRequest request = new MockHttpServletRequest() + request.setAttribute('page', '5') + + expect: + StaticCaller.readPage(request) == 5 + StaticCaller.readName(request) == 'anonymous' + } + + @CompileStatic + static class StaticCaller { + static Integer readPage(HttpServletRequest request) { + request.int('page', 1) + } + + static String readName(HttpServletRequest request) { + request.string('user', 'anonymous') + } + } +} diff --git a/grails-web-core/src/test/groovy/org/grails/web/servlet/HttpSessionExtensionSpec.groovy b/grails-web-core/src/test/groovy/org/grails/web/servlet/HttpSessionExtensionSpec.groovy new file mode 100644 index 00000000000..06de529e0c0 --- /dev/null +++ b/grails-web-core/src/test/groovy/org/grails/web/servlet/HttpSessionExtensionSpec.groovy @@ -0,0 +1,104 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 + * + * https://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.grails.web.servlet + +import groovy.transform.CompileStatic + +import jakarta.servlet.http.HttpSession + +import org.springframework.mock.web.MockHttpSession + +import spock.lang.Specification + +class HttpSessionExtensionSpec extends Specification { + + void "Test typed converters read and convert session attributes"() { + given: + HttpSession session = new MockHttpSession() + session.setAttribute('age', '42') + session.setAttribute('rate', 3.5d) + session.setAttribute('active', 'true') + session.setAttribute('tz', 'America/Los_Angeles') + + expect: + session.int('age') == 42 + session.long('age') == 42L + session.double('rate') == 3.5d + session.boolean('active') == true + session.string('tz') == 'America/Los_Angeles' + } + + void "Test typed converters are null-safe and honor defaults"() { + given: + HttpSession session = new MockHttpSession() + + expect: + session.int('missing') == null + session.int('missing', 7) == 7 + session.boolean('missing') == null + session.boolean('missing', true) == true + session.string('missing') == null + session.string('missing', 'fallback') == 'fallback' + session.list('missing') == [] + } + + void "Test the date converter parses session attributes"() { + given: + HttpSession session = new MockHttpSession() + session.setAttribute('createdOn', '2026-06-04 09:30:00.0') + + expect: + session.date('createdOn') != null + + and: "an unparseable value yields null" + session.date('missing') == null + } + + void "Test the list converter always returns a list"() { + given: + HttpSession session = new MockHttpSession() + session.setAttribute('single', 'one') + session.setAttribute('many', ['a', 'b'] as String[]) + + expect: + session.list('single') == ['one'] + session.list('many') == ['a', 'b'] + } + + void "Test converters resolve under static compilation"() { + given: + HttpSession session = new MockHttpSession() + session.setAttribute('age', '21') + + expect: "the statically compiled helper resolves session.int / session.string" + StaticCaller.readAge(session) == 21 + StaticCaller.readTimeZone(session) == 'America/Los_Angeles' + } + + @CompileStatic + static class StaticCaller { + static Integer readAge(HttpSession session) { + session.int('age') + } + + static String readTimeZone(HttpSession session) { + session.string('userTimeZoneId', 'America/Los_Angeles') + } + } +} diff --git a/grails-web-core/src/test/groovy/org/grails/web/servlet/ServletContextExtensionSpec.groovy b/grails-web-core/src/test/groovy/org/grails/web/servlet/ServletContextExtensionSpec.groovy new file mode 100644 index 00000000000..496b955d559 --- /dev/null +++ b/grails-web-core/src/test/groovy/org/grails/web/servlet/ServletContextExtensionSpec.groovy @@ -0,0 +1,72 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 + * + * https://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.grails.web.servlet + +import groovy.transform.CompileStatic + +import jakarta.servlet.ServletContext + +import org.springframework.mock.web.MockServletContext + +import spock.lang.Specification + +class ServletContextExtensionSpec extends Specification { + + void "Test typed converters read and convert servletContext attributes"() { + given: + ServletContext context = new MockServletContext() + context.setAttribute('maxUploads', '10') + context.setAttribute('appName', 'demo') + + expect: + context.int('maxUploads') == 10 + context.string('appName') == 'demo' + } + + void "Test typed converters are null-safe and honor defaults"() { + given: + ServletContext context = new MockServletContext() + + expect: + context.int('missing') == null + context.int('missing', 4) == 4 + context.string('missing', 'fallback') == 'fallback' + } + + void "Test converters resolve under static compilation"() { + given: + ServletContext context = new MockServletContext() + context.setAttribute('maxUploads', '8') + + expect: + StaticCaller.readMaxUploads(context) == 8 + StaticCaller.readAppName(context) == 'grails' + } + + @CompileStatic + static class StaticCaller { + static Integer readMaxUploads(ServletContext context) { + context.int('maxUploads', 1) + } + + static String readAppName(ServletContext context) { + context.string('appName', 'grails') + } + } +}