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

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

07 Mar 2024 11:52AM UTC coverage: 79.248% (+0.07%) from 79.18%
#166

Pull #49

web-flow
Merge branch 'main' into discriminate
Pull Request #49: extend discrimnation

5 of 6 new or added lines in 1 file covered. (83.33%)

2 existing lines in 1 file now uncovered.

485 of 612 relevant lines covered (79.25%)

0.79 hits per line

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

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

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

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

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

30
    private final MappingProperties mappingProperties;
31
    private final ObjectMapper objectMapper;
32
    private final EntitiesRepository entitiesRepository;
33

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

41
        this.objectMapper.registerModule(new SimpleModule().addDeserializer(GeoQueryVO.class,
1✔
42
                new GeoQueryDeserializer()));
43

44
        this.objectMapper.findAndRegisterModules();
1✔
45
    }
1✔
46

47
    /**
48
     * Method to convert a Java-Object to Map representation
49
     *
50
     * @param entity the entity to be converted
51
     * @return the converted map
52
     */
53
    public <T> Map<String, Object> convertEntityToMap(T entity) {
54
        return objectMapper.convertValue(entity, new TypeReference<>() {
1✔
55
        });
56
    }
57

58
    /**
59
     * Translate the given object into a Subscription.
60
     *
61
     * @param subscription the object representing the subscription
62
     * @param <T>          class of the subscription
63
     * @return the NGSI-LD subscription object
64
     */
65
    public <T> SubscriptionVO toSubscriptionVO(T subscription) {
66
        isMappingEnabled(subscription.getClass())
1✔
67
                .orElseThrow(() -> new UnsupportedOperationException(
1✔
68
                        String.format("Generic mapping to NGSI-LD subscriptions is not supported for object %s",
×
69
                                subscription)));
70

71
        SubscriptionVO subscriptionVO = objectMapper.convertValue(subscription, SubscriptionVO.class);
1✔
72
        subscriptionVO.setAtContext(mappingProperties.getContextUrl());
1✔
73

74
        return subscriptionVO;
1✔
75
    }
76

77
    /**
78
     * 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
79
     *
80
     * @param entityVO    the NGSI-LD entity to be mapped
81
     * @param targetClass class of the target object
82
     * @param <T>         generic type of the target object, has to extend provide a string-constructor to receive the entity id
83
     * @return the mapped object
84
     */
85
    public <T> Mono<T> fromEntityVO(EntityVO entityVO, Class<T> targetClass) {
86

87
        Optional<MappingEnabled> optionalMappingEnabled = isMappingEnabled(targetClass);
1✔
88
        if (!optionalMappingEnabled.isPresent()) {
1✔
89
            return Mono.error(new MappingException(String.format("Mapping is not enabled for class %s", targetClass)));
1✔
90
        }
91

92
        MappingEnabled mappingEnabled = optionalMappingEnabled.get();
1✔
93

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

99
        return getRelationshipMap(additionalPropertyVOMap, targetClass)
1✔
100
                .flatMap(relationshipMap -> fromEntityVO(entityVO, targetClass, relationshipMap));
1✔
101

102
    }
103

104
    /**
105
     * Return a single, emitting the entities associated with relationships in the given properties maps
106
     *
107
     * @param propertiesMap properties map to evaluate
108
     * @param targetClass   class of the target object
109
     * @param <T>           the class
110
     * @return a single, emitting the map of related entities
111
     */
112
    private <T> Mono<Map<String, EntityVO>> getRelationshipMap(Map<String, AdditionalPropertyVO> propertiesMap, Class<T> targetClass) {
113
        return Optional.ofNullable(entitiesRepository.getEntities(getRelationshipObjects(propertiesMap, targetClass)))
1✔
114
                .orElse(Mono.just(List.of()))
1✔
115
                .switchIfEmpty(Mono.just(List.of()))
1✔
116
                .map(relationshipsList -> relationshipsList.stream().map(EntityVO.class::cast).collect(Collectors.toMap(e -> e.getId().toString(), e -> e)))
1✔
117
                .defaultIfEmpty(Map.of());
1✔
118
    }
119

120
    /**
121
     * Create the actual object from the entity, after its relations are evaluated.
122
     *
123
     * @param entityVO        entity to create the object from
124
     * @param targetClass     class of the object to be created
125
     * @param relationShipMap all entities (directly) related to the objects. Sub relationships(e.g. relationships of properties) will be evaluated downstream.
126
     * @param <T>             the class
127
     * @return a single, emitting the actual object.
128
     */
129
    private <T> Mono<T> fromEntityVO(EntityVO entityVO, Class<T> targetClass, Map<String, EntityVO> relationShipMap) {
130
        try {
131
            Constructor<T> objectConstructor = targetClass.getDeclaredConstructor(String.class);
1✔
132
            T constructedObject = objectConstructor.newInstance(entityVO.getId().toString());
1✔
133

134
            // handle "well-known" properties
135
            Map<String, AdditionalPropertyVO> propertiesMap = new LinkedHashMap<>();
1✔
136
            propertiesMap.put(EntityVO.JSON_PROPERTY_LOCATION, entityVO.getLocation());
1✔
137
            propertiesMap.put(EntityVO.JSON_PROPERTY_OBSERVATION_SPACE, entityVO.getObservationSpace());
1✔
138
            propertiesMap.put(EntityVO.JSON_PROPERTY_OPERATION_SPACE, entityVO.getOperationSpace());
1✔
139
            propertiesMap.put(EntityVO.JSON_PROPERTY_CREATED_AT, propertyVOFromValue(entityVO.getCreatedAt()));
1✔
140
            propertiesMap.put(EntityVO.JSON_PROPERTY_MODIFIED_AT, propertyVOFromValue(entityVO.getModifiedAt()));
1✔
141
            Optional.ofNullable(entityVO.getAdditionalProperties()).ifPresent(propertiesMap::putAll);
1✔
142

143
            List<Mono<T>> singleInvocations = propertiesMap.entrySet().stream()
1✔
144
                    .map(entry -> getObjectInvocation(entry, constructedObject, relationShipMap, entityVO.getId().toString()))
1✔
145
                    .toList();
1✔
146

147
            return Mono.zip(singleInvocations, constructedObjects -> constructedObject);
1✔
148

149
        } catch (NoSuchMethodException e) {
1✔
150
            return Mono.error(new MappingException(String.format("The class %s does not declare the required String id constructor.", targetClass)));
1✔
151
        } catch (InvocationTargetException | InstantiationException | IllegalAccessException e) {
1✔
152
            return Mono.error(new MappingException(String.format("Was not able to create instance of %s.", targetClass), e));
1✔
153
        }
154
    }
155

156

157
    public NotificationVO readNotificationFromJSON(String json) throws JsonProcessingException {
158
        return objectMapper.readValue(json, NotificationVO.class);
1✔
159
    }
160

161
    /**
162
     * Helper method to create a propertyVO for well-known(thus flat) properties
163
     *
164
     * @param value the value to wrap
165
     * @return a propertyVO containing the value
166
     */
167
    private PropertyVO propertyVOFromValue(Object value) {
168
        PropertyVO propertyVO = new PropertyVO();
1✔
169
        propertyVO.setValue(value);
1✔
170
        return propertyVO;
1✔
171
    }
172

173
    /**
174
     * Get the invocation on the object to be constructed.
175
     *
176
     * @param entry                   additional properties entry
177
     * @param objectUnderConstruction the new object, to be filled with the values
178
     * @param relationShipMap         map of pre-evaluated relations
179
     * @param entityId                id of the entity
180
     * @param <T>                     class of the constructed object
181
     * @return single, emmiting the constructed object
182
     */
183
    private <T> Mono<T> getObjectInvocation(Map.Entry<String, AdditionalPropertyVO> entry, T objectUnderConstruction, Map<String, EntityVO> relationShipMap, String entityId) {
184
        Optional<Method> optionalSetter = getCorrespondingSetterMethod(objectUnderConstruction, entry.getKey());
1✔
185
        if (optionalSetter.isEmpty()) {
1✔
186
            log.warn("Ignoring property {} for entity {} since there is no mapping configured.", entry.getKey(), entityId);
1✔
187
            return Mono.just(objectUnderConstruction);
1✔
188
        }
189
        Method setterMethod = optionalSetter.get();
1✔
190
        Optional<AttributeSetter> optionalAttributeSetter = getAttributeSetterAnnotation(setterMethod);
1✔
191
        if (optionalAttributeSetter.isEmpty()) {
1✔
192
            log.warn("Ignoring property {} for entity {} since there is no attribute setter configured.", entry.getKey(), entityId);
×
193
            return Mono.just(objectUnderConstruction);
×
194
        }
195
        AttributeSetter setterAnnotation = optionalAttributeSetter.get();
1✔
196

197
        Class<?> parameterType = getParameterType(setterMethod.getParameterTypes());
1✔
198

199
        return switch (setterAnnotation.value()) {
1✔
200
            case PROPERTY, GEO_PROPERTY ->
201
                    handleProperty(entry.getValue(), objectUnderConstruction, optionalSetter.get(), parameterType);
1✔
202
            case PROPERTY_LIST ->
203
                    handlePropertyList(entry.getValue(), objectUnderConstruction, optionalSetter.get(), setterAnnotation);
1✔
204
            case RELATIONSHIP ->
205
                    handleRelationship(entry.getValue(), objectUnderConstruction, relationShipMap, optionalSetter.get(), setterAnnotation);
1✔
206
            case RELATIONSHIP_LIST ->
207
                    handleRelationshipList(entry.getValue(), objectUnderConstruction, relationShipMap, optionalSetter.get(), setterAnnotation);
1✔
208
            default ->
209
                    Mono.error(new MappingException(String.format("Received type %s is not supported.", setterAnnotation.value())));
×
210
        };
211
    }
212

213
    /**
214
     * Handle the evaluation of a property entry. Returns a single, emitting the target object, while invoking the property setting method.
215
     *
216
     * @param propertyValue           the value of the property
217
     * @param objectUnderConstruction the object under construction
218
     * @param setter                  the setter to be used for the property
219
     * @param parameterType           type of the property in the target object
220
     * @param <T>                     class of the object under construction
221
     * @return the single, emitting the objectUnderConstruction
222
     */
223
    private <T> Mono<T> handleProperty(AdditionalPropertyVO propertyValue, T objectUnderConstruction, Method setter, Class<?> parameterType) {
224
        if (propertyValue instanceof PropertyVO propertyVO)
1✔
225
            return invokeWithExceptionHandling(setter, objectUnderConstruction, objectMapper.convertValue(propertyVO.getValue(), parameterType));
1✔
226
        else {
227
            log.error("Mapping exception");
×
228
            return Mono.error(new MappingException(String.format("The attribute is not a valid property: %s ", propertyValue)));
×
229
        }
230
    }
231

232
    /**
233
     * Handle the evaluation of a property-list entry. Returns a single, emitting the target object, while invoking the property setting method.
234
     *
235
     * @param propertyListObject      the object containing the property-list
236
     * @param objectUnderConstruction the object under construction
237
     * @param setter                  the setter to be used for the property
238
     * @param <T>                     class of the object under construction
239
     * @return the single, emitting the objectUnderConstruction
240
     */
241
    private <T> Mono<T> handlePropertyList(AdditionalPropertyVO propertyListObject, T objectUnderConstruction, Method setter, AttributeSetter setterAnnotation) {
242
        if (propertyListObject instanceof PropertyListVO propertyVOS) {
1✔
243
            return invokeWithExceptionHandling(setter, objectUnderConstruction, propertyListToTargetClass(propertyVOS, setterAnnotation.targetClass()));
1✔
244
        } else if (propertyListObject instanceof PropertyVO propertyVO) {
1✔
245
            //we need special handling here, since we have no real property lists(see NGSI-LD issue)
246
            // TODO: remove as soon as ngsi-ld does properly support that.
247
            if (propertyVO.getValue() instanceof List propertyList) {
1✔
248
                return invokeWithExceptionHandling(setter, objectUnderConstruction, propertyList.stream()
1✔
249
                        .map(listValue -> objectMapper.convertValue(listValue, setterAnnotation.targetClass()))
1✔
250
                        .toList());
1✔
251
            }
252
            PropertyListVO propertyVOS = new PropertyListVO();
×
253
            propertyVOS.add(propertyVO);
×
254
            // in case of single element lists, they are returned as a flat property
255
            return invokeWithExceptionHandling(setter, objectUnderConstruction, propertyListToTargetClass(propertyVOS, setterAnnotation.targetClass()));
×
256
        } else {
257
            return Mono.error(new MappingException(String.format("The attribute is not a valid property list: %v ", propertyListObject)));
×
258
        }
259
    }
260

261
    /**
262
     * Handle the evaluation of a relationship-list entry. Returns a single, emitting the target object, while invoking the property setting method.
263
     *
264
     * @param attributeValue          the entry containing the relationship-list
265
     * @param objectUnderConstruction the object under construction
266
     * @param relationShipMap         a map containing the pre-evaluated relationships
267
     * @param setter                  the setter to be used for the property
268
     * @param setterAnnotation        attribute setter annotation on the method
269
     * @param <T>                     class of the objectUnderConstruction
270
     * @return the single, emitting the objectUnderConstruction
271
     */
272
    private <T> Mono<T> handleRelationshipList(AdditionalPropertyVO attributeValue, T objectUnderConstruction, Map<String, EntityVO> relationShipMap, Method setter, AttributeSetter setterAnnotation) {
273
        Class<?> targetClass = setterAnnotation.targetClass();
1✔
274
        if (setterAnnotation.fromProperties()) {
1✔
275
            Optional<RelationshipVO> optionalRelationshipVO = getRelationshipFromProperty(attributeValue);
1✔
276
            Optional<RelationshipListVO> optionalRelationshipListVO = getRelationshipListFromProperty(attributeValue);
1✔
277
            if (optionalRelationshipVO.isPresent()) {
1✔
NEW
278
                return relationshipFromProperties(optionalRelationshipVO.get(), targetClass)
×
UNCOV
279
                        .flatMap(relationship -> {
×
280
                            // we return the constructed object, since invoke most likely returns null, which is not allowed on mapper functions
281
                            // a list is created, since we have a relationship-list defined by the annotation
282
                            return invokeWithExceptionHandling(setter, objectUnderConstruction, List.of(relationship));
×
283
                        });
284
            } else if (optionalRelationshipListVO.isPresent()) {
1✔
285
                return Mono.zip(optionalRelationshipListVO.get().stream().map(relationshipVO -> relationshipFromProperties(relationshipVO, targetClass)).toList(),
1✔
286
                        oList -> Arrays.asList(oList).stream().map(targetClass::cast).toList()).flatMap(relationshipList -> {
1✔
287
                    // we return the constructed object, since invoke most likely returns null, which is not allowed on mapper functions
288
                    return invokeWithExceptionHandling(setter, objectUnderConstruction, relationshipList);
1✔
289
                });
290
            } else {
UNCOV
291
                return Mono.error(new MappingException(String.format("Value of the relationship %s is invalid.", attributeValue)));
×
292
            }
293
        } else {
294
            return relationshipListToTargetClass(attributeValue, targetClass, relationShipMap)
1✔
295
                    .flatMap(relatedEntities -> {
1✔
296
                        // we return the constructed object, since invoke most likely returns null, which is not allowed on mapper functions
297
                        return invokeWithExceptionHandling(setter, objectUnderConstruction, relatedEntities);
1✔
298
                    });
299
        }
300
    }
301

302
    /**
303
     * Handle the evaluation of a relationship entry. Returns a single, emitting the target object, while invoking the property setting method.
304
     *
305
     * @param relationShip            the object containing the relationship
306
     * @param objectUnderConstruction the object under construction
307
     * @param relationShipMap         a map containing the pre-evaluated relationships
308
     * @param setter                  the setter to be used for the property
309
     * @param setterAnnotation        attribute setter annotation on the method
310
     * @param <T>                     class of the objectUnderConstruction
311
     * @return the single, emitting the objectUnderConstruction
312
     */
313
    private <T> Mono<T> handleRelationship(AdditionalPropertyVO relationShip, T objectUnderConstruction, Map<String, EntityVO> relationShipMap, Method setter, AttributeSetter setterAnnotation) {
314
        Class<?> targetClass = setterAnnotation.targetClass();
1✔
315
        if (relationShip instanceof RelationshipVO relationshipVO) {
1✔
316
            if (setterAnnotation.fromProperties()) {
1✔
317
                return relationshipFromProperties(relationshipVO, targetClass)
1✔
318
                        .flatMap(relatedEntity -> {
1✔
319
                            // we return the constructed object, since invoke most likely returns null, which is not allowed on mapper functions
320
                            return invokeWithExceptionHandling(setter, objectUnderConstruction, relatedEntity);
1✔
321
                        });
322
            } else {
323
                return getObjectFromRelationship(relationshipVO, targetClass, relationShipMap, relationshipVO.getAdditionalProperties())
1✔
324
                        .flatMap(relatedEntity -> {
1✔
325
                            // we return the constructed object, since invoke most likely returns null, which is not allowed on mapper functions
326
                            return invokeWithExceptionHandling(setter, objectUnderConstruction, relatedEntity);
1✔
327
                        });
328
                // handle overwrites from property
329

330
            }
331
        } else {
332
            return Mono.error(new MappingException(String.format("Did not receive a valid relationship: %s", relationShip)));
×
333
        }
334
    }
335

336
    /**
337
     * Invoke the given method and handle potential exceptions.
338
     */
339
    private <T> Mono<T> invokeWithExceptionHandling(Method invocationMethod, T objectUnderConstruction, Object... invocationArgs) {
340
        try {
341
            invocationMethod.invoke(objectUnderConstruction, invocationArgs);
1✔
342
            return Mono.just(objectUnderConstruction);
1✔
343
        } catch (IllegalAccessException | InvocationTargetException | RuntimeException e) {
×
344
            return Mono.error(new MappingException(String.format("Was not able to invoke method %s.", invocationMethod.getName()), e));
×
345
        }
346
    }
347

348
    /**
349
     * Create the target object of a relationship from its properties(instead of entities additionally retrieved)
350
     *
351
     * @param relationshipVO representation of the current relationship(as provided by the original entitiy)
352
     * @param targetClass    class of the target object to be created(e.g. the object representing the relationship)
353
     * @param <T>            the class
354
     * @return a single emitting the object representing the relationship
355
     */
356
    private <T> Mono<T> relationshipFromProperties(RelationshipVO relationshipVO, Class<T> targetClass) {
357
        try {
358
            String entityID = relationshipVO.getObject().toString();
1✔
359

360
            Constructor<T> objectConstructor = targetClass.getDeclaredConstructor(String.class);
1✔
361
            T constructedObject = objectConstructor.newInstance(entityID);
1✔
362

363
            Map<String, Method> attributeSetters = getAttributeSetterMethodMap(constructedObject);
1✔
364

365
            return Mono.zip(attributeSetters.entrySet().stream()
1✔
366
                    .map(methodEntry -> {
1✔
367
                        String field = methodEntry.getKey();
1✔
368
                        Method setterMethod = methodEntry.getValue();
1✔
369
                        Optional<AttributeSetter> optionalAttributeSetterAnnotation = getAttributeSetterAnnotation(setterMethod);
1✔
370
                        if (optionalAttributeSetterAnnotation.isEmpty()) {
1✔
371
                            // no setter for the field, can be ignored
372
                            log.debug("No setter defined for field {}", field);
×
373
                            return Mono.just(constructedObject);
×
374
                        }
375
                        AttributeSetter setterAnnotation = optionalAttributeSetterAnnotation.get();
1✔
376

377
                        Optional<AdditionalPropertyVO> optionalProperty = switch (methodEntry.getKey()) {
1✔
378
                            case RelationshipVO.JSON_PROPERTY_OBSERVED_AT ->
379
                                    Optional.ofNullable(relationshipVO.getObservedAt()).map(this::propertyVOFromValue);
1✔
380
                            case RelationshipVO.JSON_PROPERTY_CREATED_AT ->
381
                                    Optional.ofNullable(relationshipVO.getCreatedAt()).map(this::propertyVOFromValue);
1✔
382
                            case RelationshipVO.JSON_PROPERTY_MODIFIED_AT ->
383
                                    Optional.ofNullable(relationshipVO.getModifiedAt()).map(this::propertyVOFromValue);
1✔
384
                            case RelationshipVO.JSON_PROPERTY_DATASET_ID ->
385
                                    Optional.ofNullable(relationshipVO.getDatasetId()).map(this::propertyVOFromValue);
1✔
386
                            case RelationshipVO.JSON_PROPERTY_INSTANCE_ID ->
387
                                    Optional.ofNullable(relationshipVO.getInstanceId()).map(this::propertyVOFromValue);
1✔
388
                            default -> Optional.empty();
1✔
389
                        };
390

391
                        // try to find the attribute from the additional properties
392
                        if (optionalProperty.isEmpty() && relationshipVO.getAdditionalProperties() != null && relationshipVO.getAdditionalProperties().containsKey(field)) {
1✔
393
                            optionalProperty = Optional.ofNullable(relationshipVO.getAdditionalProperties().get(field));
1✔
394
                        }
395

396
                        return optionalProperty.map(attributeValue -> {
1✔
397
                            return switch (setterAnnotation.value()) {
1✔
398
                                case PROPERTY, GEO_PROPERTY ->
399
                                        handleProperty(attributeValue, constructedObject, setterMethod, setterAnnotation.targetClass());
1✔
400
                                case RELATIONSHIP ->
401
                                        getRelationshipMap(relationshipVO.getAdditionalProperties(), targetClass)
×
402
                                                .map(rm -> handleRelationship(attributeValue, constructedObject, rm, setterMethod, setterAnnotation));
×
403
                                //resolve objects;
404
                                case RELATIONSHIP_LIST ->
405
                                        getRelationshipMap(relationshipVO.getAdditionalProperties(), targetClass)
×
406
                                                .map(rm -> handleRelationshipList(attributeValue, constructedObject, rm, setterMethod, setterAnnotation));
×
407
                                case PROPERTY_LIST ->
408
                                        handlePropertyList(attributeValue, constructedObject, setterMethod, setterAnnotation);
×
409
                                default ->
410
                                        Mono.error(new MappingException(String.format("Received type %s is not supported.", setterAnnotation.value())));
×
411
                            };
412
                        }).orElse(Mono.just(constructedObject));
1✔
413

414
                    }).toList(), constructedObjects -> constructedObject);
1✔
415

416
        } catch (NoSuchMethodException e) {
×
417
            return Mono.error(new MappingException(String.format("The class %s does not declare the required String id constructor.", targetClass)));
×
418
        } catch (InvocationTargetException | InstantiationException | IllegalAccessException e) {
×
419
            return Mono.error(new MappingException(String.format("Was not able to create instance of %s.", targetClass), e));
×
420
        }
421
    }
422

423
    /**
424
     * Returns a list of all entityIDs that are defined as relationships from the given entity.
425
     *
426
     * @param additionalProperties map of the properties to evaluate
427
     * @param targetClass          target class of the mapping
428
     * @param <T>                  the class
429
     * @return a list of uris
430
     */
431
    private <T> List<URI> getRelationshipObjects(Map<String, AdditionalPropertyVO> additionalProperties, Class<T> targetClass) {
432
        return Arrays.stream(targetClass.getMethods())
1✔
433
                .map(this::getAttributeSetterAnnotation)
1✔
434
                .filter(Optional::isPresent)
1✔
435
                .map(Optional::get)
1✔
436
                .filter(a -> (a.value().equals(AttributeType.RELATIONSHIP) || a.value().equals(AttributeType.RELATIONSHIP_LIST)))
1✔
437
                // we don't need to retrieve entities that should be filled from the properties.
438
                .filter(a -> !a.fromProperties())
1✔
439
                .flatMap(attributeSetter -> getEntityURIsByAttributeSetter(attributeSetter, additionalProperties).stream())
1✔
440
                .toList();
1✔
441

442
    }
443

444
    /**
445
     * Evaluate a properties map to get all referenced entity ids
446
     *
447
     * @param attributeSetter the attribute setter annotation
448
     * @param propertiesMap   the properties map to check
449
     * @return a list of entity ids
450
     */
451
    private List<URI> getEntityURIsByAttributeSetter(AttributeSetter attributeSetter, Map<String, AdditionalPropertyVO> propertiesMap) {
452
        return Optional.ofNullable(propertiesMap.get(attributeSetter.targetName()))
1✔
453
                .map(this::getURIsFromRelationshipObject)
1✔
454
                .orElseGet(List::of);
1✔
455
    }
456

457
    /**
458
     * Evaluate a concrete object of a realitonship. If its a list of objects, get the ids of all entities.
459
     *
460
     * @param additionalPropertyVO the object to evaluate
461
     * @return a list of all referenced ids
462
     */
463
    private List<URI> getURIsFromRelationshipObject(AdditionalPropertyVO additionalPropertyVO) {
464
        Optional<RelationshipVO> optionalRelationshipVO = getRelationshipFromProperty(additionalPropertyVO);
1✔
465
        if (optionalRelationshipVO.isPresent()) {
1✔
466
            // List.of() cannot be used, since we need a mutable list
467
            List<URI> uriList = new ArrayList<>();
1✔
468
            uriList.add(optionalRelationshipVO.get().getObject());
1✔
469
            return uriList;
1✔
470
        }
471

472
        Optional<RelationshipListVO> optionalRelationshipListVO = getRelationshipListFromProperty(additionalPropertyVO);
1✔
473
        if (optionalRelationshipListVO.isPresent()) {
1✔
474
            return optionalRelationshipListVO.get().stream().flatMap(listEntry -> getURIsFromRelationshipObject(listEntry).stream()).toList();
1✔
475
        }
476
        return List.of();
×
477
    }
478

479
    /**
480
     * Method to translate a Map-Entry(e.g. NGSI-LD relationship) to a typed list as defined by the target object
481
     *
482
     * @param entry       attribute of the entity, e.g. a relationship or a list of relationships
483
     * @param targetClass class to be used as type for the typed list
484
     * @param <T>         the type
485
     * @return a list of objects, mapping the relationship
486
     */
487
    private <T> Mono<List<T>> relationshipListToTargetClass(AdditionalPropertyVO entry, Class<T> targetClass, Map<String, EntityVO> relationShipEntitiesMap) {
488

489
        Optional<RelationshipVO> optionalRelationshipVO = getRelationshipFromProperty(entry);
1✔
490
        if (optionalRelationshipVO.isPresent()) {
1✔
491
            return getObjectFromRelationship(optionalRelationshipVO.get(), targetClass, relationShipEntitiesMap, optionalRelationshipVO.get().getAdditionalProperties())
×
492
                    .map(List::of);
×
493
        }
494

495
        Optional<RelationshipListVO> optionalRelationshipListVO = getRelationshipListFromProperty(entry);
1✔
496
        if (optionalRelationshipListVO.isPresent()) {
1✔
497
            return zipToList(optionalRelationshipListVO.get().stream(), targetClass, relationShipEntitiesMap);
1✔
498
        }
499
        return Mono.error(new MappingException(String.format("Did not receive a valid entry: %s", entry)));
×
500
    }
501

502
    private Optional<RelationshipListVO> getRelationshipListFromProperty(AdditionalPropertyVO additionalPropertyVO) {
503
        if (additionalPropertyVO instanceof RelationshipListVO rsl) {
1✔
504
            return Optional.of(rsl);
1✔
505
        } else if (additionalPropertyVO instanceof PropertyListVO propertyListVO && !propertyListVO.isEmpty() && isRelationship(propertyListVO.get(0))) {
×
506
            RelationshipListVO relationshipListVO = new RelationshipListVO();
×
507
            propertyListVO.stream()
×
508
                    .map(propertyVO -> objectMapper.convertValue(propertyVO.getValue(), RelationshipVO.class))
×
509
                    .forEach(relationshipListVO::add);
×
510
            return Optional.of(relationshipListVO);
×
511
        }
512
        return Optional.empty();
×
513
    }
514

515
    private Optional<RelationshipVO> getRelationshipFromProperty(AdditionalPropertyVO additionalPropertyVO) {
516
        if (additionalPropertyVO instanceof RelationshipVO relationshipVO) {
1✔
517
            return Optional.of(relationshipVO);
1✔
518
        } else if (additionalPropertyVO instanceof PropertyVO propertyVO && isRelationship(propertyVO)) {
1✔
519
            return Optional.of(objectMapper.convertValue(propertyVO.getValue(), RelationshipVO.class));
×
520
        }
521
        return Optional.empty();
1✔
522
    }
523

524
    private boolean isRelationship(PropertyVO testProperty) {
525
        return testProperty.getValue() instanceof Map<?, ?> valuesMap && valuesMap.get("type").equals(PropertyTypeVO.RELATIONSHIP.getValue());
×
526
    }
527

528
    /**
529
     * Helper method for combining the evaluation of relationship entities to a single result lits
530
     *
531
     * @param relationshipVOStream    the relationships to evaluate
532
     * @param targetClass             target class of the relationship object
533
     * @param relationShipEntitiesMap map of the preevaluated relationship entities
534
     * @param <T>                     target class of the relationship
535
     * @return a single emitting the full list
536
     */
537
    private <T> Mono<List<T>> zipToList(Stream<RelationshipVO> relationshipVOStream, Class<T> targetClass, Map<String, EntityVO> relationShipEntitiesMap) {
538
        return Mono.zip(
1✔
539
                relationshipVOStream.map(RelationshipVO::getObject)
1✔
540
                        .filter(Objects::nonNull)
1✔
541
                        .map(URI::toString)
1✔
542
                        .map(relationShipEntitiesMap::get)
1✔
543
                        .map(entity -> fromEntityVO(entity, targetClass))
1✔
544
                        .toList(),
1✔
545
                oList -> Arrays.stream(oList).map(targetClass::cast).toList()
1✔
546
        );
547
    }
548

549
    /**
550
     * Method to translate a Map-Entry(e.g. NGSI-LD property) to a typed list as defined by the target object
551
     *
552
     * @param propertyVOS a list of properties
553
     * @param targetClass class to be used as type for the typed list
554
     * @param <T>         the type
555
     * @return a list of objects, mapping the relationship
556
     */
557
    private <T> List<T> propertyListToTargetClass(PropertyListVO propertyVOS, Class<T> targetClass) {
558
        return propertyVOS.stream().map(propertyEntry -> objectMapper.convertValue(propertyEntry.getValue(), targetClass)).toList();
1✔
559
    }
560

561
    /**
562
     * Retrieve the object from a relationship and return it as a java object of class T. All sub relationships will be evaluated, too.
563
     *
564
     * @param relationshipVO the relationship entry
565
     * @param targetClass    the target-class of the entry
566
     * @param <T>            the class
567
     * @return the actual object
568
     */
569
    private <T> Mono<T> getObjectFromRelationship(RelationshipVO relationshipVO, Class<T> targetClass, Map<String, EntityVO> relationShipEntitiesMap, Map<String, AdditionalPropertyVO> additionalPropertyVOMap) {
570
        Optional<EntityVO> optionalEntityVO = Optional.ofNullable(relationShipEntitiesMap.get(relationshipVO.getObject().toString()));
1✔
571
        if (optionalEntityVO.isEmpty() && !mappingProperties.isStrictRelationships()) {
1✔
572
            try {
573
                Constructor<T> objectConstructor = targetClass.getDeclaredConstructor(String.class);
1✔
574
                T theObject = objectConstructor.newInstance(relationshipVO.getObject().toString());
1✔
575
                // return the empty object
576
                return Mono.just(theObject);
1✔
577
            } catch (InvocationTargetException | InstantiationException | IllegalAccessException |
×
578
                     NoSuchMethodException e) {
579
                return Mono.error(new MappingException(String.format("Was not able to instantiate %s with a string parameter.", targetClass), e));
×
580
            }
581
        } else if (optionalEntityVO.isEmpty()) {
1✔
582
            return Mono.error(new MappingException(String.format("Was not able to resolve the relationship %s", relationshipVO.getObject())));
1✔
583
        }
584

585
        var entityVO = optionalEntityVO.get();
1✔
586
        //merge with override properties
587
        if (additionalPropertyVOMap != null) {
1✔
588
            entityVO.getAdditionalProperties().putAll(additionalPropertyVOMap);
×
589
        }
590
        return fromEntityVO(entityVO, targetClass);
1✔
591
    }
592

593
    /**
594
     * Return the type of the setter's parameter.
595
     */
596
    private Class<?> getParameterType(Class<?>[] arrayOfClasses) {
597
        if (arrayOfClasses.length != 1) {
1✔
598
            throw new MappingException("Setter method should only have one parameter declared.");
×
599
        }
600
        return arrayOfClasses[0];
1✔
601
    }
602

603
    /**
604
     * Get the setter method for the given property at the entity.
605
     */
606
    private <T> Optional<Method> getCorrespondingSetterMethod(T entity, String propertyName) {
607
        return getAttributeSettersMethods(entity).stream().filter(m ->
1✔
608
                        getAttributeSetterAnnotation(m)
1✔
609
                                .map(attributeSetter -> attributeSetter.targetName().equals(propertyName)).orElse(false))
1✔
610
                .findFirst();
1✔
611
    }
612

613
    /**
614
     * Get all attribute setters for the given entity
615
     */
616
    private <T> List<Method> getAttributeSettersMethods(T entity) {
617
        return Arrays.stream(entity.getClass().getMethods()).filter(m -> getAttributeSetterAnnotation(m).isPresent()).toList();
1✔
618
    }
619

620
    private <T> Map<String, Method> getAttributeSetterMethodMap(T entity) {
621
        return Arrays.stream(entity.getClass().getMethods())
1✔
622
                .filter(m -> getAttributeSetterAnnotation(m).isPresent())
1✔
623
                .collect(Collectors.toMap(m -> getAttributeSetterAnnotation(m).get().targetName(), m -> m));
1✔
624
    }
625

626
    /**
627
     * Get the attribute setter annotation from the given method, if it exists.
628
     */
629
    private Optional<AttributeSetter> getAttributeSetterAnnotation(Method m) {
630
        return Arrays.stream(m.getAnnotations()).filter(AttributeSetter.class::isInstance)
1✔
631
                .findFirst()
1✔
632
                .map(AttributeSetter.class::cast);
1✔
633
    }
634

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