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

AuthMe / ConfigMe / 11407666717

18 Oct 2024 04:46PM UTC coverage: 99.322% (-0.09%) from 99.411%
11407666717

Pull #398

github

ljacqu
Fix toString of NumberType to be like class name
Pull Request #398: #135 Support more ways to create beans (e.g. Java records)

557 of 574 branches covered (97.04%)

201 of 202 new or added lines in 11 files covered. (99.5%)

7 existing lines in 2 files now uncovered.

1612 of 1623 relevant lines covered (99.32%)

4.6 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.instantiation.BeanInstantiation;
8
import ch.jalu.configme.beanmapper.instantiation.BeanInstantiationService;
9
import ch.jalu.configme.beanmapper.instantiation.BeanInstantiationServiceImpl;
10
import ch.jalu.configme.beanmapper.leafvaluehandler.LeafValueHandler;
11
import ch.jalu.configme.beanmapper.leafvaluehandler.LeafValueHandlerImpl;
12
import ch.jalu.configme.beanmapper.propertydescription.BeanPropertyComments;
13
import ch.jalu.configme.beanmapper.propertydescription.BeanPropertyDescription;
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 with the names in the property resource. For example, if a bean class
40
 * has a property {@code length} and should be mapped from the property resource's value at path {@code definition},
41
 * the mapper will look up {@code definition.length} to get the value of the bean property.
42
 * <p>
43
 * Classes are created by the {@link BeanInstantiationService}. The {@link BeanInstantiationServiceImpl
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 private fields are taken as properties. The perceived
47
 * properties can be modified with {@link ExportName} and {@link IgnoreInMapping}.
48
 * <p>
49
 * <b>Recursion:</b> the mapping of values to a bean is performed recursively, i.e. a bean may have other beans
50
 * as fields and generic types at any arbitrary "depth".
51
 * <p>
52
 * <b>Collections</b> are only supported if they have an explicit type argument, i.e. a field of {@code List<String>}
53
 * is supported but {@code List<?>} and {@code List<T extends Number>} are not supported. Specifically, you may
54
 * only declare fields of type {@link java.util.List} or {@link java.util.Set}, or a parent type ({@link Collection}
55
 * or {@link Iterable}) by default.
56
 * Fields of type <b>Map</b> are supported also, with similar limitations. Additionally, maps may only have
57
 * {@code String} as key type, but no restrictions are imposed on the value type.
58
 * <p>
59
 * Beans may have <b>optional fields</b>. If the mapper cannot map the property resource value to the corresponding
60
 * field, it only treats it as a failure if the field's value is {@code null}. If the field has a default value assigned
61
 * to it on initialization, the default value remains and the mapping process continues. If a bean is created with a
62
 * null property, the mapping process is stopped immediately.
63
 * <br>Optional properties can also be defined by declaring them with {@link Optional}.
64
 */
65
public class MapperImpl implements Mapper {
66

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

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

74
    private final LeafValueHandler leafValueHandler;
75
    private final BeanInstantiationService beanInstantiationService;
76

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

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

88
    protected final @NotNull BeanInstantiationService getBeanInstantiationService() {
89
        return beanInstantiationService;
3✔
90
    }
91

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

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

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

105

106
    // ---------
107
    // Export
108
    // ---------
109

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

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

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

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

152
    protected @NotNull List<BeanPropertyDescription> getBeanProperties(@NotNull Object value) {
153
        return beanInstantiationService.findInstantiation(value.getClass())
7✔
154
            .map(BeanInstantiation::getProperties)
1✔
155
            .orElse(Collections.emptyList());
3✔
156
    }
157

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

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

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

196
        return null;
2✔
197
    }
198

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

203
    // ---------
204
    // Bean mapping
205
    // ---------
206

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

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

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

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

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

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

260
    // -- Collection
261

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

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

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

308
    // -- Map
309

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

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

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

358
    // -- Optional
359

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

367
    // -- Bean
368

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

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

393
            return instantiation.get().create(propertyValues, context.getErrorRecorder());
8✔
394
        }
NEW
395
        return null;
×
396
    }
397
}
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