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

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

16 Jun 2025 09:19AM UTC coverage: 76.988% (-0.5%) from 77.455%
#286

push

web-flow
Merge pull request #83 from wistefan/fix-pl

Fix pl

0 of 6 new or added lines in 1 file covered. (0.0%)

1 existing line in 1 file now uncovered.

639 of 830 relevant lines covered (76.99%)

0.77 hits per line

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

73.58
/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 io.github.wistefan.mapping.annotations.UnmappedPropertiesSetter;
11
import lombok.extern.slf4j.Slf4j;
12
import org.fiware.ngsi.model.*;
13
import reactor.core.publisher.Mono;
14

15
import javax.inject.Singleton;
16
import java.lang.reflect.Constructor;
17
import java.lang.reflect.InvocationTargetException;
18
import java.lang.reflect.Method;
19
import java.net.URI;
20
import java.util.*;
21
import java.util.concurrent.ConcurrentHashMap;
22
import java.util.function.Function;
23
import java.util.function.Predicate;
24
import java.util.stream.Collectors;
25
import java.util.stream.Stream;
26

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

34
        private static final List<String> WELL_KNOWN_PROPERTIES = List.of(EntityVO.JSON_PROPERTY_MODIFIED_AT, EntityVO.JSON_PROPERTY_CREATED_AT, EntityVO.JSON_PROPERTY_LOCATION, EntityVO.JSON_PROPERTY_OBSERVATION_SPACE, EntityVO.JSON_PROPERTY_OPERATION_SPACE, EntityVO.JSON_PROPERTY_DELETED_AT);
1✔
35

36
        private final MappingProperties mappingProperties;
37
        private final ObjectMapper objectMapper;
38
        private final EntitiesRepository entitiesRepository;
39

40
        public EntityVOMapper(MappingProperties mappingProperties, ObjectMapper objectMapper, EntitiesRepository entitiesRepository) {
1✔
41
                this.mappingProperties = mappingProperties;
1✔
42
                this.objectMapper = objectMapper;
1✔
43
                this.entitiesRepository = entitiesRepository;
1✔
44
                this.objectMapper
1✔
45
                                .addMixIn(AdditionalPropertyVO.class, AdditionalPropertyMixin.class);
1✔
46

47
                this.objectMapper.registerModule(new SimpleModule().addDeserializer(GeoQueryVO.class,
1✔
48
                                new GeoQueryDeserializer()));
49

50
                this.objectMapper.findAndRegisterModules();
1✔
51
        }
1✔
52

53
        /**
54
         * Method to convert a Java-Object to Map representation
55
         *
56
         * @param entity the entity to be converted
57
         * @return the converted map
58
         */
59
        public <T> Map<String, Object> convertEntityToMap(T entity) {
60
                return objectMapper.convertValue(entity, new TypeReference<>() {
1✔
61
                });
62
        }
63

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

77
                SubscriptionVO subscriptionVO = objectMapper.convertValue(subscription, SubscriptionVO.class);
1✔
78
                subscriptionVO.setAtContext(mappingProperties.getContextUrl());
1✔
79

80
                return subscriptionVO;
1✔
81
        }
82

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

93
                Optional<MappingEnabled> optionalMappingEnabled = isMappingEnabled(targetClass);
1✔
94
                if (!optionalMappingEnabled.isPresent()) {
1✔
95
                        return Mono.error(new MappingException(String.format("Mapping is not enabled for class %s", targetClass)));
1✔
96
                }
97

98
                MappingEnabled mappingEnabled = optionalMappingEnabled.get();
1✔
99

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

105
                return getRelationshipMap(additionalPropertyVOMap, targetClass)
1✔
106
                                .flatMap(relationshipMap -> fromEntityVO(entityVO, targetClass, relationshipMap));
1✔
107

108
        }
109

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

129
        private static <T> Predicate<T> distinctByKey(Function<? super T, Object> keyExtractor) {
130
                Map<Object, Boolean> seen = new ConcurrentHashMap<>();
1✔
131
                return t -> seen.putIfAbsent(keyExtractor.apply(t), Boolean.TRUE) == null;
1✔
132
        }
133

134
        /**
135
         * Create the actual object from the entity, after its relations are evaluated.
136
         *
137
         * @param entityVO        entity to create the object from
138
         * @param targetClass     class of the object to be created
139
         * @param relationShipMap all entities (directly) related to the objects. Sub relationships(e.g. relationships of properties) will be evaluated downstream.
140
         * @param <T>             the class
141
         * @return a single, emitting the actual object.
142
         */
143
        private <T> Mono<T> fromEntityVO(EntityVO entityVO, Class<T> targetClass, Map<String, EntityVO> relationShipMap) {
144
                try {
145
                        Constructor<T> objectConstructor = targetClass.getDeclaredConstructor(String.class);
1✔
146
                        T constructedObject = objectConstructor.newInstance(entityVO.getId().toString());
1✔
147

148
                        // handle "well-known" properties
149
                        Map<String, AdditionalPropertyVO> propertiesMap = new LinkedHashMap<>();
1✔
150
                        propertiesMap.put(EntityVO.JSON_PROPERTY_LOCATION, entityVO.getLocation());
1✔
151
                        propertiesMap.put(EntityVO.JSON_PROPERTY_OBSERVATION_SPACE, entityVO.getObservationSpace());
1✔
152
                        propertiesMap.put(EntityVO.JSON_PROPERTY_OPERATION_SPACE, entityVO.getOperationSpace());
1✔
153
                        propertiesMap.put(EntityVO.JSON_PROPERTY_CREATED_AT, propertyVOFromValue(entityVO.getCreatedAt()));
1✔
154
                        propertiesMap.put(EntityVO.JSON_PROPERTY_MODIFIED_AT, propertyVOFromValue(entityVO.getModifiedAt()));
1✔
155
                        Optional.ofNullable(entityVO.getAdditionalProperties()).ifPresent(propertiesMap::putAll);
1✔
156

157

158
                        List<Mono<T>> singleInvocations = propertiesMap.entrySet().stream()
1✔
159
                                        .map(entry -> getObjectInvocation(entry, constructedObject, relationShipMap, entityVO.getId().toString()))
1✔
160
                                        .toList();
1✔
161

162
                        Optional<Method> unmappedPropertiesSetter = getUnmappedPropertiesSetter(constructedObject);
1✔
163
                        if (unmappedPropertiesSetter.isPresent()) {
1✔
164
                                List<Map.Entry<String, AdditionalPropertyVO>> unmappedProperties = propertiesMap.entrySet()
1✔
165
                                                .stream()
1✔
166
                                                .filter(entry -> getCorrespondingSetterMethod(constructedObject, entry.getKey()).isEmpty())
1✔
167
                                                .filter(entry -> !isWellKnownProperty(entry.getKey()))
1✔
168
                                                .toList();
1✔
169
                                singleInvocations = new ArrayList<>(singleInvocations);
1✔
170
                                singleInvocations.add(
1✔
171
                                                invokeWithExceptionHandling(unmappedPropertiesSetter.get(), constructedObject, toUnmappedProperties(unmappedProperties)));
1✔
172
                        }
173

174
                        return Mono.zip(singleInvocations, constructedObjects -> constructedObject);
1✔
175

176
                } catch (NoSuchMethodException e) {
1✔
177
                        return Mono.error(new MappingException(String.format("The class %s does not declare the required String id constructor.", targetClass)));
1✔
178
                } catch (InvocationTargetException | InstantiationException | IllegalAccessException e) {
1✔
179
                        return Mono.error(new MappingException(String.format("Was not able to create instance of %s.", targetClass), e));
1✔
180
                }
181
        }
182

183
        private boolean isWellKnownProperty(String propertyName) {
184
                return WELL_KNOWN_PROPERTIES.contains(propertyName);
1✔
185
        }
186

187
        public NotificationVO readNotificationFromJSON(String json) throws JsonProcessingException {
188
                return objectMapper.readValue(json, NotificationVO.class);
1✔
189
        }
190

191
        /**
192
         * Helper method to create a propertyVO for well-known(thus flat) properties
193
         *
194
         * @param value the value to wrap
195
         * @return a propertyVO containing the value
196
         */
197
        private PropertyVO propertyVOFromValue(Object value) {
198
                PropertyVO propertyVO = new PropertyVO();
1✔
199
                propertyVO.setValue(value);
1✔
200
                return propertyVO;
1✔
201
        }
202

203
        /**
204
         * Get the invocation on the object to be constructed.
205
         *
206
         * @param entry                   additional properties entry
207
         * @param objectUnderConstruction the new object, to be filled with the values
208
         * @param relationShipMap         map of pre-evaluated relations
209
         * @param entityId                id of the entity
210
         * @param <T>                     class of the constructed object
211
         * @return single, emmiting the constructed object
212
         */
213
        private <T> Mono<T> getObjectInvocation(Map.Entry<String, AdditionalPropertyVO> entry, T objectUnderConstruction, Map<String, EntityVO> relationShipMap, String entityId) {
214
                Optional<Method> optionalSetter = getCorrespondingSetterMethod(objectUnderConstruction, entry.getKey());
1✔
215
                if (optionalSetter.isEmpty()) {
1✔
216
                        log.debug("Ignoring property {} for entity {} since there is no mapping configured.", entry.getKey(), entityId);
1✔
217
                        return Mono.just(objectUnderConstruction);
1✔
218
                }
219
                Method setterMethod = optionalSetter.get();
1✔
220

221
                Optional<AttributeSetter> optionalAttributeSetter = getAttributeSetterAnnotation(setterMethod);
1✔
222
                if (optionalAttributeSetter.isEmpty()) {
1✔
223
                        log.debug("Ignoring property {} for entity {} since there is no attribute setter configured.", entry.getKey(), entityId);
×
224
                        return Mono.just(objectUnderConstruction);
×
225
                }
226
                AttributeSetter setterAnnotation = optionalAttributeSetter.get();
1✔
227

228
                Class<?> parameterType = getParameterType(setterMethod.getParameterTypes());
1✔
229

230
                return switch (setterAnnotation.value()) {
1✔
231
                        case PROPERTY, GEO_PROPERTY ->
232
                                        handleProperty(entry.getValue(), objectUnderConstruction, optionalSetter.get(), parameterType);
1✔
233
                        case PROPERTY_LIST ->
234
                                        handlePropertyList(entry.getValue(), objectUnderConstruction, optionalSetter.get(), setterAnnotation);
1✔
235
                        case RELATIONSHIP ->
236
                                        handleRelationship(entry.getValue(), objectUnderConstruction, relationShipMap, optionalSetter.get(), setterAnnotation);
1✔
237
                        case RELATIONSHIP_LIST ->
238
                                        handleRelationshipList(entry.getValue(), objectUnderConstruction, relationShipMap, optionalSetter.get(), setterAnnotation);
1✔
239
                        default ->
240
                                        Mono.error(new MappingException(String.format("Received type %s is not supported.", setterAnnotation.value())));
×
241
                };
242
        }
243

244
        private List<UnmappedProperty> toUnmappedProperties(List<Map.Entry<String, AdditionalPropertyVO>> unmappedAdditionalProperties) {
245
                return unmappedAdditionalProperties
1✔
246
                                .stream()
1✔
247
                                .map(this::toUnmappedProperty)
1✔
248
                                .toList();
1✔
249

250
        }
251

252
        private UnmappedProperty toUnmappedProperty(Map.Entry<String, AdditionalPropertyVO> unmappedAdditionalProperty) {
253
                UnmappedProperty unmappedProperty = new UnmappedProperty();
1✔
254
                unmappedProperty.setName(unmappedAdditionalProperty.getKey());
1✔
255

256
                if (unmappedAdditionalProperty.getValue() instanceof PropertyListVO propertyListVO) {
1✔
257
                        unmappedProperty.setValue(
×
258
                                        propertyListVO.stream()
×
259
                                                        .map(pvo -> fromProperty(unmappedAdditionalProperty.getKey(), pvo))
×
260
                                                        .map(Map.Entry::getValue)
×
261
                                                        .toList());
×
262
                } else if (unmappedAdditionalProperty.getValue() instanceof RelationshipListVO relationshipListVO) {
1✔
263
                        unmappedProperty.setValue(
×
264
                                        relationshipListVO.stream()
×
265
                                                        .map(rvo -> fromRelationship(unmappedAdditionalProperty.getKey(), rvo))
×
266
                                                        .map(Map.Entry::getValue)
×
267
                                                        .toList());
×
268
                } else if (unmappedAdditionalProperty.getValue() instanceof PropertyVO propertyVO) {
1✔
269
                        unmappedProperty.setValue(fromProperty(unmappedAdditionalProperty.getKey(), propertyVO).getValue());
1✔
270
                } else if (unmappedAdditionalProperty.getValue() instanceof RelationshipVO relationshipVO) {
1✔
271
                        unmappedProperty.setValue(fromRelationship(unmappedAdditionalProperty.getKey(), relationshipVO).getValue());
1✔
272
                }
273
                return unmappedProperty;
1✔
274
        }
275

276
        private Map.Entry<String, Object> fromRelationship(String key, RelationshipVO relationshipVO) {
277
                URI idValue = relationshipVO.getObject();
1✔
278
                if (relationshipVO.getAdditionalProperties() != null && !relationshipVO.getAdditionalProperties().isEmpty()) {
1✔
279
                        List<Map.Entry<String, Object>> entryList = new ArrayList<>(relationshipVO.getAdditionalProperties().entrySet()
1✔
280
                                        .stream()
1✔
281
                                        .map(entry -> {
1✔
282
                                                if (entry.getValue() instanceof PropertyVO pvo) {
1✔
283
                                                        return fromProperty(entry.getKey(), pvo);
1✔
284
                                                } else if (entry.getValue() instanceof RelationshipVO rvo) {
×
285
                                                        return fromRelationship(entry.getKey(), rvo);
×
286
                                                } else {
287
                                                        throw new MappingException(String.format("Entry value is not supported. Was: %s", entry.getValue()));
×
288
                                                }
289
                                        })
290
                                        .filter(entry -> entry.getValue() != null)
1✔
291
                                        .toList());
1✔
292
                        entryList.add(new AbstractMap.SimpleEntry<>("id", idValue.toString()));
1✔
293
                        return new AbstractMap.SimpleEntry<>(key, entryList.stream()
1✔
294
                                        .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)));
1✔
295
                } else {
296

297
                        return new AbstractMap.SimpleEntry<>(key, Map.of("id", idValue.toString()));
×
298
                }
299
        }
300

301
        private Map.Entry<String, Object> fromProperty(String key, PropertyVO propertyVO) {
302
                if (propertyVO.getAdditionalProperties() != null && !propertyVO.getAdditionalProperties().isEmpty()) {
1✔
303
                        return new AbstractMap.SimpleEntry<>(key, propertyVO.getAdditionalProperties().entrySet()
1✔
304
                                        .stream()
1✔
305
                                        .map(entry -> {
1✔
306
                                                if (entry.getValue() instanceof PropertyVO pvo) {
1✔
307
                                                        return fromProperty(entry.getKey(), pvo);
1✔
308
                                                } else if (entry.getValue() instanceof RelationshipVO rvo) {
×
309
                                                        return fromRelationship(entry.getKey(), rvo);
×
310
                                                } else {
311
                                                        throw new MappingException(String.format("Entry value is not supported. Was: %s", entry.getValue()));
×
312
                                                }
313
                                        })
314
                                        .filter(entry -> entry.getValue() != null)
1✔
315
                                        .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)));
1✔
316
                } else {
317
                        return new AbstractMap.SimpleEntry<>(key, propertyVO.getValue());
1✔
318
                }
319
        }
320

321
        /**
322
         * Handle the evaluation of a property entry. Returns a single, emitting the target object, while invoking the property setting method.
323
         *
324
         * @param propertyValue           the value of the property
325
         * @param objectUnderConstruction the object under construction
326
         * @param setter                  the setter to be used for the property
327
         * @param parameterType           type of the property in the target object
328
         * @param <T>                     class of the object under construction
329
         * @return the single, emitting the objectUnderConstruction
330
         */
331
        private <T> Mono<T> handleProperty(AdditionalPropertyVO propertyValue, T objectUnderConstruction, Method setter, Class<?> parameterType) {
332
                if (propertyValue instanceof PropertyVO propertyVO) {
1✔
333
                        return invokeWithExceptionHandling(setter, objectUnderConstruction, objectMapper.convertValue(propertyVO.getValue(), parameterType));
1✔
334
                } else if (propertyValue instanceof GeoPropertyVO geoPropertyVO) {
1✔
335
                        return invokeWithExceptionHandling(setter, objectUnderConstruction, objectMapper.convertValue(geoPropertyVO.getValue(), parameterType));
1✔
336
                } else {
337
                        log.error("Mapping exception");
×
338
                        return Mono.error(new MappingException(String.format("The attribute is not a valid property: %s ", propertyValue)));
×
339
                }
340
        }
341

342
        /**
343
         * Handle the evaluation of a property-list entry. Returns a single, emitting the target object, while invoking the property setting method.
344
         *
345
         * @param propertyListObject      the object containing the property-list
346
         * @param objectUnderConstruction the object under construction
347
         * @param setter                  the setter to be used for the property
348
         * @param <T>                     class of the object under construction
349
         * @return the single, emitting the objectUnderConstruction
350
         */
351
        private <T> Mono<T> handlePropertyList(AdditionalPropertyVO propertyListObject, T objectUnderConstruction, Method setter, AttributeSetter setterAnnotation) {
352
                if (propertyListObject instanceof PropertyListVO propertyVOS) {
1✔
353
                        return invokeWithExceptionHandling(setter, objectUnderConstruction, propertyListToTargetClass(propertyVOS, setterAnnotation.targetClass()));
1✔
354
                } else if (propertyListObject instanceof PropertyVO propertyVO) {
×
355
                        //we need special handling here, since we have no real property lists(see NGSI-LD issue)
356
                        // TODO: remove as soon as ngsi-ld does properly support that.
357
                        if (propertyVO.getValue() instanceof List propertyList) {
×
358
                                return invokeWithExceptionHandling(setter, objectUnderConstruction, propertyListToTargetClass(objectMapper.convertValue(propertyList, PropertyListVO.class), setterAnnotation.targetClass()));
×
359
                        }
360
                        PropertyListVO propertyVOS = new PropertyListVO();
×
NEW
361
                        Optional.ofNullable(propertyVO.getValue())
×
NEW
362
                                        .map(pvo -> {
×
NEW
363
                                                if (pvo instanceof PropertyVO pvoI) {
×
NEW
364
                                                        return pvoI;
×
365
                                                } else {
NEW
366
                                                        return propertyVO;
×
367
                                                }
368
                                        })
NEW
369
                                        .ifPresent(propertyVOS::add);
×
UNCOV
370
                        return invokeWithExceptionHandling(setter, objectUnderConstruction, propertyListToTargetClass(propertyVOS, setterAnnotation.targetClass()));
×
371
                        // in case of single element lists, they are returned as a flat property
372
                } else {
373
                        return Mono.error(new MappingException(String.format("The attribute is not a valid property list: %v ", propertyListObject)));
×
374
                }
375
        }
376

377
        /**
378
         * Handle the evaluation of a relationship-list entry. Returns a single, emitting the target object, while invoking the property setting method.
379
         *
380
         * @param attributeValue          the entry containing the relationship-list
381
         * @param objectUnderConstruction the object under construction
382
         * @param relationShipMap         a map containing the pre-evaluated relationships
383
         * @param setter                  the setter to be used for the property
384
         * @param setterAnnotation        attribute setter annotation on the method
385
         * @param <T>                     class of the objectUnderConstruction
386
         * @return the single, emitting the objectUnderConstruction
387
         */
388
        private <T> Mono<T> handleRelationshipList(AdditionalPropertyVO attributeValue, T objectUnderConstruction, Map<String, EntityVO> relationShipMap, Method setter, AttributeSetter setterAnnotation) {
389
                Class<?> targetClass = setterAnnotation.targetClass();
1✔
390
                if (setterAnnotation.fromProperties()) {
1✔
391
                        Optional<RelationshipVO> optionalRelationshipVO = getRelationshipFromProperty(attributeValue);
1✔
392
                        Optional<RelationshipListVO> optionalRelationshipListVO = getRelationshipListFromProperty(attributeValue);
1✔
393
                        if (optionalRelationshipVO.isPresent()) {
1✔
394
                                return relationshipFromProperties(optionalRelationshipVO.get(), targetClass)
×
395
                                                // we return the constructed object, since invoke most likely returns null, which is not allowed on mapper functions
396
                                                // a list is created, since we have a relationship-list defined by the annotation
397
                                                .flatMap(relationship -> invokeWithExceptionHandling(setter, objectUnderConstruction, List.of(relationship)));
×
398
                        } else if (optionalRelationshipListVO.isPresent()) {
1✔
399
                                return Mono.zip(optionalRelationshipListVO.get().stream().map(relationshipVO -> relationshipFromProperties(relationshipVO, targetClass)).toList(),
1✔
400
                                                                oList -> Arrays.asList(oList).stream().map(targetClass::cast).toList())
1✔
401
                                                // we return the constructed object, since invoke most likely returns null, which is not allowed on mapper functions
402
                                                .flatMap(relationshipList -> invokeWithExceptionHandling(setter, objectUnderConstruction, relationshipList));
1✔
403
                        } else if (attributeValue instanceof PropertyVO pvo && pvo.getValue() instanceof List<?> vl && vl.isEmpty()) {
×
404
                                return Mono.just(objectUnderConstruction);
×
405
                        } else {
406
                                return Mono.error(new MappingException(String.format("Value of the relationship %s is invalid.", attributeValue)));
×
407
                        }
408
                } else {
409
                        return relationshipListToTargetClass(attributeValue, targetClass, relationShipMap)
1✔
410
                                        .defaultIfEmpty(List.of())
1✔
411
                                        // we return the constructed object, since invoke most likely returns null, which is not allowed on mapper functions
412
                                        .flatMap(relatedEntities -> invokeWithExceptionHandling(setter, objectUnderConstruction, relatedEntities));
1✔
413
                }
414
        }
415

416
        /**
417
         * Handle the evaluation of a relationship entry. Returns a single, emitting the target object, while invoking the property setting method.
418
         *
419
         * @param relationShip            the object containing the relationship
420
         * @param objectUnderConstruction the object under construction
421
         * @param relationShipMap         a map containing the pre-evaluated relationships
422
         * @param setter                  the setter to be used for the property
423
         * @param setterAnnotation        attribute setter annotation on the method
424
         * @param <T>                     class of the objectUnderConstruction
425
         * @return the single, emitting the objectUnderConstruction
426
         */
427
        private <T> Mono<T> handleRelationship(AdditionalPropertyVO relationShip, T objectUnderConstruction, Map<String, EntityVO> relationShipMap, Method setter, AttributeSetter setterAnnotation) {
428
                Class<?> targetClass = setterAnnotation.targetClass();
1✔
429
                if (relationShip instanceof RelationshipVO relationshipVO) {
1✔
430
                        if (setterAnnotation.fromProperties()) {
1✔
431
                                return relationshipFromProperties(relationshipVO, targetClass)
1✔
432
                                                // we return the constructed object, since invoke most likely returns null, which is not allowed on mapper functions
433
                                                .flatMap(relatedEntity -> invokeWithExceptionHandling(setter, objectUnderConstruction, relatedEntity));
1✔
434
                        } else {
435
                                return getObjectFromRelationship(relationshipVO, targetClass, relationShipMap, relationshipVO.getAdditionalProperties())
1✔
436
                                                // we return the constructed object, since invoke most likely returns null, which is not allowed on mapper functions
437
                                                .flatMap(relatedEntity -> invokeWithExceptionHandling(setter, objectUnderConstruction, relatedEntity));
1✔
438
                        }
439
                } else {
440
                        return Mono.error(new MappingException(String.format("Did not receive a valid relationship: %s", relationShip)));
×
441
                }
442
        }
443

444
        /**
445
         * Invoke the given method and handle potential exceptions.
446
         */
447
        private <T> Mono<T> invokeWithExceptionHandling(Method invocationMethod, T objectUnderConstruction, Object... invocationArgs) {
448
                try {
449
                        invocationMethod.invoke(objectUnderConstruction, invocationArgs);
1✔
450
                        return Mono.just(objectUnderConstruction);
1✔
451
                } catch (IllegalAccessException | InvocationTargetException | RuntimeException e) {
×
452
                        return Mono.error(new MappingException(String.format("Was not able to invoke method %s.", invocationMethod.getName()), e));
×
453
                }
454
        }
455

456
        /**
457
         * Create the target object of a relationship from its properties(instead of entities additionally retrieved)
458
         *
459
         * @param relationshipVO representation of the current relationship(as provided by the original entitiy)
460
         * @param targetClass    class of the target object to be created(e.g. the object representing the relationship)
461
         * @param <T>            the class
462
         * @return a single emitting the object representing the relationship
463
         */
464
        private <T> Mono<T> relationshipFromProperties(RelationshipVO relationshipVO, Class<T> targetClass) {
465
                try {
466
                        String entityID = relationshipVO.getObject().toString();
1✔
467

468
                        Constructor<T> objectConstructor = targetClass.getDeclaredConstructor(String.class);
1✔
469
                        T constructedObject = objectConstructor.newInstance(entityID);
1✔
470

471
                        Map<String, Method> attributeSetters = getAttributeSetterMethodMap(constructedObject);
1✔
472

473
                        return Mono.zip(attributeSetters.entrySet().stream()
1✔
474
                                        .map(methodEntry -> {
1✔
475
                                                String field = methodEntry.getKey();
1✔
476
                                                Method setterMethod = methodEntry.getValue();
1✔
477
                                                Optional<AttributeSetter> optionalAttributeSetterAnnotation = getAttributeSetterAnnotation(setterMethod);
1✔
478
                                                if (optionalAttributeSetterAnnotation.isEmpty()) {
1✔
479
                                                        // no setter for the field, can be ignored
480
                                                        log.debug("No setter defined for field {}", field);
×
481
                                                        return Mono.just(constructedObject);
×
482
                                                }
483
                                                AttributeSetter setterAnnotation = optionalAttributeSetterAnnotation.get();
1✔
484

485
                                                Optional<AdditionalPropertyVO> optionalProperty = switch (methodEntry.getKey()) {
1✔
486
                                                        case RelationshipVO.JSON_PROPERTY_OBSERVED_AT ->
487
                                                                        Optional.ofNullable(relationshipVO.getObservedAt()).map(this::propertyVOFromValue);
1✔
488
                                                        case RelationshipVO.JSON_PROPERTY_CREATED_AT ->
489
                                                                        Optional.ofNullable(relationshipVO.getCreatedAt()).map(this::propertyVOFromValue);
1✔
490
                                                        case RelationshipVO.JSON_PROPERTY_MODIFIED_AT ->
491
                                                                        Optional.ofNullable(relationshipVO.getModifiedAt()).map(this::propertyVOFromValue);
1✔
492
                                                        case RelationshipVO.JSON_PROPERTY_DATASET_ID ->
493
                                                                        Optional.ofNullable(relationshipVO.getDatasetId()).map(this::propertyVOFromValue);
1✔
494
                                                        case RelationshipVO.JSON_PROPERTY_INSTANCE_ID ->
495
                                                                        Optional.ofNullable(relationshipVO.getInstanceId()).map(this::propertyVOFromValue);
1✔
496
                                                        default -> Optional.empty();
1✔
497
                                                };
498

499
                                                // try to find the attribute from the additional properties
500
                                                if (optionalProperty.isEmpty() && relationshipVO.getAdditionalProperties() != null && relationshipVO.getAdditionalProperties().containsKey(field)) {
1✔
501
                                                        optionalProperty = Optional.ofNullable(relationshipVO.getAdditionalProperties().get(field));
1✔
502
                                                }
503

504
                                                return optionalProperty.map(attributeValue ->
1✔
505
                                                                switch (setterAnnotation.value()) {
1✔
506
                                                                        case PROPERTY, GEO_PROPERTY ->
507
                                                                                        handleProperty(attributeValue, constructedObject, setterMethod, setterAnnotation.targetClass());
1✔
508
                                                                        case RELATIONSHIP ->
509
                                                                                        getRelationshipMap(relationshipVO.getAdditionalProperties(), targetClass)
×
510
                                                                                                        .map(rm -> handleRelationship(attributeValue, constructedObject, rm, setterMethod, setterAnnotation));
×
511
                                                                        //resolve objects;
512
                                                                        case RELATIONSHIP_LIST ->
513
                                                                                        getRelationshipMap(relationshipVO.getAdditionalProperties(), targetClass)
×
514
                                                                                                        .map(rm -> handleRelationshipList(attributeValue, constructedObject, rm, setterMethod, setterAnnotation));
×
515
                                                                        case PROPERTY_LIST ->
516
                                                                                        handlePropertyList(attributeValue, constructedObject, setterMethod, setterAnnotation);
×
517
                                                                        default ->
518
                                                                                        Mono.error(new MappingException(String.format("Received type %s is not supported.", setterAnnotation.value())));
×
519
                                                                }).orElse(Mono.just(constructedObject));
1✔
520

521
                                        }).toList(), constructedObjects -> constructedObject);
1✔
522

523
                } catch (NoSuchMethodException e) {
×
524
                        return Mono.error(new MappingException(String.format("The class %s does not declare the required String id constructor.", targetClass)));
×
525
                } catch (InvocationTargetException | InstantiationException | IllegalAccessException e) {
×
526
                        return Mono.error(new MappingException(String.format("Was not able to create instance of %s.", targetClass), e));
×
527
                }
528
        }
529

530
        /**
531
         * Returns a list of all entityIDs that are defined as relationships from the given entity.
532
         *
533
         * @param additionalProperties map of the properties to evaluate
534
         * @param targetClass          target class of the mapping
535
         * @param <T>                  the class
536
         * @return a list of uris
537
         */
538
        private <T> List<URI> getRelationshipObjects(Map<String, AdditionalPropertyVO> additionalProperties, Class<T> targetClass) {
539
                return Arrays.stream(targetClass.getMethods())
1✔
540
                                .map(this::getAttributeSetterAnnotation)
1✔
541
                                .filter(Optional::isPresent)
1✔
542
                                .map(Optional::get)
1✔
543
                                .filter(a -> (a.value().equals(AttributeType.RELATIONSHIP) || a.value().equals(AttributeType.RELATIONSHIP_LIST)))
1✔
544
                                // we don't need to retrieve entities that should be filled from the properties.
545
                                .filter(a -> !a.fromProperties())
1✔
546
                                .flatMap(attributeSetter -> getEntityURIsByAttributeSetter(attributeSetter, additionalProperties).stream())
1✔
547
                                .toList();
1✔
548

549
        }
550

551
        /**
552
         * Evaluate a properties map to get all referenced entity ids
553
         *
554
         * @param attributeSetter the attribute setter annotation
555
         * @param propertiesMap   the properties map to check
556
         * @return a list of entity ids
557
         */
558
        private List<URI> getEntityURIsByAttributeSetter(AttributeSetter attributeSetter, Map<String, AdditionalPropertyVO> propertiesMap) {
559
                return Optional.ofNullable(propertiesMap.get(attributeSetter.targetName()))
1✔
560
                                .map(this::getURIsFromRelationshipObject)
1✔
561
                                .orElseGet(List::of);
1✔
562
        }
563

564
        /**
565
         * Evaluate a concrete object of a realitonship. If its a list of objects, get the ids of all entities.
566
         *
567
         * @param additionalPropertyVO the object to evaluate
568
         * @return a list of all referenced ids
569
         */
570
        private List<URI> getURIsFromRelationshipObject(AdditionalPropertyVO additionalPropertyVO) {
571
                Optional<RelationshipVO> optionalRelationshipVO = getRelationshipFromProperty(additionalPropertyVO);
1✔
572
                if (optionalRelationshipVO.isPresent()) {
1✔
573
                        // List.of() cannot be used, since we need a mutable list
574
                        List<URI> uriList = new ArrayList<>();
1✔
575
                        uriList.add(optionalRelationshipVO.get().getObject());
1✔
576
                        return uriList;
1✔
577
                }
578

579
                Optional<RelationshipListVO> optionalRelationshipListVO = getRelationshipListFromProperty(additionalPropertyVO);
1✔
580
                if (optionalRelationshipListVO.isPresent()) {
1✔
581
                        return optionalRelationshipListVO.get().stream().flatMap(listEntry -> getURIsFromRelationshipObject(listEntry).stream()).toList();
1✔
582
                }
583
                return List.of();
×
584
        }
585

586
        /**
587
         * Method to translate a Map-Entry(e.g. NGSI-LD relationship) to a typed list as defined by the target object
588
         *
589
         * @param entry       attribute of the entity, e.g. a relationship or a list of relationships
590
         * @param targetClass class to be used as type for the typed list
591
         * @param <T>         the type
592
         * @return a list of objects, mapping the relationship
593
         */
594
        private <T> Mono<List<T>> relationshipListToTargetClass(AdditionalPropertyVO entry, Class<T> targetClass, Map<String, EntityVO> relationShipEntitiesMap) {
595

596
                Optional<RelationshipVO> optionalRelationshipVO = getRelationshipFromProperty(entry);
1✔
597
                if (optionalRelationshipVO.isPresent()) {
1✔
598
                        return getObjectFromRelationship(optionalRelationshipVO.get(), targetClass, relationShipEntitiesMap, optionalRelationshipVO.get().getAdditionalProperties())
×
599
                                        .map(List::of);
×
600
                }
601

602
                Optional<RelationshipListVO> optionalRelationshipListVO = getRelationshipListFromProperty(entry);
1✔
603
                if (optionalRelationshipListVO.isPresent()) {
1✔
604
                        return zipToList(optionalRelationshipListVO.get().stream(), targetClass, relationShipEntitiesMap);
1✔
605
                }
606
                return Mono.just(List.of());
×
607
        }
608

609
        private Optional<RelationshipListVO> getRelationshipListFromProperty(AdditionalPropertyVO additionalPropertyVO) {
610
                if (additionalPropertyVO instanceof RelationshipListVO rsl) {
1✔
611
                        return Optional.of(rsl);
1✔
612
                } else if ((additionalPropertyVO instanceof PropertyVO propertyVO && ((PropertyVO) additionalPropertyVO).getValue() instanceof List<?> propertyListVO && !propertyListVO.isEmpty() && isRelationshipList(propertyVO))) {
×
613
                        RelationshipListVO relationshipListVO = new RelationshipListVO();
×
614
                        propertyListVO.stream()
×
615
                                        .map(value -> objectMapper.convertValue(value, RelationshipVO.class))
×
616
                                        .forEach(relationshipListVO::add);
×
617
                        return Optional.of(relationshipListVO);
×
618

619
                } else if (additionalPropertyVO instanceof PropertyListVO propertyListVO && !propertyListVO.isEmpty() && isRelationship(propertyListVO.get(0))) {
×
620
                        RelationshipListVO relationshipListVO = new RelationshipListVO();
×
621
                        propertyListVO.stream()
×
622
                                        .map(propertyVO -> objectMapper.convertValue(propertyVO.getValue(), RelationshipVO.class))
×
623
                                        .forEach(relationshipListVO::add);
×
624
                        return Optional.of(relationshipListVO);
×
625
                }
626
                return Optional.empty();
×
627
        }
628

629
        private Optional<RelationshipVO> getRelationshipFromProperty(AdditionalPropertyVO additionalPropertyVO) {
630
                if (additionalPropertyVO instanceof RelationshipVO relationshipVO) {
1✔
631
                        return Optional.of(relationshipVO);
1✔
632
                } else if (additionalPropertyVO instanceof PropertyVO propertyVO && isRelationship(propertyVO)) {
1✔
633
                        return Optional.of(objectMapper.convertValue(propertyVO.getValue(), RelationshipVO.class));
×
634
                }
635
                return Optional.empty();
1✔
636
        }
637

638
        private boolean isRelationship(PropertyVO testProperty) {
639
                return testProperty.getValue() instanceof Map<?, ?> valuesMap && valuesMap.get("type").equals(PropertyTypeVO.RELATIONSHIP.getValue());
×
640
        }
641

642
        /**
643
         * Check if the given method handles access to the unmapped properties
644
         */
645
        private boolean isUnmappedPropertiesSetter(Method method) {
646
                return Arrays.stream(method.getAnnotations()).anyMatch(UnmappedPropertiesSetter.class::isInstance);
1✔
647
        }
648

649
        private boolean isRelationshipList(PropertyVO testProperty) {
650
                return testProperty.getValue() instanceof List<?> valuesList &&
×
651
                                valuesList.stream()
×
652
                                                .allMatch(v -> {
×
653
                                                        if (v instanceof Map<?, ?> valueAsMap) {
×
654
                                                                return valueAsMap.get("type").equals(PropertyTypeVO.RELATIONSHIP.getValue());
×
655
                                                        }
656
                                                        return false;
×
657
                                                });
658
        }
659

660
        /**
661
         * Helper method for combining the evaluation of relationship entities to a single result lits
662
         *
663
         * @param relationshipVOStream    the relationships to evaluate
664
         * @param targetClass             target class of the relationship object
665
         * @param relationShipEntitiesMap map of the preevaluated relationship entities
666
         * @param <T>                     target class of the relationship
667
         * @return a single emitting the full list
668
         */
669
        private <T> Mono<List<T>> zipToList(Stream<RelationshipVO> relationshipVOStream, Class<T> targetClass, Map<String, EntityVO> relationShipEntitiesMap) {
670
                return Mono.zip(
1✔
671
                                relationshipVOStream.map(RelationshipVO::getObject)
1✔
672
                                                .filter(Objects::nonNull)
1✔
673
                                                .map(URI::toString)
1✔
674
                                                .map(relationShipEntitiesMap::get)
1✔
675
                                                .filter(Objects::nonNull)
1✔
676
                                                .map(entity -> fromEntityVO(entity, targetClass))
1✔
677
                                                .toList(),
1✔
678
                                oList -> Arrays.stream(oList).map(targetClass::cast).toList()
1✔
679
                );
680
        }
681

682
        /**
683
         * Method to translate a Map-Entry(e.g. NGSI-LD property) to a typed list as defined by the target object
684
         *
685
         * @param propertyVOS a list of properties
686
         * @param targetClass class to be used as type for the typed list
687
         * @param <T>         the type
688
         * @return a list of objects, mapping the relationship
689
         */
690
        private <T> List<T> propertyListToTargetClass(PropertyListVO propertyVOS, Class<T> targetClass) {
691
                return propertyVOS.stream().map(propertyEntry -> objectMapper.convertValue(propertyEntry.getValue(), targetClass)).toList();
1✔
692
        }
693

694
        /**
695
         * Retrieve the object from a relationship and return it as a java object of class T. All sub relationships will be evaluated, too.
696
         *
697
         * @param relationshipVO the relationship entry
698
         * @param targetClass    the target-class of the entry
699
         * @param <T>            the class
700
         * @return the actual object
701
         */
702
        private <T> Mono<T> getObjectFromRelationship(RelationshipVO relationshipVO, Class<T> targetClass, Map<String, EntityVO> relationShipEntitiesMap, Map<String, AdditionalPropertyVO> additionalPropertyVOMap) {
703
                Optional<EntityVO> optionalEntityVO = Optional.ofNullable(relationShipEntitiesMap.get(relationshipVO.getObject().toString()));
1✔
704
                if (optionalEntityVO.isEmpty() && !mappingProperties.isStrictRelationships()) {
1✔
705
                        try {
706
                                Constructor<T> objectConstructor = targetClass.getDeclaredConstructor(String.class);
1✔
707
                                T theObject = objectConstructor.newInstance(relationshipVO.getObject().toString());
1✔
708
                                // return the empty object
709
                                return Mono.just(theObject);
1✔
710
                        } catch (InvocationTargetException | InstantiationException | IllegalAccessException |
×
711
                                         NoSuchMethodException e) {
712
                                return Mono.error(new MappingException(String.format("Was not able to instantiate %s with a string parameter.", targetClass), e));
×
713
                        }
714
                } else if (optionalEntityVO.isEmpty()) {
1✔
715
                        return Mono.error(new MappingException(String.format("Was not able to resolve the relationship %s", relationshipVO.getObject())));
1✔
716
                }
717

718
                var entityVO = optionalEntityVO.get();
1✔
719
                //merge with override properties
720
                if (additionalPropertyVOMap != null) {
1✔
721
                        entityVO.getAdditionalProperties().putAll(additionalPropertyVOMap);
×
722
                }
723
                return fromEntityVO(entityVO, targetClass);
1✔
724
        }
725

726
        /**
727
         * Return the type of the setter's parameter.
728
         */
729
        private Class<?> getParameterType(Class<?>[] arrayOfClasses) {
730
                if (arrayOfClasses.length != 1) {
1✔
731
                        throw new MappingException("Setter method should only have one parameter declared.");
×
732
                }
733
                return arrayOfClasses[0];
1✔
734
        }
735

736
        /**
737
         * Get the setter method for the given property at the entity.
738
         */
739
        private <T> Optional<Method> getCorrespondingSetterMethod(T entity, String propertyName) {
740
                return getAttributeSettersMethods(entity).stream().filter(m ->
1✔
741
                                                getAttributeSetterAnnotation(m)
1✔
742
                                                                .map(attributeSetter -> attributeSetter.targetName().equals(propertyName)).orElse(false))
1✔
743
                                .findFirst();
1✔
744
        }
745

746
        /**
747
         * Get all attribute setters for the given entity
748
         */
749
        private <T> List<Method> getAttributeSettersMethods(T entity) {
750
                return Arrays.stream(entity.getClass().getMethods()).filter(m -> getAttributeSetterAnnotation(m).isPresent()).toList();
1✔
751
        }
752

753
        private <T> Optional<Method> getUnmappedPropertiesSetter(T entity) {
754
                return Arrays.stream(entity.getClass().getMethods())
1✔
755
                                .filter(this::isUnmappedPropertiesSetter).findAny();
1✔
756
        }
757

758
        private <T> Map<String, Method> getAttributeSetterMethodMap(T entity) {
759
                return Arrays.stream(entity.getClass().getMethods())
1✔
760
                                .filter(m -> getAttributeSetterAnnotation(m).isPresent())
1✔
761
                                .collect(Collectors.toMap(m -> getAttributeSetterAnnotation(m).get().targetName(), m -> m));
1✔
762
        }
763

764
        /**
765
         * Get the attribute setter annotation from the given method, if it exists.
766
         */
767
        private Optional<AttributeSetter> getAttributeSetterAnnotation(Method m) {
768
                return Arrays.stream(m.getAnnotations()).filter(AttributeSetter.class::isInstance)
1✔
769
                                .findFirst()
1✔
770
                                .map(AttributeSetter.class::cast);
1✔
771
        }
772

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