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

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

04 Feb 2025 12:47PM UTC coverage: 79.542% (+1.6%) from 77.969%
#249

push

wistefan
fixes and test

12 of 12 new or added lines in 3 files covered. (100.0%)

93 existing lines in 2 files now uncovered.

556 of 699 relevant lines covered (79.54%)

0.8 hits per line

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

78.78
/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✔
UNCOV
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✔
UNCOV
223
                        log.debug("Ignoring property {} for entity {} since there is no attribute setter configured.", entry.getKey(), entityId);
×
UNCOV
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 ->
UNCOV
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
                if (unmappedAdditionalProperty.getValue() instanceof PropertyListVO propertyListVO) {
1✔
256
                        unmappedProperty.setValue(
1✔
257
                                        propertyListVO.stream()
1✔
258
                                                        .map(PropertyVO::getValue)
1✔
259
                                                        .toList());
1✔
260
                } else if (unmappedAdditionalProperty.getValue() instanceof PropertyVO propertyVO) {
1✔
261
                        unmappedProperty.setValue(propertyVO.getValue());
1✔
262
                }
263
                return unmappedProperty;
1✔
264
        }
265

266
        /**
267
         * Handle the evaluation of a property entry. Returns a single, emitting the target object, while invoking the property setting method.
268
         *
269
         * @param propertyValue           the value of the property
270
         * @param objectUnderConstruction the object under construction
271
         * @param setter                  the setter to be used for the property
272
         * @param parameterType           type of the property in the target object
273
         * @param <T>                     class of the object under construction
274
         * @return the single, emitting the objectUnderConstruction
275
         */
276
        private <T> Mono<T> handleProperty(AdditionalPropertyVO propertyValue, T objectUnderConstruction, Method setter, Class<?> parameterType) {
277
                if (propertyValue instanceof PropertyVO propertyVO) {
1✔
278
                        return invokeWithExceptionHandling(setter, objectUnderConstruction, objectMapper.convertValue(propertyVO.getValue(), parameterType));
1✔
279
                } else if (propertyValue instanceof GeoPropertyVO geoPropertyVO) {
1✔
280
                        return invokeWithExceptionHandling(setter, objectUnderConstruction, objectMapper.convertValue(geoPropertyVO.getValue(), parameterType));
1✔
281
                } else {
UNCOV
282
                        log.error("Mapping exception");
×
UNCOV
283
                        return Mono.error(new MappingException(String.format("The attribute is not a valid property: %s ", propertyValue)));
×
284
                }
285
        }
286

287
        /**
288
         * Handle the evaluation of a property-list entry. Returns a single, emitting the target object, while invoking the property setting method.
289
         *
290
         * @param propertyListObject      the object containing the property-list
291
         * @param objectUnderConstruction the object under construction
292
         * @param setter                  the setter to be used for the property
293
         * @param <T>                     class of the object under construction
294
         * @return the single, emitting the objectUnderConstruction
295
         */
296
        private <T> Mono<T> handlePropertyList(AdditionalPropertyVO propertyListObject, T objectUnderConstruction, Method setter, AttributeSetter setterAnnotation) {
297
                if (propertyListObject instanceof PropertyListVO propertyVOS) {
1✔
298
                        return invokeWithExceptionHandling(setter, objectUnderConstruction, propertyListToTargetClass(propertyVOS, setterAnnotation.targetClass()));
1✔
299
                } else if (propertyListObject instanceof PropertyVO propertyVO) {
1✔
300
                        //we need special handling here, since we have no real property lists(see NGSI-LD issue)
301
                        // TODO: remove as soon as ngsi-ld does properly support that.
302
                        if (propertyVO.getValue() instanceof List propertyList) {
1✔
303
                                return invokeWithExceptionHandling(setter, objectUnderConstruction, propertyList.stream()
1✔
304
                                                .map(listValue -> objectMapper.convertValue(listValue, setterAnnotation.targetClass()))
1✔
305
                                                .toList());
1✔
306
                        }
UNCOV
307
                        PropertyListVO propertyVOS = new PropertyListVO();
×
UNCOV
308
                        propertyVOS.add(propertyVO);
×
309
                        // in case of single element lists, they are returned as a flat property
310
                        return invokeWithExceptionHandling(setter, objectUnderConstruction, propertyListToTargetClass(propertyVOS, setterAnnotation.targetClass()));
×
311
                } else {
312
                        return Mono.error(new MappingException(String.format("The attribute is not a valid property list: %v ", propertyListObject)));
×
313
                }
314
        }
315

316
        /**
317
         * Handle the evaluation of a relationship-list entry. Returns a single, emitting the target object, while invoking the property setting method.
318
         *
319
         * @param attributeValue          the entry containing the relationship-list
320
         * @param objectUnderConstruction the object under construction
321
         * @param relationShipMap         a map containing the pre-evaluated relationships
322
         * @param setter                  the setter to be used for the property
323
         * @param setterAnnotation        attribute setter annotation on the method
324
         * @param <T>                     class of the objectUnderConstruction
325
         * @return the single, emitting the objectUnderConstruction
326
         */
327
        private <T> Mono<T> handleRelationshipList(AdditionalPropertyVO attributeValue, T objectUnderConstruction, Map<String, EntityVO> relationShipMap, Method setter, AttributeSetter setterAnnotation) {
328
                Class<?> targetClass = setterAnnotation.targetClass();
1✔
329
                if (setterAnnotation.fromProperties()) {
1✔
330
                        Optional<RelationshipVO> optionalRelationshipVO = getRelationshipFromProperty(attributeValue);
1✔
331
                        Optional<RelationshipListVO> optionalRelationshipListVO = getRelationshipListFromProperty(attributeValue);
1✔
332
                        if (optionalRelationshipVO.isPresent()) {
1✔
UNCOV
333
                                return relationshipFromProperties(optionalRelationshipVO.get(), targetClass)
×
334
                                                // we return the constructed object, since invoke most likely returns null, which is not allowed on mapper functions
335
                                                // a list is created, since we have a relationship-list defined by the annotation
UNCOV
336
                                                .flatMap(relationship -> invokeWithExceptionHandling(setter, objectUnderConstruction, List.of(relationship)));
×
337
                        } else if (optionalRelationshipListVO.isPresent()) {
1✔
338
                                return Mono.zip(optionalRelationshipListVO.get().stream().map(relationshipVO -> relationshipFromProperties(relationshipVO, targetClass)).toList(),
1✔
339
                                                                oList -> Arrays.asList(oList).stream().map(targetClass::cast).toList())
1✔
340
                                                // we return the constructed object, since invoke most likely returns null, which is not allowed on mapper functions
341
                                                .flatMap(relationshipList -> invokeWithExceptionHandling(setter, objectUnderConstruction, relationshipList));
1✔
UNCOV
342
                        } else if (attributeValue instanceof PropertyVO pvo && pvo.getValue() instanceof List<?> vl && vl.isEmpty()) {
×
UNCOV
343
                                return Mono.just(objectUnderConstruction);
×
344
                        } else {
UNCOV
345
                                return Mono.error(new MappingException(String.format("Value of the relationship %s is invalid.", attributeValue)));
×
346
                        }
347
                } else {
348
                        return relationshipListToTargetClass(attributeValue, targetClass, relationShipMap)
1✔
349
                                        .defaultIfEmpty(List.of())
1✔
350
                                        // we return the constructed object, since invoke most likely returns null, which is not allowed on mapper functions
351
                                        .flatMap(relatedEntities -> invokeWithExceptionHandling(setter, objectUnderConstruction, relatedEntities));
1✔
352
                }
353
        }
354

355
        /**
356
         * Handle the evaluation of a relationship entry. Returns a single, emitting the target object, while invoking the property setting method.
357
         *
358
         * @param relationShip            the object containing the relationship
359
         * @param objectUnderConstruction the object under construction
360
         * @param relationShipMap         a map containing the pre-evaluated relationships
361
         * @param setter                  the setter to be used for the property
362
         * @param setterAnnotation        attribute setter annotation on the method
363
         * @param <T>                     class of the objectUnderConstruction
364
         * @return the single, emitting the objectUnderConstruction
365
         */
366
        private <T> Mono<T> handleRelationship(AdditionalPropertyVO relationShip, T objectUnderConstruction, Map<String, EntityVO> relationShipMap, Method setter, AttributeSetter setterAnnotation) {
367
                Class<?> targetClass = setterAnnotation.targetClass();
1✔
368
                if (relationShip instanceof RelationshipVO relationshipVO) {
1✔
369
                        if (setterAnnotation.fromProperties()) {
1✔
370
                                return relationshipFromProperties(relationshipVO, targetClass)
1✔
371
                                                // we return the constructed object, since invoke most likely returns null, which is not allowed on mapper functions
372
                                                .flatMap(relatedEntity -> invokeWithExceptionHandling(setter, objectUnderConstruction, relatedEntity));
1✔
373
                        } else {
374
                                return getObjectFromRelationship(relationshipVO, targetClass, relationShipMap, relationshipVO.getAdditionalProperties())
1✔
375
                                                // we return the constructed object, since invoke most likely returns null, which is not allowed on mapper functions
376
                                                .flatMap(relatedEntity -> invokeWithExceptionHandling(setter, objectUnderConstruction, relatedEntity));
1✔
377
                        }
378
                } else {
UNCOV
379
                        return Mono.error(new MappingException(String.format("Did not receive a valid relationship: %s", relationShip)));
×
380
                }
381
        }
382

383
        /**
384
         * Invoke the given method and handle potential exceptions.
385
         */
386
        private <T> Mono<T> invokeWithExceptionHandling(Method invocationMethod, T objectUnderConstruction, Object... invocationArgs) {
387
                try {
388
                        invocationMethod.invoke(objectUnderConstruction, invocationArgs);
1✔
389
                        return Mono.just(objectUnderConstruction);
1✔
UNCOV
390
                } catch (IllegalAccessException | InvocationTargetException | RuntimeException e) {
×
UNCOV
391
                        return Mono.error(new MappingException(String.format("Was not able to invoke method %s.", invocationMethod.getName()), e));
×
392
                }
393
        }
394

395
        /**
396
         * Create the target object of a relationship from its properties(instead of entities additionally retrieved)
397
         *
398
         * @param relationshipVO representation of the current relationship(as provided by the original entitiy)
399
         * @param targetClass    class of the target object to be created(e.g. the object representing the relationship)
400
         * @param <T>            the class
401
         * @return a single emitting the object representing the relationship
402
         */
403
        private <T> Mono<T> relationshipFromProperties(RelationshipVO relationshipVO, Class<T> targetClass) {
404
                try {
405
                        String entityID = relationshipVO.getObject().toString();
1✔
406

407
                        Constructor<T> objectConstructor = targetClass.getDeclaredConstructor(String.class);
1✔
408
                        T constructedObject = objectConstructor.newInstance(entityID);
1✔
409

410
                        Map<String, Method> attributeSetters = getAttributeSetterMethodMap(constructedObject);
1✔
411

412
                        return Mono.zip(attributeSetters.entrySet().stream()
1✔
413
                                        .map(methodEntry -> {
1✔
414
                                                String field = methodEntry.getKey();
1✔
415
                                                Method setterMethod = methodEntry.getValue();
1✔
416
                                                Optional<AttributeSetter> optionalAttributeSetterAnnotation = getAttributeSetterAnnotation(setterMethod);
1✔
417
                                                if (optionalAttributeSetterAnnotation.isEmpty()) {
1✔
418
                                                        // no setter for the field, can be ignored
UNCOV
419
                                                        log.debug("No setter defined for field {}", field);
×
UNCOV
420
                                                        return Mono.just(constructedObject);
×
421
                                                }
422
                                                AttributeSetter setterAnnotation = optionalAttributeSetterAnnotation.get();
1✔
423

424
                                                Optional<AdditionalPropertyVO> optionalProperty = switch (methodEntry.getKey()) {
1✔
425
                                                        case RelationshipVO.JSON_PROPERTY_OBSERVED_AT ->
426
                                                                        Optional.ofNullable(relationshipVO.getObservedAt()).map(this::propertyVOFromValue);
1✔
427
                                                        case RelationshipVO.JSON_PROPERTY_CREATED_AT ->
428
                                                                        Optional.ofNullable(relationshipVO.getCreatedAt()).map(this::propertyVOFromValue);
1✔
429
                                                        case RelationshipVO.JSON_PROPERTY_MODIFIED_AT ->
430
                                                                        Optional.ofNullable(relationshipVO.getModifiedAt()).map(this::propertyVOFromValue);
1✔
431
                                                        case RelationshipVO.JSON_PROPERTY_DATASET_ID ->
432
                                                                        Optional.ofNullable(relationshipVO.getDatasetId()).map(this::propertyVOFromValue);
1✔
433
                                                        case RelationshipVO.JSON_PROPERTY_INSTANCE_ID ->
434
                                                                        Optional.ofNullable(relationshipVO.getInstanceId()).map(this::propertyVOFromValue);
1✔
435
                                                        default -> Optional.empty();
1✔
436
                                                };
437

438
                                                // try to find the attribute from the additional properties
439
                                                if (optionalProperty.isEmpty() && relationshipVO.getAdditionalProperties() != null && relationshipVO.getAdditionalProperties().containsKey(field)) {
1✔
440
                                                        optionalProperty = Optional.ofNullable(relationshipVO.getAdditionalProperties().get(field));
1✔
441
                                                }
442

443
                                                return optionalProperty.map(attributeValue ->
1✔
444
                                                                switch (setterAnnotation.value()) {
1✔
445
                                                                        case PROPERTY, GEO_PROPERTY ->
446
                                                                                        handleProperty(attributeValue, constructedObject, setterMethod, setterAnnotation.targetClass());
1✔
447
                                                                        case RELATIONSHIP ->
UNCOV
448
                                                                                        getRelationshipMap(relationshipVO.getAdditionalProperties(), targetClass)
×
UNCOV
449
                                                                                                        .map(rm -> handleRelationship(attributeValue, constructedObject, rm, setterMethod, setterAnnotation));
×
450
                                                                        //resolve objects;
451
                                                                        case RELATIONSHIP_LIST ->
UNCOV
452
                                                                                        getRelationshipMap(relationshipVO.getAdditionalProperties(), targetClass)
×
UNCOV
453
                                                                                                        .map(rm -> handleRelationshipList(attributeValue, constructedObject, rm, setterMethod, setterAnnotation));
×
454
                                                                        case PROPERTY_LIST ->
UNCOV
455
                                                                                        handlePropertyList(attributeValue, constructedObject, setterMethod, setterAnnotation);
×
456
                                                                        default ->
UNCOV
457
                                                                                        Mono.error(new MappingException(String.format("Received type %s is not supported.", setterAnnotation.value())));
×
458
                                                                }).orElse(Mono.just(constructedObject));
1✔
459

460
                                        }).toList(), constructedObjects -> constructedObject);
1✔
461

UNCOV
462
                } catch (NoSuchMethodException e) {
×
UNCOV
463
                        return Mono.error(new MappingException(String.format("The class %s does not declare the required String id constructor.", targetClass)));
×
UNCOV
464
                } catch (InvocationTargetException | InstantiationException | IllegalAccessException e) {
×
UNCOV
465
                        return Mono.error(new MappingException(String.format("Was not able to create instance of %s.", targetClass), e));
×
466
                }
467
        }
468

469
        /**
470
         * Returns a list of all entityIDs that are defined as relationships from the given entity.
471
         *
472
         * @param additionalProperties map of the properties to evaluate
473
         * @param targetClass          target class of the mapping
474
         * @param <T>                  the class
475
         * @return a list of uris
476
         */
477
        private <T> List<URI> getRelationshipObjects(Map<String, AdditionalPropertyVO> additionalProperties, Class<T> targetClass) {
478
                return Arrays.stream(targetClass.getMethods())
1✔
479
                                .map(this::getAttributeSetterAnnotation)
1✔
480
                                .filter(Optional::isPresent)
1✔
481
                                .map(Optional::get)
1✔
482
                                .filter(a -> (a.value().equals(AttributeType.RELATIONSHIP) || a.value().equals(AttributeType.RELATIONSHIP_LIST)))
1✔
483
                                // we don't need to retrieve entities that should be filled from the properties.
484
                                .filter(a -> !a.fromProperties())
1✔
485
                                .flatMap(attributeSetter -> getEntityURIsByAttributeSetter(attributeSetter, additionalProperties).stream())
1✔
486
                                .toList();
1✔
487

488
        }
489

490
        /**
491
         * Evaluate a properties map to get all referenced entity ids
492
         *
493
         * @param attributeSetter the attribute setter annotation
494
         * @param propertiesMap   the properties map to check
495
         * @return a list of entity ids
496
         */
497
        private List<URI> getEntityURIsByAttributeSetter(AttributeSetter attributeSetter, Map<String, AdditionalPropertyVO> propertiesMap) {
498
                return Optional.ofNullable(propertiesMap.get(attributeSetter.targetName()))
1✔
499
                                .map(this::getURIsFromRelationshipObject)
1✔
500
                                .orElseGet(List::of);
1✔
501
        }
502

503
        /**
504
         * Evaluate a concrete object of a realitonship. If its a list of objects, get the ids of all entities.
505
         *
506
         * @param additionalPropertyVO the object to evaluate
507
         * @return a list of all referenced ids
508
         */
509
        private List<URI> getURIsFromRelationshipObject(AdditionalPropertyVO additionalPropertyVO) {
510
                Optional<RelationshipVO> optionalRelationshipVO = getRelationshipFromProperty(additionalPropertyVO);
1✔
511
                if (optionalRelationshipVO.isPresent()) {
1✔
512
                        // List.of() cannot be used, since we need a mutable list
513
                        List<URI> uriList = new ArrayList<>();
1✔
514
                        uriList.add(optionalRelationshipVO.get().getObject());
1✔
515
                        return uriList;
1✔
516
                }
517

518
                Optional<RelationshipListVO> optionalRelationshipListVO = getRelationshipListFromProperty(additionalPropertyVO);
1✔
519
                if (optionalRelationshipListVO.isPresent()) {
1✔
520
                        return optionalRelationshipListVO.get().stream().flatMap(listEntry -> getURIsFromRelationshipObject(listEntry).stream()).toList();
1✔
521
                }
UNCOV
522
                return List.of();
×
523
        }
524

525
        /**
526
         * Method to translate a Map-Entry(e.g. NGSI-LD relationship) to a typed list as defined by the target object
527
         *
528
         * @param entry       attribute of the entity, e.g. a relationship or a list of relationships
529
         * @param targetClass class to be used as type for the typed list
530
         * @param <T>         the type
531
         * @return a list of objects, mapping the relationship
532
         */
533
        private <T> Mono<List<T>> relationshipListToTargetClass(AdditionalPropertyVO entry, Class<T> targetClass, Map<String, EntityVO> relationShipEntitiesMap) {
534

535
                Optional<RelationshipVO> optionalRelationshipVO = getRelationshipFromProperty(entry);
1✔
536
                if (optionalRelationshipVO.isPresent()) {
1✔
537
                        return getObjectFromRelationship(optionalRelationshipVO.get(), targetClass, relationShipEntitiesMap, optionalRelationshipVO.get().getAdditionalProperties())
×
538
                                        .map(List::of);
×
539
                }
540

541
                Optional<RelationshipListVO> optionalRelationshipListVO = getRelationshipListFromProperty(entry);
1✔
542
                if (optionalRelationshipListVO.isPresent()) {
1✔
543
                        return zipToList(optionalRelationshipListVO.get().stream(), targetClass, relationShipEntitiesMap);
1✔
544
                }
UNCOV
545
                return Mono.just(List.of());
×
546
        }
547

548
        private Optional<RelationshipListVO> getRelationshipListFromProperty(AdditionalPropertyVO additionalPropertyVO) {
549
                if (additionalPropertyVO instanceof RelationshipListVO rsl) {
1✔
550
                        return Optional.of(rsl);
1✔
UNCOV
551
                } else if ((additionalPropertyVO instanceof PropertyVO propertyVO && ((PropertyVO) additionalPropertyVO).getValue() instanceof List<?> propertyListVO && !propertyListVO.isEmpty() && isRelationshipList(propertyVO))) {
×
UNCOV
552
                        RelationshipListVO relationshipListVO = new RelationshipListVO();
×
UNCOV
553
                        propertyListVO.stream()
×
554
                                        .map(value -> objectMapper.convertValue(value, RelationshipVO.class))
×
UNCOV
555
                                        .forEach(relationshipListVO::add);
×
UNCOV
556
                        return Optional.of(relationshipListVO);
×
557

558
                } else if (additionalPropertyVO instanceof PropertyListVO propertyListVO && !propertyListVO.isEmpty() && isRelationship(propertyListVO.get(0))) {
×
559
                        RelationshipListVO relationshipListVO = new RelationshipListVO();
×
560
                        propertyListVO.stream()
×
561
                                        .map(propertyVO -> objectMapper.convertValue(propertyVO.getValue(), RelationshipVO.class))
×
562
                                        .forEach(relationshipListVO::add);
×
UNCOV
563
                        return Optional.of(relationshipListVO);
×
564
                }
UNCOV
565
                return Optional.empty();
×
566
        }
567

568
        private Optional<RelationshipVO> getRelationshipFromProperty(AdditionalPropertyVO additionalPropertyVO) {
569
                if (additionalPropertyVO instanceof RelationshipVO relationshipVO) {
1✔
570
                        return Optional.of(relationshipVO);
1✔
571
                } else if (additionalPropertyVO instanceof PropertyVO propertyVO && isRelationship(propertyVO)) {
1✔
UNCOV
572
                        return Optional.of(objectMapper.convertValue(propertyVO.getValue(), RelationshipVO.class));
×
573
                }
574
                return Optional.empty();
1✔
575
        }
576

577
        private boolean isRelationship(PropertyVO testProperty) {
UNCOV
578
                return testProperty.getValue() instanceof Map<?, ?> valuesMap && valuesMap.get("type").equals(PropertyTypeVO.RELATIONSHIP.getValue());
×
579
        }
580

581
        /**
582
         * Check if the given method handles access to the unmapped properties
583
         */
584
        private boolean isUnmappedPropertiesSetter(Method method) {
585
                return Arrays.stream(method.getAnnotations()).anyMatch(UnmappedPropertiesSetter.class::isInstance);
1✔
586
        }
587

588
        private boolean isRelationshipList(PropertyVO testProperty) {
UNCOV
589
                return testProperty.getValue() instanceof List<?> valuesList &&
×
UNCOV
590
                                valuesList.stream()
×
UNCOV
591
                                                .allMatch(v -> {
×
UNCOV
592
                                                        if (v instanceof Map<?, ?> valueAsMap) {
×
UNCOV
593
                                                                return valueAsMap.get("type").equals(PropertyTypeVO.RELATIONSHIP.getValue());
×
594
                                                        }
UNCOV
595
                                                        return false;
×
596
                                                });
597
        }
598

599
        /**
600
         * Helper method for combining the evaluation of relationship entities to a single result lits
601
         *
602
         * @param relationshipVOStream    the relationships to evaluate
603
         * @param targetClass             target class of the relationship object
604
         * @param relationShipEntitiesMap map of the preevaluated relationship entities
605
         * @param <T>                     target class of the relationship
606
         * @return a single emitting the full list
607
         */
608
        private <T> Mono<List<T>> zipToList(Stream<RelationshipVO> relationshipVOStream, Class<T> targetClass, Map<String, EntityVO> relationShipEntitiesMap) {
609
                return Mono.zip(
1✔
610
                                relationshipVOStream.map(RelationshipVO::getObject)
1✔
611
                                                .filter(Objects::nonNull)
1✔
612
                                                .map(URI::toString)
1✔
613
                                                .map(relationShipEntitiesMap::get)
1✔
614
                                                .filter(Objects::nonNull)
1✔
615
                                                .map(entity -> fromEntityVO(entity, targetClass))
1✔
616
                                                .toList(),
1✔
617
                                oList -> Arrays.stream(oList).map(targetClass::cast).toList()
1✔
618
                );
619
        }
620

621
        /**
622
         * Method to translate a Map-Entry(e.g. NGSI-LD property) to a typed list as defined by the target object
623
         *
624
         * @param propertyVOS a list of properties
625
         * @param targetClass class to be used as type for the typed list
626
         * @param <T>         the type
627
         * @return a list of objects, mapping the relationship
628
         */
629
        private <T> List<T> propertyListToTargetClass(PropertyListVO propertyVOS, Class<T> targetClass) {
630
                return propertyVOS.stream().map(propertyEntry -> objectMapper.convertValue(propertyEntry.getValue(), targetClass)).toList();
1✔
631
        }
632

633
        /**
634
         * Retrieve the object from a relationship and return it as a java object of class T. All sub relationships will be evaluated, too.
635
         *
636
         * @param relationshipVO the relationship entry
637
         * @param targetClass    the target-class of the entry
638
         * @param <T>            the class
639
         * @return the actual object
640
         */
641
        private <T> Mono<T> getObjectFromRelationship(RelationshipVO relationshipVO, Class<T> targetClass, Map<String, EntityVO> relationShipEntitiesMap, Map<String, AdditionalPropertyVO> additionalPropertyVOMap) {
642
                Optional<EntityVO> optionalEntityVO = Optional.ofNullable(relationShipEntitiesMap.get(relationshipVO.getObject().toString()));
1✔
643
                if (optionalEntityVO.isEmpty() && !mappingProperties.isStrictRelationships()) {
1✔
644
                        try {
645
                                Constructor<T> objectConstructor = targetClass.getDeclaredConstructor(String.class);
1✔
646
                                T theObject = objectConstructor.newInstance(relationshipVO.getObject().toString());
1✔
647
                                // return the empty object
648
                                return Mono.just(theObject);
1✔
UNCOV
649
                        } catch (InvocationTargetException | InstantiationException | IllegalAccessException |
×
650
                                         NoSuchMethodException e) {
UNCOV
651
                                return Mono.error(new MappingException(String.format("Was not able to instantiate %s with a string parameter.", targetClass), e));
×
652
                        }
653
                } else if (optionalEntityVO.isEmpty()) {
1✔
654
                        return Mono.error(new MappingException(String.format("Was not able to resolve the relationship %s", relationshipVO.getObject())));
1✔
655
                }
656

657
                var entityVO = optionalEntityVO.get();
1✔
658
                //merge with override properties
659
                if (additionalPropertyVOMap != null) {
1✔
UNCOV
660
                        entityVO.getAdditionalProperties().putAll(additionalPropertyVOMap);
×
661
                }
662
                return fromEntityVO(entityVO, targetClass);
1✔
663
        }
664

665
        /**
666
         * Return the type of the setter's parameter.
667
         */
668
        private Class<?> getParameterType(Class<?>[] arrayOfClasses) {
669
                if (arrayOfClasses.length != 1) {
1✔
UNCOV
670
                        throw new MappingException("Setter method should only have one parameter declared.");
×
671
                }
672
                return arrayOfClasses[0];
1✔
673
        }
674

675
        /**
676
         * Get the setter method for the given property at the entity.
677
         */
678
        private <T> Optional<Method> getCorrespondingSetterMethod(T entity, String propertyName) {
679
                return getAttributeSettersMethods(entity).stream().filter(m ->
1✔
680
                                                getAttributeSetterAnnotation(m)
1✔
681
                                                                .map(attributeSetter -> attributeSetter.targetName().equals(propertyName)).orElse(false))
1✔
682
                                .findFirst();
1✔
683
        }
684

685
        /**
686
         * Get all attribute setters for the given entity
687
         */
688
        private <T> List<Method> getAttributeSettersMethods(T entity) {
689
                return Arrays.stream(entity.getClass().getMethods()).filter(m -> getAttributeSetterAnnotation(m).isPresent()).toList();
1✔
690
        }
691

692
        private <T> Optional<Method> getUnmappedPropertiesSetter(T entity) {
693
                return Arrays.stream(entity.getClass().getMethods())
1✔
694
                                .filter(this::isUnmappedPropertiesSetter).findAny();
1✔
695
        }
696

697
        private <T> Map<String, Method> getAttributeSetterMethodMap(T entity) {
698
                return Arrays.stream(entity.getClass().getMethods())
1✔
699
                                .filter(m -> getAttributeSetterAnnotation(m).isPresent())
1✔
700
                                .collect(Collectors.toMap(m -> getAttributeSetterAnnotation(m).get().targetName(), m -> m));
1✔
701
        }
702

703
        /**
704
         * Get the attribute setter annotation from the given method, if it exists.
705
         */
706
        private Optional<AttributeSetter> getAttributeSetterAnnotation(Method m) {
707
                return Arrays.stream(m.getAnnotations()).filter(AttributeSetter.class::isInstance)
1✔
708
                                .findFirst()
1✔
709
                                .map(AttributeSetter.class::cast);
1✔
710
        }
711

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