Skip to content

Commit 6571c9c

Browse files
authored
Add support for wildcards on the CustomClassMapper. (#792)
* Add support for wildcards on the CustomClassMapper. CustomClassMapper can now handle GenericTypeIndicators with wildcards, instead of strictly requiring a specific type.
1 parent 4fa77b2 commit 6571c9c

File tree

3 files changed

+68
-2
lines changed

3 files changed

+68
-2
lines changed

firebase-database/CHANGELOG.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
# 19.2.0
2+
- [changed] Added support for type wildcards in GenericTypeIndicator, expanding
3+
our custom class serialization to include classes with wildcard generics
4+
(#792).
5+
16
# 19.1.1
27
- [fixed] Fixed a crash that occurred when we attempted to start a network
38
connection during app shutdown (#672).
@@ -26,6 +31,6 @@
2631
- [internal] Removed ``@PublicApi` annotations as they are no longer enforced
2732
and have no semantic meaning.
2833

29-
# 16.0.6
34+
# 16.0.6
3035
- [fixed] Fixed an issue that could cause a NullPointerException during the
3136
initial handshake with the Firebase backend (#119).

firebase-database/src/main/java/com/google/firebase/database/core/utilities/encoding/CustomClassMapper.java

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,24 @@ private static <T> T deserializeToType(Object o, Type type) {
177177
} else if (type instanceof Class) {
178178
return deserializeToClass(o, (Class<T>) type);
179179
} else if (type instanceof WildcardType) {
180-
throw new DatabaseException("Generic wildcard types are not supported");
180+
Type[] lowerBounds = ((WildcardType) type).getLowerBounds();
181+
if (lowerBounds.length > 0) {
182+
throw new DatabaseException("Generic lower-bounded wildcard types are not supported");
183+
}
184+
185+
// Upper bounded wildcards are of the form <? extends Foo>. Multiple upper bounds are allowed
186+
// but if any of the bounds are of class type, that bound must come first in this array. Note
187+
// that this array always has at least one element, since the unbounded wildcard <?> always
188+
// has at least an upper bound of Object.
189+
Type[] upperBounds = ((WildcardType) type).getUpperBounds();
190+
hardAssert(upperBounds.length > 0, "Wildcard type " + type + " is not upper bounded.");
191+
return deserializeToType(o, upperBounds[0]);
192+
} else if (type instanceof TypeVariable) {
193+
// As above, TypeVariables always have at least one upper bound of Object.
194+
Type[] upperBounds = ((TypeVariable<?>) type).getBounds();
195+
hardAssert(upperBounds.length > 0, "Wildcard type " + type + " is not upper bounded.");
196+
return deserializeToType(o, upperBounds[0]);
197+
181198
} else if (type instanceof GenericArrayType) {
182199
throw new DatabaseException(
183200
"Generic Arrays are not supported, please use Lists " + "instead");

firebase-database/src/test/java/com/google/firebase/database/MapperTest.java

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121

2222
import androidx.annotation.Keep;
2323
import com.google.firebase.database.core.utilities.encoding.CustomClassMapper;
24+
import java.io.Serializable;
2425
import java.util.ArrayList;
2526
import java.util.Arrays;
2627
import java.util.Collection;
@@ -628,6 +629,22 @@ public String getValue() {
628629
}
629630
}
630631

632+
private static class MultiBoundedMapBean<T extends String & Serializable> {
633+
private Map<String, T> values;
634+
635+
public Map<String, T> getValues() {
636+
return values;
637+
}
638+
}
639+
640+
private static class MultiBoundedMapHolderBean {
641+
private MultiBoundedMapBean<String> map;
642+
643+
public MultiBoundedMapBean<String> getMap() {
644+
return map;
645+
}
646+
}
647+
631648
private static class StaticFieldBean {
632649
public static String value1 = "static-value";
633650
public String value2;
@@ -1763,6 +1780,33 @@ public void subclassingGenericTypeIndicatorIsAllowed() {
17631780
assertEquals("foo", bean.value);
17641781
}
17651782

1783+
@Test
1784+
public void usingWildcardInGenericTypeIndicatorIsAllowed() {
1785+
Map<String, String> fooMap = Collections.singletonMap("foo", "bar");
1786+
assertEquals(
1787+
fooMap,
1788+
CustomClassMapper.convertToCustomClass(
1789+
fooMap, new GenericTypeIndicator<Map<String, ? extends String>>() {}));
1790+
}
1791+
1792+
@Test(expected = DatabaseException.class)
1793+
public void usingLowerBoundWildcardsIsForbidden() {
1794+
Map<String, String> fooMap = Collections.singletonMap("foo", "bar");
1795+
CustomClassMapper.convertToCustomClass(
1796+
fooMap, new GenericTypeIndicator<Map<String, ? super String>>() {});
1797+
}
1798+
1799+
@Test
1800+
public void multiBoundedWildcardsUsesTheFirst() {
1801+
Map<String, Object> source =
1802+
Collections.singletonMap(
1803+
"map", Collections.singletonMap("values", Collections.singletonMap("foo", "bar")));
1804+
MultiBoundedMapHolderBean bean =
1805+
CustomClassMapper.convertToCustomClass(source, MultiBoundedMapHolderBean.class);
1806+
Map<String, String> expected = Collections.singletonMap("foo", "bar");
1807+
assertEquals(expected, bean.map.values);
1808+
}
1809+
17661810
@Test(expected = DatabaseException.class)
17671811
public void unknownTypeParametersNotSupported() {
17681812
deserialize("{'value': 'foo'}", new GenericTypeIndicatorSubclass<GenericBean<?>>() {});

0 commit comments

Comments
 (0)