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

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

05 Feb 2025 03:12PM UTC coverage: 78.205% (-1.4%) from 79.617%
#258

Pull #76

web-flow
Merge branch 'main' into with-rel
Pull Request #76: support relationships

46 of 72 new or added lines in 3 files covered. (63.89%)

1 existing line in 1 file now uncovered.

610 of 780 relevant lines covered (78.21%)

0.78 hits per line

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

77.32
/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(
1✔
258
                                        propertyListVO.stream()
1✔
259
                                                        .map(pvo -> fromProperty(unmappedAdditionalProperty.getKey(), pvo))
1✔
260
                                                        .map(Map.Entry::getValue)
1✔
261
                                                        .toList());
1✔
262
                } else if (unmappedAdditionalProperty.getValue() instanceof RelationshipListVO relationshipListVO) {
1✔
NEW
263
                        unmappedProperty.setValue(
×
NEW
264
                                        relationshipListVO.stream()
×
NEW
265
                                                        .map(rvo -> fromRelationship(unmappedAdditionalProperty.getKey(), rvo))
×
NEW
266
                                                        .map(Map.Entry::getValue)
×
NEW
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✔
NEW
284
                                                } else if (entry.getValue() instanceof RelationshipVO rvo) {
×
NEW
285
                                                        return fromRelationship(entry.getKey(), rvo);
×
286
                                                } else {
NEW
287
                                                        throw new MappingException(String.format("Entry value is not supported. Was: %s", entry.getValue()));
×
288
                                                }
289
                                        }).toList());
1✔
290
                        entryList.add(new AbstractMap.SimpleEntry<>("id", idValue.toString()));
1✔
291
                        return new AbstractMap.SimpleEntry<>(key, entryList.stream()
1✔
292
                                        .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)));
1✔
293
                } else {
294

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

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

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

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

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

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

435
        /**
436
         * Invoke the given method and handle potential exceptions.
437
         */
438
        private <T> Mono<T> invokeWithExceptionHandling(Method invocationMethod, T objectUnderConstruction, Object... invocationArgs) {
439
                try {
440
                        invocationMethod.invoke(objectUnderConstruction, invocationArgs);
1✔
441
                        return Mono.just(objectUnderConstruction);
1✔
442
                } catch (IllegalAccessException | InvocationTargetException | RuntimeException e) {
×
443
                        return Mono.error(new MappingException(String.format("Was not able to invoke method %s.", invocationMethod.getName()), e));
×
444
                }
445
        }
446

447
        /**
448
         * Create the target object of a relationship from its properties(instead of entities additionally retrieved)
449
         *
450
         * @param relationshipVO representation of the current relationship(as provided by the original entitiy)
451
         * @param targetClass    class of the target object to be created(e.g. the object representing the relationship)
452
         * @param <T>            the class
453
         * @return a single emitting the object representing the relationship
454
         */
455
        private <T> Mono<T> relationshipFromProperties(RelationshipVO relationshipVO, Class<T> targetClass) {
456
                try {
457
                        String entityID = relationshipVO.getObject().toString();
1✔
458

459
                        Constructor<T> objectConstructor = targetClass.getDeclaredConstructor(String.class);
1✔
460
                        T constructedObject = objectConstructor.newInstance(entityID);
1✔
461

462
                        Map<String, Method> attributeSetters = getAttributeSetterMethodMap(constructedObject);
1✔
463

464
                        return Mono.zip(attributeSetters.entrySet().stream()
1✔
465
                                        .map(methodEntry -> {
1✔
466
                                                String field = methodEntry.getKey();
1✔
467
                                                Method setterMethod = methodEntry.getValue();
1✔
468
                                                Optional<AttributeSetter> optionalAttributeSetterAnnotation = getAttributeSetterAnnotation(setterMethod);
1✔
469
                                                if (optionalAttributeSetterAnnotation.isEmpty()) {
1✔
470
                                                        // no setter for the field, can be ignored
471
                                                        log.debug("No setter defined for field {}", field);
×
472
                                                        return Mono.just(constructedObject);
×
473
                                                }
474
                                                AttributeSetter setterAnnotation = optionalAttributeSetterAnnotation.get();
1✔
475

476
                                                Optional<AdditionalPropertyVO> optionalProperty = switch (methodEntry.getKey()) {
1✔
477
                                                        case RelationshipVO.JSON_PROPERTY_OBSERVED_AT ->
478
                                                                        Optional.ofNullable(relationshipVO.getObservedAt()).map(this::propertyVOFromValue);
1✔
479
                                                        case RelationshipVO.JSON_PROPERTY_CREATED_AT ->
480
                                                                        Optional.ofNullable(relationshipVO.getCreatedAt()).map(this::propertyVOFromValue);
1✔
481
                                                        case RelationshipVO.JSON_PROPERTY_MODIFIED_AT ->
482
                                                                        Optional.ofNullable(relationshipVO.getModifiedAt()).map(this::propertyVOFromValue);
1✔
483
                                                        case RelationshipVO.JSON_PROPERTY_DATASET_ID ->
484
                                                                        Optional.ofNullable(relationshipVO.getDatasetId()).map(this::propertyVOFromValue);
1✔
485
                                                        case RelationshipVO.JSON_PROPERTY_INSTANCE_ID ->
486
                                                                        Optional.ofNullable(relationshipVO.getInstanceId()).map(this::propertyVOFromValue);
1✔
487
                                                        default -> Optional.empty();
1✔
488
                                                };
489

490
                                                // try to find the attribute from the additional properties
491
                                                if (optionalProperty.isEmpty() && relationshipVO.getAdditionalProperties() != null && relationshipVO.getAdditionalProperties().containsKey(field)) {
1✔
492
                                                        optionalProperty = Optional.ofNullable(relationshipVO.getAdditionalProperties().get(field));
1✔
493
                                                }
494

495
                                                return optionalProperty.map(attributeValue ->
1✔
496
                                                                switch (setterAnnotation.value()) {
1✔
497
                                                                        case PROPERTY, GEO_PROPERTY ->
498
                                                                                        handleProperty(attributeValue, constructedObject, setterMethod, setterAnnotation.targetClass());
1✔
499
                                                                        case RELATIONSHIP ->
500
                                                                                        getRelationshipMap(relationshipVO.getAdditionalProperties(), targetClass)
×
501
                                                                                                        .map(rm -> handleRelationship(attributeValue, constructedObject, rm, setterMethod, setterAnnotation));
×
502
                                                                        //resolve objects;
503
                                                                        case RELATIONSHIP_LIST ->
504
                                                                                        getRelationshipMap(relationshipVO.getAdditionalProperties(), targetClass)
×
505
                                                                                                        .map(rm -> handleRelationshipList(attributeValue, constructedObject, rm, setterMethod, setterAnnotation));
×
506
                                                                        case PROPERTY_LIST ->
507
                                                                                        handlePropertyList(attributeValue, constructedObject, setterMethod, setterAnnotation);
×
508
                                                                        default ->
509
                                                                                        Mono.error(new MappingException(String.format("Received type %s is not supported.", setterAnnotation.value())));
×
510
                                                                }).orElse(Mono.just(constructedObject));
1✔
511

512
                                        }).toList(), constructedObjects -> constructedObject);
1✔
513

514
                } catch (NoSuchMethodException e) {
×
515
                        return Mono.error(new MappingException(String.format("The class %s does not declare the required String id constructor.", targetClass)));
×
516
                } catch (InvocationTargetException | InstantiationException | IllegalAccessException e) {
×
517
                        return Mono.error(new MappingException(String.format("Was not able to create instance of %s.", targetClass), e));
×
518
                }
519
        }
520

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

540
        }
541

542
        /**
543
         * Evaluate a properties map to get all referenced entity ids
544
         *
545
         * @param attributeSetter the attribute setter annotation
546
         * @param propertiesMap   the properties map to check
547
         * @return a list of entity ids
548
         */
549
        private List<URI> getEntityURIsByAttributeSetter(AttributeSetter attributeSetter, Map<String, AdditionalPropertyVO> propertiesMap) {
550
                return Optional.ofNullable(propertiesMap.get(attributeSetter.targetName()))
1✔
551
                                .map(this::getURIsFromRelationshipObject)
1✔
552
                                .orElseGet(List::of);
1✔
553
        }
554

555
        /**
556
         * Evaluate a concrete object of a realitonship. If its a list of objects, get the ids of all entities.
557
         *
558
         * @param additionalPropertyVO the object to evaluate
559
         * @return a list of all referenced ids
560
         */
561
        private List<URI> getURIsFromRelationshipObject(AdditionalPropertyVO additionalPropertyVO) {
562
                Optional<RelationshipVO> optionalRelationshipVO = getRelationshipFromProperty(additionalPropertyVO);
1✔
563
                if (optionalRelationshipVO.isPresent()) {
1✔
564
                        // List.of() cannot be used, since we need a mutable list
565
                        List<URI> uriList = new ArrayList<>();
1✔
566
                        uriList.add(optionalRelationshipVO.get().getObject());
1✔
567
                        return uriList;
1✔
568
                }
569

570
                Optional<RelationshipListVO> optionalRelationshipListVO = getRelationshipListFromProperty(additionalPropertyVO);
1✔
571
                if (optionalRelationshipListVO.isPresent()) {
1✔
572
                        return optionalRelationshipListVO.get().stream().flatMap(listEntry -> getURIsFromRelationshipObject(listEntry).stream()).toList();
1✔
573
                }
574
                return List.of();
×
575
        }
576

577
        /**
578
         * Method to translate a Map-Entry(e.g. NGSI-LD relationship) to a typed list as defined by the target object
579
         *
580
         * @param entry       attribute of the entity, e.g. a relationship or a list of relationships
581
         * @param targetClass class to be used as type for the typed list
582
         * @param <T>         the type
583
         * @return a list of objects, mapping the relationship
584
         */
585
        private <T> Mono<List<T>> relationshipListToTargetClass(AdditionalPropertyVO entry, Class<T> targetClass, Map<String, EntityVO> relationShipEntitiesMap) {
586

587
                Optional<RelationshipVO> optionalRelationshipVO = getRelationshipFromProperty(entry);
1✔
588
                if (optionalRelationshipVO.isPresent()) {
1✔
589
                        return getObjectFromRelationship(optionalRelationshipVO.get(), targetClass, relationShipEntitiesMap, optionalRelationshipVO.get().getAdditionalProperties())
×
590
                                        .map(List::of);
×
591
                }
592

593
                Optional<RelationshipListVO> optionalRelationshipListVO = getRelationshipListFromProperty(entry);
1✔
594
                if (optionalRelationshipListVO.isPresent()) {
1✔
595
                        return zipToList(optionalRelationshipListVO.get().stream(), targetClass, relationShipEntitiesMap);
1✔
596
                }
597
                return Mono.just(List.of());
×
598
        }
599

600
        private Optional<RelationshipListVO> getRelationshipListFromProperty(AdditionalPropertyVO additionalPropertyVO) {
601
                if (additionalPropertyVO instanceof RelationshipListVO rsl) {
1✔
602
                        return Optional.of(rsl);
1✔
603
                } else if ((additionalPropertyVO instanceof PropertyVO propertyVO && ((PropertyVO) additionalPropertyVO).getValue() instanceof List<?> propertyListVO && !propertyListVO.isEmpty() && isRelationshipList(propertyVO))) {
×
604
                        RelationshipListVO relationshipListVO = new RelationshipListVO();
×
605
                        propertyListVO.stream()
×
606
                                        .map(value -> objectMapper.convertValue(value, RelationshipVO.class))
×
607
                                        .forEach(relationshipListVO::add);
×
608
                        return Optional.of(relationshipListVO);
×
609

610
                } else if (additionalPropertyVO instanceof PropertyListVO propertyListVO && !propertyListVO.isEmpty() && isRelationship(propertyListVO.get(0))) {
×
611
                        RelationshipListVO relationshipListVO = new RelationshipListVO();
×
612
                        propertyListVO.stream()
×
613
                                        .map(propertyVO -> objectMapper.convertValue(propertyVO.getValue(), RelationshipVO.class))
×
614
                                        .forEach(relationshipListVO::add);
×
615
                        return Optional.of(relationshipListVO);
×
616
                }
617
                return Optional.empty();
×
618
        }
619

620
        private Optional<RelationshipVO> getRelationshipFromProperty(AdditionalPropertyVO additionalPropertyVO) {
621
                if (additionalPropertyVO instanceof RelationshipVO relationshipVO) {
1✔
622
                        return Optional.of(relationshipVO);
1✔
623
                } else if (additionalPropertyVO instanceof PropertyVO propertyVO && isRelationship(propertyVO)) {
1✔
624
                        return Optional.of(objectMapper.convertValue(propertyVO.getValue(), RelationshipVO.class));
×
625
                }
626
                return Optional.empty();
1✔
627
        }
628

629
        private boolean isRelationship(PropertyVO testProperty) {
630
                return testProperty.getValue() instanceof Map<?, ?> valuesMap && valuesMap.get("type").equals(PropertyTypeVO.RELATIONSHIP.getValue());
×
631
        }
632

633
        /**
634
         * Check if the given method handles access to the unmapped properties
635
         */
636
        private boolean isUnmappedPropertiesSetter(Method method) {
637
                return Arrays.stream(method.getAnnotations()).anyMatch(UnmappedPropertiesSetter.class::isInstance);
1✔
638
        }
639

640
        private boolean isRelationshipList(PropertyVO testProperty) {
641
                return testProperty.getValue() instanceof List<?> valuesList &&
×
642
                                valuesList.stream()
×
643
                                                .allMatch(v -> {
×
644
                                                        if (v instanceof Map<?, ?> valueAsMap) {
×
645
                                                                return valueAsMap.get("type").equals(PropertyTypeVO.RELATIONSHIP.getValue());
×
646
                                                        }
647
                                                        return false;
×
648
                                                });
649
        }
650

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

673
        /**
674
         * Method to translate a Map-Entry(e.g. NGSI-LD property) to a typed list as defined by the target object
675
         *
676
         * @param propertyVOS a list of properties
677
         * @param targetClass class to be used as type for the typed list
678
         * @param <T>         the type
679
         * @return a list of objects, mapping the relationship
680
         */
681
        private <T> List<T> propertyListToTargetClass(PropertyListVO propertyVOS, Class<T> targetClass) {
682
                return propertyVOS.stream().map(propertyEntry -> objectMapper.convertValue(propertyEntry.getValue(), targetClass)).toList();
1✔
683
        }
684

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

709
                var entityVO = optionalEntityVO.get();
1✔
710
                //merge with override properties
711
                if (additionalPropertyVOMap != null) {
1✔
712
                        entityVO.getAdditionalProperties().putAll(additionalPropertyVOMap);
×
713
                }
714
                return fromEntityVO(entityVO, targetClass);
1✔
715
        }
716

717
        /**
718
         * Return the type of the setter's parameter.
719
         */
720
        private Class<?> getParameterType(Class<?>[] arrayOfClasses) {
721
                if (arrayOfClasses.length != 1) {
1✔
722
                        throw new MappingException("Setter method should only have one parameter declared.");
×
723
                }
724
                return arrayOfClasses[0];
1✔
725
        }
726

727
        /**
728
         * Get the setter method for the given property at the entity.
729
         */
730
        private <T> Optional<Method> getCorrespondingSetterMethod(T entity, String propertyName) {
731
                return getAttributeSettersMethods(entity).stream().filter(m ->
1✔
732
                                                getAttributeSetterAnnotation(m)
1✔
733
                                                                .map(attributeSetter -> attributeSetter.targetName().equals(propertyName)).orElse(false))
1✔
734
                                .findFirst();
1✔
735
        }
736

737
        /**
738
         * Get all attribute setters for the given entity
739
         */
740
        private <T> List<Method> getAttributeSettersMethods(T entity) {
741
                return Arrays.stream(entity.getClass().getMethods()).filter(m -> getAttributeSetterAnnotation(m).isPresent()).toList();
1✔
742
        }
743

744
        private <T> Optional<Method> getUnmappedPropertiesSetter(T entity) {
745
                return Arrays.stream(entity.getClass().getMethods())
1✔
746
                                .filter(this::isUnmappedPropertiesSetter).findAny();
1✔
747
        }
748

749
        private <T> Map<String, Method> getAttributeSetterMethodMap(T entity) {
750
                return Arrays.stream(entity.getClass().getMethods())
1✔
751
                                .filter(m -> getAttributeSetterAnnotation(m).isPresent())
1✔
752
                                .collect(Collectors.toMap(m -> getAttributeSetterAnnotation(m).get().targetName(), m -> m));
1✔
753
        }
754

755
        /**
756
         * Get the attribute setter annotation from the given method, if it exists.
757
         */
758
        private Optional<AttributeSetter> getAttributeSetterAnnotation(Method m) {
759
                return Arrays.stream(m.getAnnotations()).filter(AttributeSetter.class::isInstance)
1✔
760
                                .findFirst()
1✔
761
                                .map(AttributeSetter.class::cast);
1✔
762
        }
763

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