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

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

16 Jun 2025 11:43AM UTC coverage: 76.895% (-0.09%) from 76.988%
#289

push

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

fix

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

1 existing line in 1 file now uncovered.

639 of 831 relevant lines covered (76.9%)

0.77 hits per line

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

73.35
/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();
×
361
                        Optional.ofNullable(propertyVO.getValue())
×
362
                                        .map(pvo -> {
×
363
                                                try {
NEW
364
                                                        return objectMapper.convertValue(pvo, PropertyVO.class);
×
NEW
365
                                                } catch (Exception e) {
×
NEW
366
                                                        log.debug("The value cant be converted to a PropertyVO.", e);
×
UNCOV
367
                                                        return propertyVO;
×
368
                                                }
369
                                        })
370
                                        .ifPresent(propertyVOS::add);
×
371
                        return invokeWithExceptionHandling(setter, objectUnderConstruction, propertyListToTargetClass(propertyVOS, setterAnnotation.targetClass()));
×
372
                        // in case of single element lists, they are returned as a flat property
373
                } else {
374
                        return Mono.error(new MappingException(String.format("The attribute is not a valid property list: %v ", propertyListObject)));
×
375
                }
376
        }
377

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

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

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

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

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

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

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

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

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

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

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

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

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

550
        }
551

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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