Skip to content

Commit 0892f07

Browse files
committed
Add MapAdapter
1 parent 13578bd commit 0892f07

10 files changed

Lines changed: 284 additions & 14 deletions

File tree

core/src/main/java/net/roxymc/jserialize/adapter/TypeAdapters.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package net.roxymc.jserialize.adapter;
22

3+
import net.roxymc.jserialize.adapter.map.MapAdapter;
34
import net.roxymc.jserialize.adapter.object.ObjectAdapter;
45
import net.roxymc.jserialize.type.TypeToken;
56
import org.jetbrains.annotations.ApiStatus;
@@ -8,6 +9,7 @@
89
@ApiStatus.NonExtendable
910
public interface TypeAdapters {
1011
TypeAdapters DEFAULT = builder()
12+
.add(MapAdapter.factory())
1113
.add(ObjectAdapter.factory())
1214
.build();
1315

@@ -49,6 +51,7 @@ default <T> KeyAdapter<T> getKeyOrThrow(TypeToken<T> type) {
4951
if (adapter != null) {
5052
return adapter;
5153
}
54+
5255
throw new IllegalStateException("Could not find key adapter for " + type.getAnnotatedType());
5356
}
5457

core/src/main/java/net/roxymc/jserialize/adapter/TypeAdaptersImpl.java

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
import java.util.concurrent.ConcurrentHashMap;
1010
import java.util.function.BiFunction;
1111

12+
import static net.roxymc.jserialize.util.ObjectUtils.nonNull;
13+
1214
final class TypeAdaptersImpl implements TypeAdapters {
1315
private final Registry<TypeAdapter.Factory, TypeAdapter<?>> typeAdapters;
1416
private final Registry<KeyAdapter.Factory, KeyAdapter<?>> keyAdapters;
@@ -22,14 +24,14 @@ private TypeAdaptersImpl(BuilderImpl builder) {
2224
@Override
2325
public @Nullable <T> TypeAdapter<T> get(TypeToken<T> type) {
2426
@SuppressWarnings("unchecked")
25-
TypeAdapter<T> adapter = (TypeAdapter<T>) typeAdapters.get(type).orElse(null);
27+
TypeAdapter<T> adapter = (TypeAdapter<T>) typeAdapters.get(nonNull(type, "type")).orElse(null);
2628
return adapter;
2729
}
2830

2931
@Override
3032
public @Nullable <T> KeyAdapter<T> getKey(TypeToken<T> type) {
3133
@SuppressWarnings("unchecked")
32-
KeyAdapter<T> adapter = (KeyAdapter<T>) keyAdapters.get(type).orElse(null);
34+
KeyAdapter<T> adapter = (KeyAdapter<T>) keyAdapters.get(nonNull(type, "type")).orElse(null);
3335
return adapter;
3436
}
3537

@@ -66,13 +68,13 @@ static final class BuilderImpl implements Builder {
6668

6769
@Override
6870
public Builder add(TypeAdapter.Factory factory) {
69-
typeAdapters.add(factory);
71+
typeAdapters.add(nonNull(factory, "factory"));
7072
return this;
7173
}
7274

7375
@Override
7476
public Builder addKey(KeyAdapter.Factory factory) {
75-
keyAdapters.add(factory);
77+
keyAdapters.add(nonNull(factory, "factory"));
7678
return this;
7779
}
7880

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package net.roxymc.jserialize.adapter.map;
2+
3+
import net.roxymc.jserialize.type.TypeToken;
4+
import org.jspecify.annotations.Nullable;
5+
6+
import java.util.LinkedHashMap;
7+
import java.util.Map;
8+
import java.util.TreeMap;
9+
import java.util.concurrent.ConcurrentHashMap;
10+
import java.util.concurrent.ConcurrentSkipListMap;
11+
12+
public final class DefaultMapProvider implements MapProvider {
13+
public static final DefaultMapProvider INSTANCE = new DefaultMapProvider();
14+
15+
private DefaultMapProvider() {
16+
}
17+
18+
@Override
19+
public @Nullable <K, V> MapFactory<K, V> resolve(TypeToken<? extends Map<K, V>> type) {
20+
Class<?> rawType = type.getRawType();
21+
22+
if (rawType.isAssignableFrom(LinkedHashMap.class)) {
23+
return LinkedHashMap::new;
24+
}
25+
26+
if (rawType.isAssignableFrom(TreeMap.class)) {
27+
return TreeMap::new;
28+
}
29+
30+
if (rawType.isAssignableFrom(ConcurrentHashMap.class)) {
31+
return ConcurrentHashMap::new;
32+
}
33+
34+
if (rawType.isAssignableFrom(ConcurrentSkipListMap.class)) {
35+
return ConcurrentSkipListMap::new;
36+
}
37+
38+
return null;
39+
}
40+
}
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
package net.roxymc.jserialize.adapter.map;
2+
3+
import net.roxymc.jserialize.Reader;
4+
import net.roxymc.jserialize.Writer;
5+
import net.roxymc.jserialize.adapter.KeyAdapter;
6+
import net.roxymc.jserialize.adapter.ReadContext;
7+
import net.roxymc.jserialize.adapter.TypeAdapter;
8+
import net.roxymc.jserialize.adapter.WriteContext;
9+
import net.roxymc.jserialize.token.TokenType;
10+
import net.roxymc.jserialize.type.TypeToken;
11+
import org.jetbrains.annotations.UnknownNullability;
12+
import org.jspecify.annotations.Nullable;
13+
14+
import java.io.IOException;
15+
import java.lang.reflect.Type;
16+
import java.util.Map;
17+
import java.util.concurrent.ConcurrentHashMap;
18+
19+
import static net.roxymc.jserialize.util.ObjectUtils.nonNull;
20+
21+
public final class MapAdapter implements TypeAdapter.Mutable<Map<?, ?>> {
22+
private static final TypeAdapter.Factory FACTORY = factory(DefaultMapProvider.INSTANCE);
23+
24+
private final Map<Type, MapType<?, ?>> cache = new ConcurrentHashMap<>();
25+
private final MapProvider[] providers;
26+
27+
public MapAdapter(MapProvider... providers) {
28+
this.providers = nonNull(providers, "providers").clone();
29+
}
30+
31+
public static TypeAdapter.Factory factory() {
32+
return FACTORY;
33+
}
34+
35+
public static TypeAdapter.Factory factory(MapProvider... providers) {
36+
return TypeAdapter.Factory.polymorphic(Map.class, new MapAdapter(providers));
37+
}
38+
39+
private <K, V> MapType<K, V> resolveMapType(TypeToken<? extends Map<?, ?>> type) {
40+
@SuppressWarnings("unchecked")
41+
MapType<K, V> mapType = (MapType<K, V>) cache.computeIfAbsent(type.getType(), $ -> new MapType<>(type));
42+
return mapType;
43+
}
44+
45+
@Override
46+
public @Nullable Map<?, ?> mutate(
47+
Reader reader, TypeToken<? extends Map<?, ?>> type, @Nullable Map<?, ?> value, ReadContext ctx
48+
) throws IOException {
49+
return mutate0(reader, type, value, ctx);
50+
}
51+
52+
private <K, V> @Nullable Map<K, V> mutate0(
53+
Reader reader, TypeToken<? extends Map<?, ?>> type, @Nullable Map<@UnknownNullability K, @UnknownNullability V> map, ReadContext ctx
54+
) throws IOException {
55+
if (reader.peek() == TokenType.NULL) {
56+
reader.readNull();
57+
return map;
58+
}
59+
60+
MapType<K, V> mapType = resolveMapType(type);
61+
62+
if (map == null) {
63+
map = mapType.createMap(providers);
64+
}
65+
66+
KeyAdapter<K> keyAdapter = mapType.keyAdapter(ctx.typeAdapters());
67+
TypeAdapter<V> valueAdapter = mapType.valueAdapter(ctx.typeAdapters());
68+
69+
reader.readObjectStart();
70+
71+
while (reader.peek() == TokenType.NAME) {
72+
String name = reader.readName();
73+
74+
K key = keyAdapter.decode(name);
75+
V value = valueAdapter.read(reader, mapType.valueType, ctx.withKey(name));
76+
77+
map.put(key, value);
78+
}
79+
80+
reader.readObjectEnd();
81+
82+
return map;
83+
}
84+
85+
@Override
86+
public void write(
87+
Writer writer, TypeToken<? extends Map<?, ?>> type, @Nullable Map<?, ?> value, WriteContext ctx
88+
) throws IOException {
89+
write0(writer, type, value, ctx);
90+
}
91+
92+
private <K, V> void write0(
93+
Writer writer, TypeToken<? extends Map<?, ?>> type, @Nullable Map<K, V> map, WriteContext ctx
94+
) throws IOException {
95+
if (map == null) {
96+
writer.writeNull();
97+
return;
98+
}
99+
100+
MapType<K, V> mapType = resolveMapType(type);
101+
102+
KeyAdapter<K> keyAdapter = mapType.keyAdapter(ctx.typeAdapters());
103+
TypeAdapter<V> valueAdapter = mapType.valueAdapter(ctx.typeAdapters());
104+
105+
writer.writeObjectStart();
106+
107+
for (Map.Entry<K, V> entry : map.entrySet()) {
108+
String name = keyAdapter.encode(entry.getKey());
109+
writer.writeName(name);
110+
111+
valueAdapter.write(writer, mapType.valueType, entry.getValue(), ctx);
112+
}
113+
114+
writer.writeObjectEnd();
115+
}
116+
117+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package net.roxymc.jserialize.adapter.map;
2+
3+
import java.util.Map;
4+
5+
@FunctionalInterface
6+
public interface MapFactory<K, V> {
7+
Map<K, V> create();
8+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package net.roxymc.jserialize.adapter.map;
2+
3+
import net.roxymc.jserialize.type.TypeToken;
4+
import org.jspecify.annotations.Nullable;
5+
6+
import java.util.Map;
7+
8+
public interface MapProvider {
9+
<K, V> @Nullable MapFactory<K, V> resolve(TypeToken<? extends Map<K, V>> type);
10+
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
package net.roxymc.jserialize.adapter.map;
2+
3+
import io.leangen.geantyref.GenericTypeReflector;
4+
import net.roxymc.jserialize.adapter.KeyAdapter;
5+
import net.roxymc.jserialize.adapter.TypeAdapter;
6+
import net.roxymc.jserialize.adapter.TypeAdapters;
7+
import net.roxymc.jserialize.type.TypeToken;
8+
import net.roxymc.jserialize.util.VarHandles;
9+
import org.jspecify.annotations.Nullable;
10+
11+
import java.lang.invoke.VarHandle;
12+
import java.lang.reflect.AnnotatedParameterizedType;
13+
import java.lang.reflect.AnnotatedType;
14+
import java.util.Map;
15+
16+
import static io.leangen.geantyref.GenericTypeReflector.capture;
17+
import static io.leangen.geantyref.GenericTypeReflector.resolveType;
18+
19+
final class MapType<K, V> {
20+
private static final AnnotatedType MAP_TYPE = GenericTypeReflector.annotate(Map.class);
21+
private static final VarHandle MAP_FACTORY_HANDLE = VarHandles.find(MapType.class, "mapFactory", MapFactory.class);
22+
23+
final TypeToken<? extends Map<K, V>> mapType;
24+
final TypeToken<K> keyType;
25+
final TypeToken<V> valueType;
26+
private @Nullable MapFactory<K, V> mapFactory;
27+
28+
MapType(TypeToken<? extends Map<?, ?>> mapType) {
29+
AnnotatedType type = resolveType(MAP_TYPE, capture(mapType.getAnnotatedType()));
30+
if (!(type instanceof AnnotatedParameterizedType)) {
31+
throw new IllegalStateException(mapType.getRawType() + " must be parameterized");
32+
}
33+
34+
AnnotatedParameterizedType ptype = (AnnotatedParameterizedType) type;
35+
36+
this.mapType = TypeToken.of(ptype);
37+
this.keyType = TypeToken.of(ptype.getAnnotatedActualTypeArguments()[0]);
38+
this.valueType = TypeToken.of(ptype.getAnnotatedActualTypeArguments()[1]);
39+
}
40+
41+
KeyAdapter<K> keyAdapter(TypeAdapters adapters) {
42+
return adapters.getKeyOrThrow(keyType);
43+
}
44+
45+
TypeAdapter<V> valueAdapter(TypeAdapters adapters) {
46+
return adapters.getOrThrow(valueType);
47+
}
48+
49+
@SuppressWarnings("unchecked")
50+
Map<K, V> createMap(MapProvider[] providers) {
51+
MapFactory<K, V> value = (MapFactory<K, V>) MAP_FACTORY_HANDLE.getAcquire(this);
52+
53+
if (value == null) {
54+
MapFactory<K, V> resolved = null;
55+
56+
for (MapProvider provider : providers) {
57+
MapFactory<K, V> factory = provider.resolve(mapType);
58+
59+
if (factory != null) {
60+
resolved = factory;
61+
break;
62+
}
63+
}
64+
65+
if (resolved == null) {
66+
resolved = () -> {
67+
throw new IllegalStateException("Failed to find map factory for " + mapType.getType());
68+
};
69+
}
70+
71+
MapFactory<K, V> witness = (MapFactory<K, V>) MAP_FACTORY_HANDLE.compareAndExchangeRelease(this, null, resolved);
72+
value = witness != null ? witness : resolved;
73+
}
74+
75+
return value.create();
76+
}
77+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
@NullMarked
2+
package net.roxymc.jserialize.adapter.map;
3+
4+
import org.jspecify.annotations.NullMarked;

core/src/main/java/net/roxymc/jserialize/type/TypeToken.java

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,18 @@
11
package net.roxymc.jserialize.type;
22

33
import io.leangen.geantyref.GenericTypeReflector;
4+
import net.roxymc.jserialize.util.VarHandles;
45
import org.jetbrains.annotations.UnknownNullability;
56
import org.jspecify.annotations.Nullable;
67

7-
import java.lang.invoke.MethodHandles;
88
import java.lang.invoke.VarHandle;
99
import java.lang.reflect.AnnotatedParameterizedType;
1010
import java.lang.reflect.AnnotatedType;
1111
import java.lang.reflect.ParameterizedType;
1212
import java.lang.reflect.Type;
1313

1414
public class TypeToken<T extends @UnknownNullability Object> {
15-
private static final VarHandle RAW_TYPE_HANDLE;
16-
17-
static {
18-
try {
19-
RAW_TYPE_HANDLE = MethodHandles.lookup().findVarHandle(TypeToken.class, "rawType", Class.class);
20-
} catch (NoSuchFieldException | IllegalAccessException ex) {
21-
throw new ExceptionInInitializerError(ex);
22-
}
23-
}
15+
private static final VarHandle RAW_TYPE_HANDLE = VarHandles.find(TypeToken.class, "rawType", Class.class);
2416

2517
private final AnnotatedType annotatedType;
2618
private @Nullable Class<? super T> rawType;
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package net.roxymc.jserialize.util;
2+
3+
import java.lang.invoke.MethodHandles;
4+
import java.lang.invoke.VarHandle;
5+
6+
public final class VarHandles {
7+
private VarHandles() {
8+
}
9+
10+
public static VarHandle find(Class<?> clazz, String name, Class<?> type) {
11+
try {
12+
return MethodHandles.lookup().findVarHandle(clazz, name, type);
13+
} catch (NoSuchFieldException | IllegalAccessException ex) {
14+
throw SneakyThrow.sneakyThrow(ex);
15+
}
16+
}
17+
}

0 commit comments

Comments
 (0)