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

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

13 Sep 2023 08:24AM UTC coverage: 82.375% (-0.05%) from 82.42%
#124

Pull #37

beknazaresenbek
Updated api docs
Pull Request #37: Notifications

10 of 10 new or added lines in 2 files covered. (100.0%)

444 of 539 relevant lines covered (82.37%)

0.82 hits per line

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

82.83
/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.databind.DeserializationFeature;
5
import com.fasterxml.jackson.databind.ObjectMapper;
6
import io.github.wistefan.mapping.annotations.AttributeSetter;
7
import lombok.extern.slf4j.Slf4j;
8
import org.fiware.ngsi.model.*;
9
import io.github.wistefan.mapping.annotations.AttributeType;
10
import io.github.wistefan.mapping.annotations.MappingEnabled;
11
import reactor.core.publisher.Mono;
12

13
import javax.inject.Singleton;
14
import java.lang.reflect.Constructor;
15
import java.lang.reflect.InvocationTargetException;
16
import java.lang.reflect.Method;
17
import java.net.URI;
18
import java.util.ArrayList;
19
import java.util.Arrays;
20
import java.util.LinkedHashMap;
21
import java.util.List;
22
import java.util.Map;
23
import java.util.Objects;
24
import java.util.Optional;
25
import java.util.stream.Collectors;
26
import java.util.stream.Stream;
27

28
/**
29
 * Mapper to handle translation from NGSI-LD entities to Java-Objects, based on annotations added to the target class
30
 */
31
@Slf4j
1✔
32
@Singleton
33
public class EntityVOMapper extends Mapper {
34

35
    private final ObjectMapper objectMapper;
36
    private final EntitiesRepository entitiesRepository;
37

38
    public EntityVOMapper(ObjectMapper objectMapper, EntitiesRepository entitiesRepository) {
1✔
39
        this.objectMapper = objectMapper;
1✔
40
        this.entitiesRepository = entitiesRepository;
1✔
41
        this.objectMapper
1✔
42
                .addMixIn(AdditionalPropertyVO.class, AdditionalPropertyMixin.class);
1✔
43
        this.objectMapper.addMixIn(SubscriptionVO.class, SubscriptionMixin.class);
1✔
44
        this.objectMapper.findAndRegisterModules();
1✔
45
    }
1✔
46

47
    public NotificationVO readNotificationFromJSON(String json) throws JsonProcessingException {
48
        return objectMapper.readValue(json, NotificationVO.class);
1✔
49
    }
50

51
    public <T> SubscriptionVO toSubscriptionVO(T subscription) {
52
        isMappingEnabled(subscription.getClass())
1✔
53
                .orElseThrow(() -> new UnsupportedOperationException(
1✔
54
                        String.format("Generic mapping to NGSI-LD subscriptions is not supported for object %s",
×
55
                                subscription)));
56

57
        SubscriptionVO subscriptionVO = objectMapper.convertValue(subscription, SubscriptionVO.class);
1✔
58
        subscriptionVO.setAtContext(JavaObjectMapper.DEFAULT_CONTEXT);
1✔
59
        subscriptionVO.setGeoQ(null);
1✔
60

61
        return subscriptionVO;
1✔
62
    }
63

64
    /**
65
     * 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
66
     *
67
     * @param entityVO    the NGSI-LD entity to be mapped
68
     * @param targetClass class of the target object
69
     * @param <T>         generic type of the target object, has to extend provide a string-constructor to receive the entity id
70
     * @return the mapped object
71
     */
72
    public <T> Mono<T> fromEntityVO(EntityVO entityVO, Class<T> targetClass) {
73

74
        Optional<MappingEnabled> optionalMappingEnabled = isMappingEnabled(targetClass);
1✔
75
        if (!optionalMappingEnabled.isPresent()) {
1✔
76
            return Mono.error(new MappingException(String.format("Mapping is not enabled for class %s", targetClass)));
1✔
77
        }
78

79
        MappingEnabled mappingEnabled = optionalMappingEnabled.get();
1✔
80

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

86
        return getRelationshipMap(additionalPropertyVOMap, targetClass)
1✔
87
                .flatMap(relationshipMap -> fromEntityVO(entityVO, targetClass, relationshipMap));
1✔
88

89
    }
90

91
    /**
92
     * Return a single, emitting the enitites associated with relationships in the given properties maps
93
     *
94
     * @param propertiesMap properties map to evaluate
95
     * @param targetClass   class of the target object
96
     * @param <T>           the class
97
     * @return a single, emitting the map of related entities
98
     */
99
    private <T> Mono<Map<String, EntityVO>> getRelationshipMap(Map<String, AdditionalPropertyVO> propertiesMap, Class<T> targetClass) {
100
        return Optional.ofNullable(entitiesRepository.getEntities(getRelationshipObjects(propertiesMap, targetClass)))
1✔
101
                .orElse(Mono.just(List.of()))
1✔
102
                .switchIfEmpty(Mono.just(List.of()))
1✔
103
                .map(relationshipsList -> relationshipsList.stream().map(EntityVO.class::cast).collect(Collectors.toMap(e -> e.getId().toString(), e -> e)))
1✔
104
                .defaultIfEmpty(Map.of());
1✔
105
    }
106

107
    /**
108
     * Create the actual object from the entity, after its relations are evaluated.
109
     *
110
     * @param entityVO        entity to create the object from
111
     * @param targetClass     class of the object to be created
112
     * @param relationShipMap all entities (directly) related to the objects. Sub relationships(e.g. relationships of properties) will be evaluated downstream.
113
     * @param <T>             the class
114
     * @return a single, emitting the actual object.
115
     */
116
    private <T> Mono<T> fromEntityVO(EntityVO entityVO, Class<T> targetClass, Map<String, EntityVO> relationShipMap) {
117
        try {
118
            Constructor<T> objectConstructor = targetClass.getDeclaredConstructor(String.class);
1✔
119
            T constructedObject = objectConstructor.newInstance(entityVO.getId().toString());
1✔
120

121
            // handle "well-known" properties
122
            Map<String, AdditionalPropertyVO> propertiesMap = new LinkedHashMap<>();
1✔
123
            propertiesMap.put(EntityVO.JSON_PROPERTY_LOCATION, entityVO.getLocation());
1✔
124
            propertiesMap.put(EntityVO.JSON_PROPERTY_OBSERVATION_SPACE, entityVO.getObservationSpace());
1✔
125
            propertiesMap.put(EntityVO.JSON_PROPERTY_OPERATION_SPACE, entityVO.getOperationSpace());
1✔
126
            propertiesMap.put(EntityVO.JSON_PROPERTY_CREATED_AT, propertyVOFromValue(entityVO.getCreatedAt()));
1✔
127
            propertiesMap.put(EntityVO.JSON_PROPERTY_MODIFIED_AT, propertyVOFromValue(entityVO.getModifiedAt()));
1✔
128
            Optional.ofNullable(entityVO.getAdditionalProperties()).ifPresent(propertiesMap::putAll);
1✔
129

130
            List<Mono<T>> singleInvocations = propertiesMap.entrySet().stream()
1✔
131
                    .map(entry -> getObjectInvocation(entry, constructedObject, relationShipMap, entityVO.getId().toString()))
1✔
132
                    .toList();
1✔
133

134
            return Mono.zip(singleInvocations, constructedObjects -> constructedObject);
1✔
135

136
        } catch (NoSuchMethodException e) {
1✔
137
            return Mono.error(new MappingException(String.format("The class %s does not declare the required String id constructor.", targetClass)));
1✔
138
        } catch (InvocationTargetException | InstantiationException | IllegalAccessException e) {
1✔
139
            return Mono.error(new MappingException(String.format("Was not able to create instance of %s.", targetClass), e));
1✔
140
        }
141
    }
142

143
    /**
144
     * Helper method to create a propertyVO for well-known(thus flat) properties
145
     *
146
     * @param value the value to wrap
147
     * @return a propertyVO containing the value
148
     */
149
    private PropertyVO propertyVOFromValue(Object value) {
150
        PropertyVO propertyVO = new PropertyVO();
1✔
151
        propertyVO.setValue(value);
1✔
152
        return propertyVO;
1✔
153
    }
154

155
    /**
156
     * Get the invocation on the object to be constructed.
157
     *
158
     * @param entry                   additional properties entry
159
     * @param objectUnderConstruction the new object, to be filled with the values
160
     * @param relationShipMap         map of preevaluated realtions
161
     * @param entityId                id of the entity
162
     * @param <T>                     class of the constructed object
163
     * @return single, emmiting the constructed object
164
     */
165
    private <T> Mono<T> getObjectInvocation(Map.Entry<String, AdditionalPropertyVO> entry, T objectUnderConstruction, Map<String, EntityVO> relationShipMap, String entityId) {
166
        Optional<Method> optionalSetter = getCorrespondingSetterMethod(objectUnderConstruction, entry.getKey());
1✔
167
        if (optionalSetter.isEmpty()) {
1✔
168
            log.warn("Ignoring property {} for entity {} since there is no mapping configured.", entry.getKey(), entityId);
1✔
169
            return Mono.just(objectUnderConstruction);
1✔
170
        }
171
        Method setterMethod = optionalSetter.get();
1✔
172
        Optional<AttributeSetter> optionalAttributeSetter = getAttributeSetterAnnotation(setterMethod);
1✔
173
        if (optionalAttributeSetter.isEmpty()) {
1✔
174
            log.warn("Ignoring property {} for entity {} since there is no attribute setter configured.", entry.getKey(), entityId);
×
175
            return Mono.just(objectUnderConstruction);
×
176
        }
177
        AttributeSetter setterAnnotation = optionalAttributeSetter.get();
1✔
178

179
        Class<?> parameterType = getParameterType(setterMethod.getParameterTypes());
1✔
180

181
        return switch (setterAnnotation.value()) {
1✔
182
            case PROPERTY, GEO_PROPERTY -> handleProperty(entry.getValue(), objectUnderConstruction, optionalSetter.get(), parameterType);
1✔
183
            case PROPERTY_LIST -> handlePropertyList(entry.getValue(), objectUnderConstruction, optionalSetter.get(), setterAnnotation);
1✔
184
            case RELATIONSHIP -> handleRelationship(entry.getValue(), objectUnderConstruction, relationShipMap, optionalSetter.get(), setterAnnotation);
1✔
185
            case RELATIONSHIP_LIST -> handleRelationshipList(entry.getValue(), objectUnderConstruction, relationShipMap, optionalSetter.get(), setterAnnotation);
1✔
186
            default -> Mono.error(new MappingException(String.format("Received type %s is not supported.", setterAnnotation.value())));
×
187
        };
188
    }
189

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

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

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

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

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

306
            }
307
        } else {
308
            return Mono.error(new MappingException(String.format("Did not receive a valid relationship: %s", relationShip)));
×
309
        }
310
    }
311

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

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

336
            Constructor<T> objectConstructor = targetClass.getDeclaredConstructor(String.class);
1✔
337
            T constructedObject = objectConstructor.newInstance(entityID);
1✔
338

339
            Map<String, Method> attributeSetters = getAttributeSetterMethodMap(constructedObject);
1✔
340

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

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

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

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

379
                    }).toList(), constructedObjects -> constructedObject);
1✔
380

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

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

407
    }
408

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

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

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

454
        }
455
        return Mono.error(new MappingException(String.format("Did not receive a valid entry: %s", entry)));
×
456
    }
457

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

479
    /**
480
     * Method to translate a Map-Entry(e.g. NGSI-LD property) to a typed list as defined by the target object
481
     *
482
     * @param propertyVOS a list of properties
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> List<T> propertyListToTargetClass(PropertyListVO propertyVOS, Class<T> targetClass) {
488
        return propertyVOS.stream().map(propertyEntry -> objectMapper.convertValue(propertyEntry.getValue(), targetClass)).toList();
1✔
489
    }
490

491
    /**
492
     * Retrieve the object from a relationship and return it as a java object of class T. All sub relationships will be evaluated, too.
493
     *
494
     * @param relationshipVO the relationship entry
495
     * @param targetClass    the target-class of the entry
496
     * @param <T>            the class
497
     * @return the actual object
498
     */
499
    private <T> Mono<T> getObjectFromRelationship(RelationshipVO relationshipVO, Class<T> targetClass, Map<String, EntityVO> relationShipEntitiesMap, Map<String, AdditionalPropertyVO> additionalPropertyVOMap) {
500
        EntityVO entityVO = relationShipEntitiesMap.get(relationshipVO.getObject().toString());
1✔
501
        //merge with override properties
502
        if (additionalPropertyVOMap != null) {
1✔
503
            entityVO.getAdditionalProperties().putAll(additionalPropertyVOMap);
×
504
        }
505
        return fromEntityVO(entityVO, targetClass);
1✔
506

507
    }
508

509
    /**
510
     * Return the type of the setter's parameter.
511
     */
512
    private Class<?> getParameterType(Class<?>[] arrayOfClasses) {
513
        if (arrayOfClasses.length != 1) {
1✔
514
            throw new MappingException("Setter method should only have one parameter declared.");
×
515
        }
516
        return arrayOfClasses[0];
1✔
517
    }
518

519
    /**
520
     * Get the setter method for the given property at the entity.
521
     */
522
    private <T> Optional<Method> getCorrespondingSetterMethod(T entity, String propertyName) {
523
        return getAttributeSettersMethods(entity).stream().filter(m ->
1✔
524
                        getAttributeSetterAnnotation(m)
1✔
525
                                .map(attributeSetter -> attributeSetter.targetName().equals(propertyName)).orElse(false))
1✔
526
                .findFirst();
1✔
527
    }
528

529
    /**
530
     * Get all attribute setters for the given entity
531
     */
532
    private <T> List<Method> getAttributeSettersMethods(T entity) {
533
        return Arrays.stream(entity.getClass().getMethods()).filter(m -> getAttributeSetterAnnotation(m).isPresent()).toList();
1✔
534
    }
535

536
    private <T> Map<String, Method> getAttributeSetterMethodMap(T entity) {
537
        return Arrays.stream(entity.getClass().getMethods())
1✔
538
                .filter(m -> getAttributeSetterAnnotation(m).isPresent())
1✔
539
                .collect(Collectors.toMap(m -> getAttributeSetterAnnotation(m).get().targetName(), m -> m));
1✔
540
    }
541

542
    /**
543
     * Get the attribute setter annotation from the given method, if it exists.
544
     */
545
    private Optional<AttributeSetter> getAttributeSetterAnnotation(Method m) {
546
        return Arrays.stream(m.getAnnotations()).filter(AttributeSetter.class::isInstance)
1✔
547
                .findFirst()
1✔
548
                .map(AttributeSetter.class::cast);
1✔
549
    }
550

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