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

wistefan / ngsi-ld-java-mapping / #153

07 Dec 2023 12:08PM UTC coverage: 79.483% (-0.4%) from 79.859%
#153

Pull #46

wistefan
testing and empty handling
Pull Request #46: allow mappings with non-existent relationships

249 of 312 new or added lines in 3 files covered. (79.81%)

2 existing lines in 1 file now uncovered.

461 of 580 relevant lines covered (79.48%)

0.79 hits per line

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

81.28
/src/main/java/io/github/wistefan/mapping/EntityVOMapper.java
1
package io.github.wistefan.mapping;
2

3
import com.fasterxml.jackson.core.type.TypeReference;
4
import com.fasterxml.jackson.databind.ObjectMapper;
5
import io.github.wistefan.mapping.annotations.AttributeSetter;
6
import io.github.wistefan.mapping.annotations.AttributeType;
7
import io.github.wistefan.mapping.annotations.MappingEnabled;
8
import lombok.extern.slf4j.Slf4j;
9
import org.fiware.ngsi.model.*;
10
import reactor.core.publisher.Mono;
11

12
import javax.inject.Singleton;
13
import java.lang.reflect.Constructor;
14
import java.lang.reflect.InvocationTargetException;
15
import java.lang.reflect.Method;
16
import java.net.URI;
17
import java.util.*;
18
import java.util.stream.Collectors;
19
import java.util.stream.Stream;
20

21
/**
22
 * Mapper to handle translation from NGSI-LD entities to Java-Objects, based on annotations added to the target class
23
 */
24
@Slf4j
1✔
25
@Singleton
26
public class EntityVOMapper extends Mapper {
27

28
    private final MappingProperties mappingProperties;
29
    private final ObjectMapper objectMapper;
30
    private final EntitiesRepository entitiesRepository;
31

32
    public EntityVOMapper(MappingProperties mappingProperties, ObjectMapper objectMapper, EntitiesRepository entitiesRepository) {
1✔
33
        this.mappingProperties = mappingProperties;
1✔
34
        this.objectMapper = objectMapper;
1✔
35
        this.entitiesRepository = entitiesRepository;
1✔
36
        this.objectMapper
1✔
37
                .addMixIn(AdditionalPropertyVO.class, AdditionalPropertyMixin.class);
1✔
38
        this.objectMapper.findAndRegisterModules();
1✔
39
    }
1✔
40

41
    /**
42
     * Method to convert a Java-Object to Map representation
43
     *
44
     * @param entity the entity to be converted
45
     * @return the converted map
46
     */
47
    public <T> Map<String, Object> convertEntityToMap(T entity) {
48
        return objectMapper.convertValue(entity, new TypeReference<>() {
1✔
49
        });
50
    }
51

52
    /**
53
     * Method to map an NGSI-LD Entity into a Java-Object of class targetClass. The class has to provide a string constructor to receive the entity id
54
     *
55
     * @param entityVO    the NGSI-LD entity to be mapped
56
     * @param targetClass class of the target object
57
     * @param <T>         generic type of the target object, has to extend provide a string-constructor to receive the entity id
58
     * @return the mapped object
59
     */
60
    public <T> Mono<T> fromEntityVO(EntityVO entityVO, Class<T> targetClass) {
61

62
        Optional<MappingEnabled> optionalMappingEnabled = isMappingEnabled(targetClass);
1✔
63
        if (!optionalMappingEnabled.isPresent()) {
1✔
64
            return Mono.error(new MappingException(String.format("Mapping is not enabled for class %s", targetClass)));
1✔
65
        }
66

67
        MappingEnabled mappingEnabled = optionalMappingEnabled.get();
1✔
68

69
        if (!Arrays.stream(mappingEnabled.entityType()).toList().contains(entityVO.getType())) {
1✔
70
            return Mono.error(new MappingException(String.format("Entity and Class type do not match - %s vs %s.", entityVO.getType(), Arrays.asList(mappingEnabled.entityType()))));
1✔
71
        }
72
        Map<String, AdditionalPropertyVO> additionalPropertyVOMap = Optional.ofNullable(entityVO.getAdditionalProperties()).orElse(Map.of());
1✔
73

74
        return getRelationshipMap(additionalPropertyVOMap, targetClass)
1✔
75
                .flatMap(relationshipMap -> fromEntityVO(entityVO, targetClass, relationshipMap));
1✔
76

77
    }
78

79
    /**
80
     * Return a single, emitting the entities associated with relationships in the given properties maps
81
     *
82
     * @param propertiesMap properties map to evaluate
83
     * @param targetClass   class of the target object
84
     * @param <T>           the class
85
     * @return a single, emitting the map of related entities
86
     */
87
    private <T> Mono<Map<String, EntityVO>> getRelationshipMap(Map<String, AdditionalPropertyVO> propertiesMap, Class<T> targetClass) {
88
        return Optional.ofNullable(entitiesRepository.getEntities(getRelationshipObjects(propertiesMap, targetClass)))
1✔
89
                .orElse(Mono.just(List.of()))
1✔
90
                .switchIfEmpty(Mono.just(List.of()))
1✔
91
                .map(relationshipsList -> relationshipsList.stream().map(EntityVO.class::cast).collect(Collectors.toMap(e -> e.getId().toString(), e -> e)))
1✔
92
                .defaultIfEmpty(Map.of());
1✔
93
    }
94

95
    /**
96
     * Create the actual object from the entity, after its relations are evaluated.
97
     *
98
     * @param entityVO        entity to create the object from
99
     * @param targetClass     class of the object to be created
100
     * @param relationShipMap all entities (directly) related to the objects. Sub relationships(e.g. relationships of properties) will be evaluated downstream.
101
     * @param <T>             the class
102
     * @return a single, emitting the actual object.
103
     */
104
    private <T> Mono<T> fromEntityVO(EntityVO entityVO, Class<T> targetClass, Map<String, EntityVO> relationShipMap) {
105
        try {
106
            Constructor<T> objectConstructor = targetClass.getDeclaredConstructor(String.class);
1✔
107
            T constructedObject = objectConstructor.newInstance(entityVO.getId().toString());
1✔
108

109
            // handle "well-known" properties
110
            Map<String, AdditionalPropertyVO> propertiesMap = new LinkedHashMap<>();
1✔
111
            propertiesMap.put(EntityVO.JSON_PROPERTY_LOCATION, entityVO.getLocation());
1✔
112
            propertiesMap.put(EntityVO.JSON_PROPERTY_OBSERVATION_SPACE, entityVO.getObservationSpace());
1✔
113
            propertiesMap.put(EntityVO.JSON_PROPERTY_OPERATION_SPACE, entityVO.getOperationSpace());
1✔
114
            propertiesMap.put(EntityVO.JSON_PROPERTY_CREATED_AT, propertyVOFromValue(entityVO.getCreatedAt()));
1✔
115
            propertiesMap.put(EntityVO.JSON_PROPERTY_MODIFIED_AT, propertyVOFromValue(entityVO.getModifiedAt()));
1✔
116
            Optional.ofNullable(entityVO.getAdditionalProperties()).ifPresent(propertiesMap::putAll);
1✔
117

118
            List<Mono<T>> singleInvocations = propertiesMap.entrySet().stream()
1✔
119
                    .map(entry -> getObjectInvocation(entry, constructedObject, relationShipMap, entityVO.getId().toString()))
1✔
120
                    .toList();
1✔
121

122
            return Mono.zip(singleInvocations, constructedObjects -> constructedObject);
1✔
123

124
        } catch (NoSuchMethodException e) {
1✔
125
            return Mono.error(new MappingException(String.format("The class %s does not declare the required String id constructor.", targetClass)));
1✔
126
        } catch (InvocationTargetException | InstantiationException | IllegalAccessException e) {
1✔
127
            return Mono.error(new MappingException(String.format("Was not able to create instance of %s.", targetClass), e));
1✔
128
        }
129
    }
130

131
    /**
132
     * Helper method to create a propertyVO for well-known(thus flat) properties
133
     *
134
     * @param value the value to wrap
135
     * @return a propertyVO containing the value
136
     */
137
    private PropertyVO propertyVOFromValue(Object value) {
138
        PropertyVO propertyVO = new PropertyVO();
1✔
139
        propertyVO.setValue(value);
1✔
140
        return propertyVO;
1✔
141
    }
142

143
    /**
144
     * Get the invocation on the object to be constructed.
145
     *
146
     * @param entry                   additional properties entry
147
     * @param objectUnderConstruction the new object, to be filled with the values
148
     * @param relationShipMap         map of pre-evaluated relations
149
     * @param entityId                id of the entity
150
     * @param <T>                     class of the constructed object
151
     * @return single, emmiting the constructed object
152
     */
153
    private <T> Mono<T> getObjectInvocation(Map.Entry<String, AdditionalPropertyVO> entry, T objectUnderConstruction, Map<String, EntityVO> relationShipMap, String entityId) {
154
        Optional<Method> optionalSetter = getCorrespondingSetterMethod(objectUnderConstruction, entry.getKey());
1✔
155
        if (optionalSetter.isEmpty()) {
1✔
156
            log.warn("Ignoring property {} for entity {} since there is no mapping configured.", entry.getKey(), entityId);
1✔
157
            return Mono.just(objectUnderConstruction);
1✔
158
        }
159
        Method setterMethod = optionalSetter.get();
1✔
160
        Optional<AttributeSetter> optionalAttributeSetter = getAttributeSetterAnnotation(setterMethod);
1✔
161
        if (optionalAttributeSetter.isEmpty()) {
1✔
162
            log.warn("Ignoring property {} for entity {} since there is no attribute setter configured.", entry.getKey(), entityId);
×
163
            return Mono.just(objectUnderConstruction);
×
164
        }
165
        AttributeSetter setterAnnotation = optionalAttributeSetter.get();
1✔
166

167
        Class<?> parameterType = getParameterType(setterMethod.getParameterTypes());
1✔
168

169
        return switch (setterAnnotation.value()) {
1✔
170
            case PROPERTY, GEO_PROPERTY ->
171
                    handleProperty(entry.getValue(), objectUnderConstruction, optionalSetter.get(), parameterType);
1✔
172
            case PROPERTY_LIST ->
173
                    handlePropertyList(entry.getValue(), objectUnderConstruction, optionalSetter.get(), setterAnnotation);
1✔
174
            case RELATIONSHIP ->
175
                    handleRelationship(entry.getValue(), objectUnderConstruction, relationShipMap, optionalSetter.get(), setterAnnotation);
1✔
176
            case RELATIONSHIP_LIST ->
177
                    handleRelationshipList(entry.getValue(), objectUnderConstruction, relationShipMap, optionalSetter.get(), setterAnnotation);
1✔
178
            default ->
NEW
179
                    Mono.error(new MappingException(String.format("Received type %s is not supported.", setterAnnotation.value())));
×
180
        };
181
    }
182

183
    /**
184
     * Handle the evaluation of a property entry. Returns a single, emitting the target object, while invoking the property setting method.
185
     *
186
     * @param propertyValue           the value of the property
187
     * @param objectUnderConstruction the object under construction
188
     * @param setter                  the setter to be used for the property
189
     * @param parameterType           type of the property in the target object
190
     * @param <T>                     class of the object under construction
191
     * @return the single, emitting the objectUnderConstruction
192
     */
193
    private <T> Mono<T> handleProperty(AdditionalPropertyVO propertyValue, T objectUnderConstruction, Method setter, Class<?> parameterType) {
194
        if (propertyValue instanceof PropertyVO propertyVO)
1✔
195
            return invokeWithExceptionHandling(setter, objectUnderConstruction, objectMapper.convertValue(propertyVO.getValue(), parameterType));
1✔
196
        else {
197
            log.error("Mapping exception");
×
198
            return Mono.error(new MappingException(String.format("The attribute is not a valid property: %s ", propertyValue)));
×
199
        }
200
    }
201

202
    /**
203
     * Handle the evaluation of a property-list entry. Returns a single, emitting the target object, while invoking the property setting method.
204
     *
205
     * @param propertyListObject      the object containing the property-list
206
     * @param objectUnderConstruction the object under construction
207
     * @param setter                  the setter to be used for the property
208
     * @param <T>                     class of the object under construction
209
     * @return the single, emitting the objectUnderConstruction
210
     */
211
    private <T> Mono<T> handlePropertyList(AdditionalPropertyVO propertyListObject, T objectUnderConstruction, Method setter, AttributeSetter setterAnnotation) {
212
        if (propertyListObject instanceof PropertyListVO propertyVOS) {
1✔
213
            return invokeWithExceptionHandling(setter, objectUnderConstruction, propertyListToTargetClass(propertyVOS, setterAnnotation.targetClass()));
1✔
214
        } else if (propertyListObject instanceof PropertyVO propertyVO) {
1✔
215
            //we need special handling here, since we have no real property lists(see NGSI-LD issue)
216
            // TODO: remove as soon as ngsi-ld does properly support that.
217
            if (propertyVO.getValue() instanceof List propertyList) {
1✔
218
                return invokeWithExceptionHandling(setter, objectUnderConstruction, propertyList.stream()
1✔
219
                        .map(listValue -> objectMapper.convertValue(listValue, setterAnnotation.targetClass()))
1✔
220
                        .toList());
1✔
221
            }
222
            PropertyListVO propertyVOS = new PropertyListVO();
×
223
            propertyVOS.add(propertyVO);
×
224
            // in case of single element lists, they are returned as a flat property
225
            return invokeWithExceptionHandling(setter, objectUnderConstruction, propertyListToTargetClass(propertyVOS, setterAnnotation.targetClass()));
×
226
        } else {
227
            return Mono.error(new MappingException(String.format("The attribute is not a valid property list: %v ", propertyListObject)));
×
228
        }
229
    }
230

231
    /**
232
     * Handle the evaluation of a relationship-list entry. Returns a single, emitting the target object, while invoking the property setting method.
233
     *
234
     * @param attributeValue          the entry containing the relationship-list
235
     * @param objectUnderConstruction the object under construction
236
     * @param relationShipMap         a map containing the pre-evaluated relationships
237
     * @param setter                  the setter to be used for the property
238
     * @param setterAnnotation        attribute setter annotation on the method
239
     * @param <T>                     class of the objectUnderConstruction
240
     * @return the single, emitting the objectUnderConstruction
241
     */
242
    private <T> Mono<T> handleRelationshipList(AdditionalPropertyVO attributeValue, T objectUnderConstruction, Map<String, EntityVO> relationShipMap, Method setter, AttributeSetter setterAnnotation) {
243
        Class<?> targetClass = setterAnnotation.targetClass();
1✔
244
        if (setterAnnotation.fromProperties()) {
1✔
245
            if (attributeValue instanceof RelationshipVO singlePropertyMap) {
1✔
246
                return relationshipFromProperties(singlePropertyMap, targetClass)
×
247
                        .flatMap(relationship -> {
×
248
                            // we return the constructed object, since invoke most likely returns null, which is not allowed on mapper functions
249
                            // a list is created, since we have a relationship-list defined by the annotation
250
                            return invokeWithExceptionHandling(setter, objectUnderConstruction, List.of(relationship));
×
251
                        });
252
            } else if (attributeValue instanceof RelationshipListVO multiPropertyList) {
1✔
253
                return Mono.zip(multiPropertyList.stream().map(relationshipVO -> relationshipFromProperties(relationshipVO, targetClass)).toList(),
1✔
254
                        oList -> Arrays.asList(oList).stream().map(targetClass::cast).toList()).flatMap(relationshipList -> {
1✔
255
                    // we return the constructed object, since invoke most likely returns null, which is not allowed on mapper functions
256
                    return invokeWithExceptionHandling(setter, objectUnderConstruction, relationshipList);
1✔
257
                });
258

259
            } else {
260
                return Mono.error(new MappingException(String.format("Value of the relationship %s is invalid.", attributeValue)));
×
261
            }
262
        } else {
263
            return relationshipListToTargetClass(attributeValue, targetClass, relationShipMap)
1✔
264
                    .flatMap(relatedEntities -> {
1✔
265
                        // we return the constructed object, since invoke most likely returns null, which is not allowed on mapper functions
266
                        return invokeWithExceptionHandling(setter, objectUnderConstruction, relatedEntities);
1✔
267
                    });
268
        }
269
    }
270

271
    /**
272
     * Handle the evaluation of a relationship entry. Returns a single, emitting the target object, while invoking the property setting method.
273
     *
274
     * @param relationShip            the object containing the relationship
275
     * @param objectUnderConstruction the object under construction
276
     * @param relationShipMap         a map containing the pre-evaluated relationships
277
     * @param setter                  the setter to be used for the property
278
     * @param setterAnnotation        attribute setter annotation on the method
279
     * @param <T>                     class of the objectUnderConstruction
280
     * @return the single, emitting the objectUnderConstruction
281
     */
282
    private <T> Mono<T> handleRelationship(AdditionalPropertyVO relationShip, T objectUnderConstruction, Map<String, EntityVO> relationShipMap, Method setter, AttributeSetter setterAnnotation) {
283
        Class<?> targetClass = setterAnnotation.targetClass();
1✔
284
        if (relationShip instanceof RelationshipVO relationshipVO) {
1✔
285
            if (setterAnnotation.fromProperties()) {
1✔
286
                return relationshipFromProperties(relationshipVO, targetClass)
1✔
287
                        .flatMap(relatedEntity -> {
1✔
288
                            // we return the constructed object, since invoke most likely returns null, which is not allowed on mapper functions
289
                            return invokeWithExceptionHandling(setter, objectUnderConstruction, relatedEntity);
1✔
290
                        });
291
            } else {
292
                return getObjectFromRelationship(relationshipVO, targetClass, relationShipMap, relationshipVO.getAdditionalProperties())
1✔
293
                        .flatMap(relatedEntity -> {
1✔
294
                            // we return the constructed object, since invoke most likely returns null, which is not allowed on mapper functions
295
                            return invokeWithExceptionHandling(setter, objectUnderConstruction, relatedEntity);
1✔
296
                        });
297
                // handle overwrites from property
298

299
            }
300
        } else {
301
            return Mono.error(new MappingException(String.format("Did not receive a valid relationship: %s", relationShip)));
×
302
        }
303
    }
304

305
    /**
306
     * Invoke the given method and handle potential exceptions.
307
     */
308
    private <T> Mono<T> invokeWithExceptionHandling(Method invocationMethod, T objectUnderConstruction, Object... invocationArgs) {
309
        try {
310
            invocationMethod.invoke(objectUnderConstruction, invocationArgs);
1✔
311
            return Mono.just(objectUnderConstruction);
1✔
312
        } catch (IllegalAccessException | InvocationTargetException | RuntimeException e) {
×
313
            return Mono.error(new MappingException(String.format("Was not able to invoke method %s.", invocationMethod.getName()), e));
×
314
        }
315
    }
316

317
    /**
318
     * Create the target object of a relationship from its properties(instead of entities additionally retrieved)
319
     *
320
     * @param relationshipVO representation of the current relationship(as provided by the original entitiy)
321
     * @param targetClass    class of the target object to be created(e.g. the object representing the relationship)
322
     * @param <T>            the class
323
     * @return a single emitting the object representing the relationship
324
     */
325
    private <T> Mono<T> relationshipFromProperties(RelationshipVO relationshipVO, Class<T> targetClass) {
326
        try {
327
            String entityID = relationshipVO.getObject().toString();
1✔
328

329
            Constructor<T> objectConstructor = targetClass.getDeclaredConstructor(String.class);
1✔
330
            T constructedObject = objectConstructor.newInstance(entityID);
1✔
331

332
            Map<String, Method> attributeSetters = getAttributeSetterMethodMap(constructedObject);
1✔
333

334
            return Mono.zip(attributeSetters.entrySet().stream()
1✔
335
                    .map(methodEntry -> {
1✔
336
                        String field = methodEntry.getKey();
1✔
337
                        Method setterMethod = methodEntry.getValue();
1✔
338
                        Optional<AttributeSetter> optionalAttributeSetterAnnotation = getAttributeSetterAnnotation(setterMethod);
1✔
339
                        if (optionalAttributeSetterAnnotation.isEmpty()) {
1✔
340
                            // no setter for the field, can be ignored
341
                            log.debug("No setter defined for field {}", field);
×
342
                            return Mono.just(constructedObject);
×
343
                        }
344
                        AttributeSetter setterAnnotation = optionalAttributeSetterAnnotation.get();
1✔
345

346
                        Optional<AdditionalPropertyVO> optionalProperty = switch (methodEntry.getKey()) {
1✔
347
                            case RelationshipVO.JSON_PROPERTY_OBSERVED_AT ->
348
                                    Optional.ofNullable(relationshipVO.getObservedAt()).map(this::propertyVOFromValue);
1✔
349
                            case RelationshipVO.JSON_PROPERTY_CREATED_AT ->
350
                                    Optional.ofNullable(relationshipVO.getCreatedAt()).map(this::propertyVOFromValue);
1✔
351
                            case RelationshipVO.JSON_PROPERTY_MODIFIED_AT ->
352
                                    Optional.ofNullable(relationshipVO.getModifiedAt()).map(this::propertyVOFromValue);
1✔
353
                            case RelationshipVO.JSON_PROPERTY_DATASET_ID ->
354
                                    Optional.ofNullable(relationshipVO.getDatasetId()).map(this::propertyVOFromValue);
1✔
355
                            case RelationshipVO.JSON_PROPERTY_INSTANCE_ID ->
356
                                    Optional.ofNullable(relationshipVO.getInstanceId()).map(this::propertyVOFromValue);
1✔
357
                            default -> Optional.empty();
1✔
358
                        };
359

360
                        // try to find the attribute from the additional properties
361
                        if (optionalProperty.isEmpty() && relationshipVO.getAdditionalProperties() != null && relationshipVO.getAdditionalProperties().containsKey(field)) {
1✔
362
                            optionalProperty = Optional.ofNullable(relationshipVO.getAdditionalProperties().get(field));
1✔
363
                        }
364

365
                        return optionalProperty.map(attributeValue -> {
1✔
366
                            return switch (setterAnnotation.value()) {
1✔
367
                                case PROPERTY, GEO_PROPERTY ->
368
                                        handleProperty(attributeValue, constructedObject, setterMethod, setterAnnotation.targetClass());
1✔
369
                                case RELATIONSHIP ->
NEW
370
                                        getRelationshipMap(relationshipVO.getAdditionalProperties(), targetClass)
×
NEW
371
                                                .map(rm -> handleRelationship(attributeValue, constructedObject, rm, setterMethod, setterAnnotation));
×
372
                                //resolve objects;
373
                                case RELATIONSHIP_LIST ->
NEW
374
                                        getRelationshipMap(relationshipVO.getAdditionalProperties(), targetClass)
×
NEW
375
                                                .map(rm -> handleRelationshipList(attributeValue, constructedObject, rm, setterMethod, setterAnnotation));
×
376
                                case PROPERTY_LIST ->
NEW
377
                                        handlePropertyList(attributeValue, constructedObject, setterMethod, setterAnnotation);
×
378
                                default ->
NEW
379
                                        Mono.error(new MappingException(String.format("Received type %s is not supported.", setterAnnotation.value())));
×
380
                            };
381
                        }).orElse(Mono.just(constructedObject));
1✔
382

383
                    }).toList(), constructedObjects -> constructedObject);
1✔
384

385
        } catch (NoSuchMethodException e) {
×
386
            return Mono.error(new MappingException(String.format("The class %s does not declare the required String id constructor.", targetClass)));
×
387
        } catch (InvocationTargetException | InstantiationException | IllegalAccessException e) {
×
388
            return Mono.error(new MappingException(String.format("Was not able to create instance of %s.", targetClass), e));
×
389
        }
390
    }
391

392
    /**
393
     * Returns a list of all entityIDs that are defined as relationships from the given entity.
394
     *
395
     * @param additionalProperties map of the properties to evaluate
396
     * @param targetClass          target class of the mapping
397
     * @param <T>                  the class
398
     * @return a list of uris
399
     */
400
    private <T> List<URI> getRelationshipObjects(Map<String, AdditionalPropertyVO> additionalProperties, Class<T> targetClass) {
401
        return Arrays.stream(targetClass.getMethods())
1✔
402
                .map(this::getAttributeSetterAnnotation)
1✔
403
                .filter(Optional::isPresent)
1✔
404
                .map(Optional::get)
1✔
405
                .filter(a -> (a.value().equals(AttributeType.RELATIONSHIP) || a.value().equals(AttributeType.RELATIONSHIP_LIST)))
1✔
406
                // we don't need to retrieve entities that should be filled from the properties.
407
                .filter(a -> !a.fromProperties())
1✔
408
                .flatMap(attributeSetter -> getEntityURIsByAttributeSetter(attributeSetter, additionalProperties).stream())
1✔
409
                .toList();
1✔
410

411
    }
412

413
    /**
414
     * Evaluate a properties map to get all referenced entity ids
415
     *
416
     * @param attributeSetter the attribute setter annotation
417
     * @param propertiesMap   the properties map to check
418
     * @return a list of entity ids
419
     */
420
    private List<URI> getEntityURIsByAttributeSetter(AttributeSetter attributeSetter, Map<String, AdditionalPropertyVO> propertiesMap) {
421
        return Optional.ofNullable(propertiesMap.get(attributeSetter.targetName()))
1✔
422
                .map(this::getURIsFromRelationshipObject)
1✔
423
                .orElseGet(List::of);
1✔
424
    }
425

426
    /**
427
     * Evaluate a concrete object of a realitonship. If its a list of objects, get the ids of all entities.
428
     *
429
     * @param additionalPropertyVO the object to evaluate
430
     * @return a list of all referenced ids
431
     */
432
    private List<URI> getURIsFromRelationshipObject(AdditionalPropertyVO additionalPropertyVO) {
433
        if (additionalPropertyVO instanceof RelationshipVO relationshipVO) {
1✔
434
            // List.of() cannot be used, since we need a mutable list
435
            List<URI> uriList = new ArrayList<>();
1✔
436
            uriList.add(relationshipVO.getObject());
1✔
437
            return uriList;
1✔
438
        } else if (additionalPropertyVO instanceof RelationshipListVO relationshipList) {
1✔
439
            return relationshipList.stream().flatMap(listEntry -> getURIsFromRelationshipObject(listEntry).stream()).toList();
1✔
440
        }
441
        return List.of();
×
442
    }
443

444
    /**
445
     * Method to translate a Map-Entry(e.g. NGSI-LD relationship) to a typed list as defined by the target object
446
     *
447
     * @param entry       attribute of the entity, e.g. a relationship or a list of relationships
448
     * @param targetClass class to be used as type for the typed list
449
     * @param <T>         the type
450
     * @return a list of objects, mapping the relationship
451
     */
452
    private <T> Mono<List<T>> relationshipListToTargetClass(AdditionalPropertyVO entry, Class<T> targetClass, Map<String, EntityVO> relationShipEntitiesMap) {
453
        if (entry instanceof RelationshipVO relationshipVO) {
1✔
NEW
454
            return getObjectFromRelationship(relationshipVO, targetClass, relationShipEntitiesMap, relationshipVO.getAdditionalProperties())
×
NEW
455
                    .map(List::of);
×
456
        } else if (entry instanceof RelationshipListVO relationshipMap) {
1✔
457
            return zipToList(relationshipMap.stream(), targetClass, relationShipEntitiesMap);
1✔
458

459
        }
460
        return Mono.error(new MappingException(String.format("Did not receive a valid entry: %s", entry)));
×
461
    }
462

463
    /**
464
     * Helper method for combining the evaluation of relationship entities to a single result lits
465
     *
466
     * @param relationshipVOStream    the relationships to evaluate
467
     * @param targetClass             target class of the relationship object
468
     * @param relationShipEntitiesMap map of the preevaluated relationship entities
469
     * @param <T>                     target class of the relationship
470
     * @return a single emitting the full list
471
     */
472
    private <T> Mono<List<T>> zipToList(Stream<RelationshipVO> relationshipVOStream, Class<T> targetClass, Map<String, EntityVO> relationShipEntitiesMap) {
473
        return Mono.zip(
1✔
474
                relationshipVOStream.map(RelationshipVO::getObject)
1✔
475
                        .filter(Objects::nonNull)
1✔
476
                        .map(URI::toString)
1✔
477
                        .map(relationShipEntitiesMap::get)
1✔
478
                        .map(entity -> fromEntityVO(entity, targetClass))
1✔
479
                        .toList(),
1✔
480
                oList -> Arrays.stream(oList).map(targetClass::cast).toList()
1✔
481
        );
482
    }
483

484
    /**
485
     * Method to translate a Map-Entry(e.g. NGSI-LD property) to a typed list as defined by the target object
486
     *
487
     * @param propertyVOS a list of properties
488
     * @param targetClass class to be used as type for the typed list
489
     * @param <T>         the type
490
     * @return a list of objects, mapping the relationship
491
     */
492
    private <T> List<T> propertyListToTargetClass(PropertyListVO propertyVOS, Class<T> targetClass) {
493
        return propertyVOS.stream().map(propertyEntry -> objectMapper.convertValue(propertyEntry.getValue(), targetClass)).toList();
1✔
494
    }
495

496
    /**
497
     * Retrieve the object from a relationship and return it as a java object of class T. All sub relationships will be evaluated, too.
498
     *
499
     * @param relationshipVO the relationship entry
500
     * @param targetClass    the target-class of the entry
501
     * @param <T>            the class
502
     * @return the actual object
503
     */
504
    private <T> Mono<T> getObjectFromRelationship(RelationshipVO relationshipVO, Class<T> targetClass, Map<String, EntityVO> relationShipEntitiesMap, Map<String, AdditionalPropertyVO> additionalPropertyVOMap) {
505
        Optional<EntityVO> optionalEntityVO = Optional.ofNullable(relationShipEntitiesMap.get(relationshipVO.getObject().toString()));
1✔
506
        if (optionalEntityVO.isEmpty() && !mappingProperties.isStrictRelationships()) {
1✔
507
            try {
508
                Constructor<T> objectConstructor = targetClass.getDeclaredConstructor(String.class);
1✔
509
                T theObject = objectConstructor.newInstance(relationshipVO.getObject().toString());
1✔
510
                // return the empty object
511
                return Mono.just(theObject);
1✔
NEW
512
            } catch (NoSuchMethodException e) {
×
NEW
513
                return Mono.error(new MappingException(String.format("Requested class %s does not have the required string constructor.", targetClass), e));
×
NEW
514
            } catch (InvocationTargetException | InstantiationException | IllegalAccessException e) {
×
NEW
515
                return Mono.error(new MappingException(String.format("Was not able to instantiate %s with a string parameter.", targetClass), e));
×
516
            }
517
        } else if (optionalEntityVO.isEmpty()) {
1✔
518
            return Mono.error(new MappingException(String.format("Was not able to resolve the relationship %s", relationshipVO.getObject())));
1✔
519
        }
520

521
        var entityVO = optionalEntityVO.get();
1✔
522
        //merge with override properties
523
        if (additionalPropertyVOMap != null) {
1✔
UNCOV
524
            entityVO.getAdditionalProperties().putAll(additionalPropertyVOMap);
×
525
        }
526
        return fromEntityVO(entityVO, targetClass);
1✔
527
    }
528

529
    /**
530
     * Return the type of the setter's parameter.
531
     */
532
    private Class<?> getParameterType(Class<?>[] arrayOfClasses) {
533
        if (arrayOfClasses.length != 1) {
1✔
UNCOV
534
            throw new MappingException("Setter method should only have one parameter declared.");
×
535
        }
536
        return arrayOfClasses[0];
1✔
537
    }
538

539
    /**
540
     * Get the setter method for the given property at the entity.
541
     */
542
    private <T> Optional<Method> getCorrespondingSetterMethod(T entity, String propertyName) {
543
        return getAttributeSettersMethods(entity).stream().filter(m ->
1✔
544
                        getAttributeSetterAnnotation(m)
1✔
545
                                .map(attributeSetter -> attributeSetter.targetName().equals(propertyName)).orElse(false))
1✔
546
                .findFirst();
1✔
547
    }
548

549
    /**
550
     * Get all attribute setters for the given entity
551
     */
552
    private <T> List<Method> getAttributeSettersMethods(T entity) {
553
        return Arrays.stream(entity.getClass().getMethods()).filter(m -> getAttributeSetterAnnotation(m).isPresent()).toList();
1✔
554
    }
555

556
    private <T> Map<String, Method> getAttributeSetterMethodMap(T entity) {
557
        return Arrays.stream(entity.getClass().getMethods())
1✔
558
                .filter(m -> getAttributeSetterAnnotation(m).isPresent())
1✔
559
                .collect(Collectors.toMap(m -> getAttributeSetterAnnotation(m).get().targetName(), m -> m));
1✔
560
    }
561

562
    /**
563
     * Get the attribute setter annotation from the given method, if it exists.
564
     */
565
    private Optional<AttributeSetter> getAttributeSetterAnnotation(Method m) {
566
        return Arrays.stream(m.getAnnotations()).filter(AttributeSetter.class::isInstance)
1✔
567
                .findFirst()
1✔
568
                .map(AttributeSetter.class::cast);
1✔
569
    }
570

571
}
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