• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

AuthMe / ConfigMe / 17190022456

24 Aug 2025 01:41PM UTC coverage: 99.328%. First build
17190022456

Pull #398

github

ljacqu
Small documentation additions
Pull Request #398: #135 Support more ways to create beans (e.g. Java records)

557 of 574 branches covered (97.04%)

220 of 221 new or added lines in 10 files covered. (99.55%)

1627 of 1638 relevant lines covered (99.33%)

4.59 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

99.24
/src/main/java/ch/jalu/configme/beanmapper/MapperImpl.java
1
package ch.jalu.configme.beanmapper;
2

3
import ch.jalu.configme.beanmapper.context.ExportContext;
4
import ch.jalu.configme.beanmapper.context.ExportContextImpl;
5
import ch.jalu.configme.beanmapper.context.MappingContext;
6
import ch.jalu.configme.beanmapper.context.MappingContextImpl;
7
import ch.jalu.configme.beanmapper.definition.BeanDefinition;
8
import ch.jalu.configme.beanmapper.definition.BeanDefinitionService;
9
import ch.jalu.configme.beanmapper.definition.BeanDefinitionServiceImpl;
10
import ch.jalu.configme.beanmapper.definition.properties.BeanPropertyComments;
11
import ch.jalu.configme.beanmapper.definition.properties.BeanPropertyDefinition;
12
import ch.jalu.configme.beanmapper.leafvaluehandler.LeafValueHandler;
13
import ch.jalu.configme.beanmapper.leafvaluehandler.LeafValueHandlerImpl;
14
import ch.jalu.configme.properties.convertresult.ConvertErrorRecorder;
15
import ch.jalu.configme.properties.convertresult.ValueWithComments;
16
import ch.jalu.typeresolver.TypeInfo;
17
import org.jetbrains.annotations.NotNull;
18
import org.jetbrains.annotations.Nullable;
19

20
import java.util.ArrayList;
21
import java.util.Collection;
22
import java.util.Collections;
23
import java.util.LinkedHashMap;
24
import java.util.LinkedHashSet;
25
import java.util.List;
26
import java.util.Map;
27
import java.util.Optional;
28
import java.util.TreeMap;
29
import java.util.stream.Collectors;
30

31
import static ch.jalu.configme.internal.PathUtils.OPTIONAL_SPECIFIER;
32
import static ch.jalu.configme.internal.PathUtils.pathSpecifierForIndex;
33
import static ch.jalu.configme.internal.PathUtils.pathSpecifierForMapKey;
34

35
/**
36
 * Default implementation of {@link Mapper}.
37
 * <p>
38
 * Maps a section of a property resource to the provided Java class (called a "bean" type). The mapping is based on the
39
 * bean's properties, whose names must correspond to the names in the property resource. For example, if a bean
40
 * has a property {@code length} and it should be mapped from the property resource's value at path {@code definition},
41
 * the mapper will look up {@code definition.length} in the resource to determine the value.
42
 * <p>
43
 * Classes are created by the {@link BeanDefinitionService}. The {@link BeanDefinitionServiceImpl
44
 * default implementation} supports Java classes with a zero-args constructor, as well as Java records. The service can
45
 * be extended to support more types of classes.
46
 * <br>For Java classes with a zero-args constructor, the class's instance fields are taken as properties. You can
47
 * change the behavior of the fields with &#64;{@link ExportName} and &#64;{@link IgnoreInMapping}. There must be at
48
 * least one property for the class to be treated as a bean.
49
 * <p>
50
 * <b>Recursion:</b> the mapping of values to a bean is performed recursively, i.e. a bean may have other beans
51
 * as fields and generic types at any arbitrary "depth".
52
 * <p>
53
 * <b>Collections</b> are only supported if they have an explicit type argument, i.e. a field of {@code List<String>}
54
 * is supported but {@code List<?>} and {@code List<T extends Number>} are not supported. Specifically, you may
55
 * only declare fields of type {@link java.util.List} or {@link java.util.Set}, or a parent type ({@link Collection}
56
 * or {@link Iterable}) by default.
57
 * Fields of type <b>Map</b> are supported also, with similar limitations. Additionally, maps may only have
58
 * {@code String} as key type, but no restrictions are imposed on the value type.
59
 * <p>
60
 * Beans may have <b>optional fields</b>. If the mapper cannot map the property resource value to the corresponding
61
 * field, it only treats it as a failure if the field's value is {@code null}. If the field has a default value assigned
62
 * to it on initialization, the default value remains and the mapping process continues. If a bean is created with a
63
 * null property, the mapping process is stopped immediately.
64
 * <br>Optional properties can also be defined by declaring them with {@link Optional}.
65
 */
66
public class MapperImpl implements Mapper {
67

68
    /** Marker object to signal that null is meant to be used as value. */
69
    public static final Object RETURN_NULL = new Object();
5✔
70

71
    // ---------
72
    // Fields and general configurable methods
73
    // ---------
74

75
    private final LeafValueHandler leafValueHandler;
76
    private final BeanDefinitionService beanDefinitionService;
77

78
    public MapperImpl() {
79
        this(new BeanDefinitionServiceImpl(),
7✔
80
             new LeafValueHandlerImpl(LeafValueHandlerImpl.createDefaultLeafTypes()));
2✔
81
    }
1✔
82

83
    public MapperImpl(@NotNull BeanDefinitionService beanDefinitionService,
84
                      @NotNull LeafValueHandler leafValueHandler) {
2✔
85
        this.beanDefinitionService = beanDefinitionService;
3✔
86
        this.leafValueHandler = leafValueHandler;
3✔
87
    }
1✔
88

89
    protected final @NotNull BeanDefinitionService getBeanDefinitionService() {
90
        return beanDefinitionService;
3✔
91
    }
92

93
    protected final @NotNull LeafValueHandler getLeafValueHandler() {
94
        return leafValueHandler;
3✔
95
    }
96

97
    protected @NotNull MappingContext createRootMappingContext(@NotNull TypeInfo beanType,
98
                                                               @NotNull ConvertErrorRecorder errorRecorder) {
99
        return MappingContextImpl.createRoot(beanType, errorRecorder);
4✔
100
    }
101

102
    protected @NotNull ExportContext createRootExportContext() {
103
        return ExportContextImpl.createRoot();
2✔
104
    }
105

106

107
    // ---------
108
    // Export
109
    // ---------
110

111
    @Override
112
    public @Nullable Object toExportValue(@NotNull Object value) {
113
        return toExportValue(value, createRootExportContext());
6✔
114
    }
115

116
    /**
117
     * Transforms the given value to an object suitable for the export to a configuration file.
118
     *
119
     * @param value the value to transform
120
     * @param exportContext export context
121
     * @return export value to use
122
     */
123
    protected @Nullable Object toExportValue(@Nullable Object value, @NotNull ExportContext exportContext) {
124
        // Step 1: attempt simple value transformation
125
        Object exportValue = leafValueHandler.toExportValue(value, exportContext);
6✔
126
        if (exportValue != null || value == null) {
4✔
127
            return unwrapReturnNull(exportValue);
3✔
128
        }
129

130
        // Step 2: handle special cases like Collection
131
        exportValue = createExportValueForSpecialTypes(value, exportContext);
5✔
132
        if (exportValue != null) {
2✔
133
            return unwrapReturnNull(exportValue);
3✔
134
        }
135

136
        // Step 3: treat as bean
137
        Map<String, Object> mappedBean = new LinkedHashMap<>();
4✔
138
        for (BeanPropertyDefinition property : getBeanProperties(value)) {
12✔
139
            Object exportValueOfProperty = toExportValue(property.getValue(value), exportContext);
7✔
140
            if (exportValueOfProperty != null) {
2✔
141
                BeanPropertyComments propComments = property.getComments();
3✔
142
                if (exportContext.shouldInclude(propComments)) {
4✔
143
                    exportContext.registerComment(propComments);
3✔
144
                    exportValueOfProperty = new ValueWithComments(exportValueOfProperty,
4✔
145
                        propComments.getComments(), propComments.getUuid());
5✔
146
                }
147
                mappedBean.put(property.getName(), exportValueOfProperty);
6✔
148
            }
149
        }
1✔
150
        return mappedBean;
2✔
151
    }
152

153
    protected @NotNull List<BeanPropertyDefinition> getBeanProperties(@NotNull Object value) {
154
        return beanDefinitionService.findDefinition(value.getClass())
7✔
155
            .map(BeanDefinition::getProperties)
1✔
156
            .orElse(Collections.emptyList());
3✔
157
    }
158

159
    /**
160
     * Handles values of types which need special handling (such as Optional). Null means the value is not
161
     * a special type and that the export value should be built differently. Use {@link #RETURN_NULL} to
162
     * signal that null should be used as the export value of the provided value.
163
     *
164
     * @param value the value to convert
165
     * @param exportContext export context
166
     * @return the export value to use or {@link #RETURN_NULL}, or null if not applicable
167
     */
168
    protected @Nullable Object createExportValueForSpecialTypes(@Nullable Object value,
169
                                                                @NotNull ExportContext exportContext) {
170
        if (value instanceof Iterable<?>) {
3✔
171
            int index = 0;
2✔
172
            List<Object> result = new ArrayList<>();
4✔
173
            for (Object entry : (Iterable<?>) value) {
10✔
174
                ExportContext entryContext = exportContext.createChildContext(pathSpecifierForIndex(index));
5✔
175
                result.add(toExportValue(entry, entryContext));
7✔
176
                ++index;
1✔
177
            }
1✔
178
            return result;
2✔
179
        }
180

181
        if (value instanceof Map<?, ?>) {
3✔
182
            Map<Object, Object> result = new LinkedHashMap<>();
4✔
183
            for (Map.Entry<?, ?> entry : ((Map<?, ?>) value).entrySet()) {
12✔
184
                ExportContext entryContext = exportContext.createChildContext(pathSpecifierForMapKey(entry));
5✔
185
                result.put(entry.getKey(), toExportValue(entry.getValue(), entryContext));
10✔
186
            }
1✔
187
            return result;
2✔
188
        }
189

190
        if (value instanceof Optional<?>) {
3✔
191
            Optional<?> optional = (Optional<?>) value;
3✔
192
            return optional
5✔
193
                .map(v -> toExportValue(v, exportContext.createChildContext(OPTIONAL_SPECIFIER)))
9✔
194
                .orElse(RETURN_NULL);
1✔
195
        }
196

197
        return null;
2✔
198
    }
199

200
    protected static @Nullable Object unwrapReturnNull(@Nullable Object o) {
201
        return o == RETURN_NULL ? null : o;
7✔
202
    }
203

204
    // ---------
205
    // Bean mapping
206
    // ---------
207

208
    @Override
209
    public @Nullable Object convertToBean(@Nullable Object value, @NotNull TypeInfo targetType,
210
                                          @NotNull ConvertErrorRecorder errorRecorder) {
211
        if (value == null) {
2✔
212
            return null;
2✔
213
        }
214

215
        return convertValueForType(createRootMappingContext(targetType, errorRecorder), value);
8✔
216
    }
217

218
    /**
219
     * Main method for converting a value to another type.
220
     *
221
     * @param context the mapping context
222
     * @param value the value to convert from
223
     * @return object whose type matches the one in the mapping context, or null if not applicable
224
     */
225
    protected @Nullable Object convertValueForType(@NotNull MappingContext context, @Nullable Object value) {
226
        // Step 1: check if the value is a leaf
227
        Object result = leafValueHandler.convert(value, context);
6✔
228
        if (result != null) {
2✔
229
            return result;
2✔
230
        }
231

232
        // Step 2: check if we have a special type like List that is handled separately
233
        result = convertSpecialTypes(context, value);
5✔
234
        if (result != null) {
2✔
235
            return result;
2✔
236
        }
237

238
        // Step 3: last possibility - assume it's a bean and try to map values to its structure
239
        return createBean(context, value);
5✔
240
    }
241

242
    /**
243
     * Converts types in the bean mapping process which require special handling.
244
     *
245
     * @param context the mapping context
246
     * @param value the value to convert from
247
     * @return object whose type matches the one in the mapping context, or null if not applicable
248
     */
249
    protected @Nullable Object convertSpecialTypes(@NotNull MappingContext context, @Nullable Object value) {
250
        final Class<?> rawClass = context.getTargetTypeAsClassOrThrow();
3✔
251
        if (Iterable.class.isAssignableFrom(rawClass)) {
4✔
252
            return convertToCollection(context, value);
5✔
253
        } else if (Map.class.isAssignableFrom(rawClass)) {
4✔
254
            return convertToMap(context, value);
5✔
255
        } else if (Optional.class.isAssignableFrom(rawClass)) {
4✔
256
            return convertOptional(context, value);
5✔
257
        }
258
        return null;
2✔
259
    }
260

261
    // -- Collection
262

263
    /**
264
     * Handles the creation of Collection properties.
265
     *
266
     * @param context the mapping context
267
     * @param value the value to map from
268
     * @return Collection property from the value, or null if not applicable
269
     */
270
    @SuppressWarnings({"unchecked", "rawtypes"})
271
    protected @Nullable Collection<?> convertToCollection(@NotNull MappingContext context, @Nullable Object value) {
272
        if (value instanceof Iterable<?>) {
3✔
273
            TypeInfo entryType = context.getTargetTypeArgumentOrThrow(0);
4✔
274
            Collection result = createCollectionMatchingType(context);
4✔
275

276
            int index = 0;
2✔
277
            for (Object entry : (Iterable<?>) value) {
10✔
278
                MappingContext entryContext = context.createChild(pathSpecifierForIndex(index), entryType);
6✔
279
                Object convertedEntry = convertValueForType(entryContext, entry);
5✔
280
                if (convertedEntry == null) {
2✔
281
                    context.registerError("Cannot convert value at index " + index);
11✔
282
                } else {
283
                    result.add(convertedEntry);
4✔
284
                }
285
                ++index;
1✔
286
            }
1✔
287
            return result;
2✔
288
        }
289
        return null;
2✔
290
    }
291

292
    /**
293
     * Creates a Collection of a type which can be assigned to the provided type.
294
     *
295
     * @param mappingContext the current mapping context with a collection type
296
     * @return Collection of matching type
297
     */
298
    protected @NotNull Collection<?> createCollectionMatchingType(@NotNull MappingContext mappingContext) {
299
        Class<?> collectionType = mappingContext.getTargetTypeAsClassOrThrow();
3✔
300
        if (collectionType.isAssignableFrom(ArrayList.class)) {
4✔
301
            return new ArrayList<>();
4✔
302
        } else if (collectionType.isAssignableFrom(LinkedHashSet.class)) {
4✔
303
            return new LinkedHashSet<>();
4✔
304
        } else {
305
            throw new ConfigMeMapperException(mappingContext, "Unsupported collection type '" + collectionType + "'");
15✔
306
        }
307
    }
308

309
    // -- Map
310

311
    /**
312
     * Handles the creation of a Map property.
313
     *
314
     * @param context mapping context
315
     * @param value value to map from
316
     * @return Map property, or null if not applicable
317
     */
318
    @SuppressWarnings({"unchecked", "rawtypes"})
319
    protected @Nullable Map<?, ?> convertToMap(@NotNull MappingContext context, @Nullable Object value) {
320
        if (value instanceof Map<?, ?>) {
3✔
321
            if (context.getTargetTypeArgumentOrThrow(0).toClass() != String.class) {
6✔
322
                throw new ConfigMeMapperException(context, "The key type of maps may only be of String type");
6✔
323
            }
324
            TypeInfo mapValueType = context.getTargetTypeArgumentOrThrow(1);
4✔
325

326
            Map<String, ?> entries = (Map<String, ?>) value;
3✔
327
            Map result = createMapMatchingType(context);
4✔
328
            for (Map.Entry<String, ?> entry : entries.entrySet()) {
11✔
329
                MappingContext entryContext = context.createChild(pathSpecifierForMapKey(entry), mapValueType);
6✔
330
                Object mappedValue = convertValueForType(entryContext, entry.getValue());
6✔
331
                if (mappedValue == null) {
2✔
332
                    context.registerError("Cannot map value for key " + entry.getKey());
13✔
333
                } else {
334
                    result.put(entry.getKey(), mappedValue);
6✔
335
                }
336
            }
1✔
337
            return result;
2✔
338
        }
339
        return null;
2✔
340
    }
341

342
    /**
343
     * Creates a Map of a type which can be assigned to the provided type.
344
     *
345
     * @param mappingContext the current mapping context with a map type
346
     * @return Map of matching type
347
     */
348
    protected @NotNull Map<?, ?> createMapMatchingType(@NotNull MappingContext mappingContext) {
349
        Class<?> mapType = mappingContext.getTargetTypeAsClassOrThrow();
3✔
350
        if (mapType.isAssignableFrom(LinkedHashMap.class)) {
4✔
351
            return new LinkedHashMap<>();
4✔
352
        } else if (mapType.isAssignableFrom(TreeMap.class)) {
4✔
353
            return new TreeMap<>();
4✔
354
        } else {
355
            throw new ConfigMeMapperException(mappingContext, "Unsupported map type '" + mapType + "'");
15✔
356
        }
357
    }
358

359
    // -- Optional
360

361
    // Return value is never null, but if someone wants to override this, it's fine for it to be null
362
    protected @Nullable Object convertOptional(@NotNull MappingContext context, @Nullable Object value) {
363
        MappingContext childContext = context.createChild(OPTIONAL_SPECIFIER, context.getTargetTypeArgumentOrThrow(0));
7✔
364
        Object result = convertValueForType(childContext, value);
5✔
365
        return Optional.ofNullable(result);
3✔
366
    }
367

368
    // -- Bean
369

370
    /**
371
     * Converts the provided value to the requested bean class if possible.
372
     *
373
     * @param context mapping context (incl. desired type)
374
     * @param value the value from the property resource
375
     * @return the converted value, or null if not possible
376
     */
377
    protected @Nullable Object createBean(@NotNull MappingContext context, @Nullable Object value) {
378
        // Ensure that the value is a map so we can map it to a bean
379
        if (!(value instanceof Map<?, ?>)) {
3✔
380
            return null;
2✔
381
        }
382
        Map<?, ?> entries = (Map<?, ?>) value;
3✔
383

384
        Optional<BeanDefinition> definition =
3✔
385
            beanDefinitionService.findDefinition(context.getTargetTypeAsClassOrThrow());
3✔
386
        if (definition.isPresent()) {
3!
387
            List<Object> propertyValues = definition.get().getProperties().stream()
9✔
388
                .map(prop -> {
1✔
389
                    MappingContext childContext = context.createChild(prop.getName(), prop.getTypeInformation());
7✔
390
                    return convertValueForType(childContext, entries.get(prop.getName()));
8✔
391
                })
392
                .collect(Collectors.toList());
4✔
393

394
            return definition.get().create(propertyValues, context.getErrorRecorder());
8✔
395
        }
NEW
396
        return null;
×
397
    }
398
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc