Skip to content

Commit 7c5b48d

Browse files
authored
Fix declarative request parameter codegen (helidon-io#11745)
* Fix declarative request parameter codegen * Fix declarative parameter codegen CI validation * Fix declarative mapper qualifier fallback * Fix defaulted list query codegen * Protect mapper qualifier cache keys * Document declarative parameter helpers
1 parent fbbb3cc commit 7c5b48d

21 files changed

Lines changed: 1836 additions & 178 deletions

File tree

common/mapper/src/main/java/io/helidon/common/mapper/Mappers.java

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
import java.util.Optional;
2020
import java.util.function.Consumer;
21+
import java.util.function.Function;
2122

2223
import io.helidon.builder.api.RuntimeType;
2324
import io.helidon.common.Api;
@@ -125,4 +126,30 @@ <SOURCE, TARGET> TARGET map(SOURCE source, Class<SOURCE> sourceType, Class<TARGE
125126
<SOURCE, TARGET> Optional<Mapper<SOURCE, TARGET>> mapper(GenericType<SOURCE> sourceType,
126127
GenericType<TARGET> targetType,
127128
String... qualifiers);
129+
130+
/**
131+
* Map from source to target, also mapping exception to a custom exception type.
132+
*
133+
* @param source object to map
134+
* @param sourceType type of the source object (to locate the mapper)
135+
* @param targetType type of the target object (to locate the mapper)
136+
* @param exceptionMapper a function to map from {@link io.helidon.common.mapper.MapperException} to your type
137+
* @param qualifiers qualifiers of the usage (such as {@code http-headers, http}, most specific one first)
138+
* @param <SOURCE> type of the source
139+
* @param <TARGET> type of the target
140+
* @param <T> type of the exception produced by {@code exceptionMapper}
141+
* @return result of the mapping
142+
* @throws T if mapping fails and {@code exceptionMapper} produces an exception
143+
*/
144+
default <SOURCE, TARGET, T extends Throwable> TARGET map(SOURCE source,
145+
GenericType<SOURCE> sourceType,
146+
GenericType<TARGET> targetType,
147+
Function<MapperException, T> exceptionMapper,
148+
String... qualifiers) throws T {
149+
try {
150+
return map(source, sourceType, targetType, qualifiers);
151+
} catch (MapperException e) {
152+
throw exceptionMapper.apply(e);
153+
}
154+
}
128155
}

common/mapper/src/main/java/io/helidon/common/mapper/MappersImpl.java

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import java.util.Arrays;
2121
import java.util.List;
2222
import java.util.Map;
23+
import java.util.Objects;
2324
import java.util.Optional;
2425
import java.util.concurrent.ConcurrentHashMap;
2526
import java.util.function.BiFunction;
@@ -123,7 +124,9 @@ private <SOURCE, TARGET> Mapper<SOURCE, TARGET> findMapper(Class<SOURCE> sourceT
123124
Class<TARGET> targetType,
124125
boolean fromTypes,
125126
String... qualifiers) {
126-
Mapper<?, ?> mapper = classCache.computeIfAbsent(new ClassCacheKey(sourceType, targetType, List.of(qualifiers)), key -> {
127+
Mapper<?, ?> mapper = classCache.computeIfAbsent(new ClassCacheKey(sourceType,
128+
targetType,
129+
new QualifiersWrapper(qualifiers)), key -> {
127130
// first attempt to find by classes
128131
return fromProviders(sourceType, targetType, qualifiers)
129132
.orElseGet(() -> {
@@ -143,7 +146,9 @@ private <SOURCE, TARGET> Mapper<SOURCE, TARGET> findMapper(GenericType<SOURCE> s
143146
GenericType<TARGET> targetType,
144147
boolean fromClasses,
145148
String... qualifiers) {
146-
Mapper<?, ?> mapper = typeCache.computeIfAbsent(new GenericCacheKey(sourceType, targetType, List.of(qualifiers)), key -> {
149+
Mapper<?, ?> mapper = typeCache.computeIfAbsent(new GenericCacheKey(sourceType,
150+
targetType,
151+
new QualifiersWrapper(qualifiers)), key -> {
147152
// first attempt to find by types
148153
return fromProviders(sourceType, targetType, qualifiers)
149154
.orElseGet(() -> {
@@ -223,10 +228,33 @@ private <SOURCE, TARGET> Mapper<SOURCE, TARGET> findMapper(GenericType<SOURCE> s
223228
qualifiers);
224229
}
225230

226-
private record GenericCacheKey(GenericType<?> sourceType, GenericType<?> targetType, List<String> qualifiers) {
231+
// simple array wrapper that has equals and hashcode - faster than creating a List
232+
private static class QualifiersWrapper {
233+
private final String[] qualifiers;
234+
235+
private QualifiersWrapper(String[] qualifiers) {
236+
// defensive copy, so if somebody modifies the original array, we do not get broken
237+
this.qualifiers = Arrays.copyOf(qualifiers, qualifiers.length);
238+
}
239+
240+
@Override
241+
public boolean equals(Object o) {
242+
if (!(o instanceof QualifiersWrapper that)) {
243+
return false;
244+
}
245+
return Objects.deepEquals(qualifiers, that.qualifiers);
246+
}
247+
248+
@Override
249+
public int hashCode() {
250+
return Arrays.hashCode(qualifiers);
251+
}
252+
}
253+
254+
private record GenericCacheKey(GenericType<?> sourceType, GenericType<?> targetType, QualifiersWrapper qualifiers) {
227255
}
228256

229-
private record ClassCacheKey(Class<?> sourceType, Class<?> targetType, List<String> qualifiers) {
257+
private record ClassCacheKey(Class<?> sourceType, Class<?> targetType, QualifiersWrapper qualifiers) {
230258
}
231259

232260
@SuppressWarnings("rawtypes")

common/mapper/src/test/java/io/helidon/common/mapper/MappersTest.java

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -328,6 +328,38 @@ void testCacheWorks() {
328328
assertThat(mm.typeCacheSize(), is(1));
329329
}
330330

331+
@Test
332+
void testQualifierArrayMutationDoesNotCorruptCache() {
333+
CountingQualifiedProvider provider = new CountingQualifiedProvider();
334+
MappersImpl mm = new MappersImpl(Mappers.builder()
335+
.mapperProvidersDiscoverServices(false)
336+
.useBuiltInMappers(false)
337+
.addMapperProvider(provider)
338+
.buildPrototype());
339+
340+
String[] classQualifiers = {"http", "query"};
341+
assertThat(mm.map("value", String.class, String.class, classQualifiers), is("class_value"));
342+
assertThat(mm.classCacheSize(), is(1));
343+
int classCalls = provider.classCalls();
344+
345+
classQualifiers[1] = "header";
346+
347+
assertThat(mm.map("value", String.class, String.class, "http", "query"), is("class_value"));
348+
assertThat(mm.classCacheSize(), is(1));
349+
assertThat(provider.classCalls(), is(classCalls));
350+
351+
String[] genericQualifiers = {"http", "query"};
352+
assertThat(mm.map("value", GenericType.STRING, GenericType.STRING, genericQualifiers), is("type_value"));
353+
assertThat(mm.typeCacheSize(), is(1));
354+
int typeCalls = provider.typeCalls();
355+
356+
genericQualifiers[1] = "header";
357+
358+
assertThat(mm.map("value", GenericType.STRING, GenericType.STRING, "http", "query"), is("type_value"));
359+
assertThat(mm.typeCacheSize(), is(1));
360+
assertThat(provider.typeCalls(), is(typeCalls));
361+
}
362+
331363
private static class TestProvider implements MapperProvider {
332364
@Override
333365
public ProviderResponse mapper(Class<?> sourceClass, Class<?> targetClass, String qualifier) {
@@ -340,4 +372,35 @@ public ProviderResponse mapper(GenericType<?> sourceType, GenericType<?> targetT
340372
}
341373
}
342374

375+
private static class CountingQualifiedProvider implements MapperProvider {
376+
private int classCalls;
377+
private int typeCalls;
378+
379+
@Override
380+
public ProviderResponse mapper(Class<?> sourceClass, Class<?> targetClass, String qualifier) {
381+
classCalls++;
382+
if (String.class.equals(sourceClass) && String.class.equals(targetClass) && qualifier.equals("http/query")) {
383+
return new ProviderResponse(Support.SUPPORTED, req -> "class_" + req);
384+
}
385+
return ProviderResponse.unsupported();
386+
}
387+
388+
@Override
389+
public ProviderResponse mapper(GenericType<?> sourceType, GenericType<?> targetType, String qualifier) {
390+
typeCalls++;
391+
if (GenericType.STRING.equals(sourceType) && GenericType.STRING.equals(targetType) && qualifier.equals("http/query")) {
392+
return new ProviderResponse(Support.SUPPORTED, req -> "type_" + req);
393+
}
394+
return ProviderResponse.unsupported();
395+
}
396+
397+
int classCalls() {
398+
return classCalls;
399+
}
400+
401+
int typeCalls() {
402+
return typeCalls;
403+
}
404+
}
405+
343406
}

0 commit comments

Comments
 (0)