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

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

06 Feb 2025 02:01PM UTC coverage: 75.601% (-2.5%) from 78.133%
#268

push

web-flow
Merge pull request #78 from wistefan/fix-lists

fix the mapping

5 of 26 new or added lines in 1 file covered. (19.23%)

6 existing lines in 2 files now uncovered.

598 of 791 relevant lines covered (75.6%)

0.76 hits per line

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

78.06
/src/main/java/io/github/wistefan/mapping/JavaObjectMapper.java
1
package io.github.wistefan.mapping;
2

3
import io.github.wistefan.mapping.annotations.*;
4
import lombok.RequiredArgsConstructor;
5
import lombok.extern.slf4j.Slf4j;
6
import org.fiware.ngsi.model.*;
7

8
import javax.inject.Singleton;
9
import java.lang.annotation.Annotation;
10
import java.lang.reflect.InvocationTargetException;
11
import java.lang.reflect.Method;
12
import java.net.URI;
13
import java.util.*;
14
import java.util.stream.Collectors;
15
import java.util.stream.Stream;
16

17
/**
18
 * Mapper to handle translation from Java-Objects into NGSI-LD entities.
19
 */
20
@Slf4j
1✔
21
@Singleton
22
@RequiredArgsConstructor
1✔
23
public class JavaObjectMapper extends Mapper {
24

25
        // name of the property containing the ID
26
        private static final String ID_PROPERTY = "id";
27
        private final MappingProperties mappingProperties;
28

29
        public static final String NO_MAPPING_DEFINED_FOR_METHOD_TEMPLATE = "No mapping defined for method %s";
30
        public static final String WAS_NOT_ABLE_INVOKE_METHOD_TEMPLATE = "Was not able invoke method %s on %s";
31

32

33
        /**
34
         * Translate the attribute path for the given object into the path in the ngsi-ld model.
35
         *
36
         * @param attributePath the original path
37
         * @param tClass        class to use for translation
38
         * @return the path in ngsi-ld and the type of the target attribute
39
         */
40
        public static <T> NgsiLdAttribute getNGSIAttributePath(List<String> attributePath, Class<T> tClass) {
41
                List<String> ngsiAttributePath = new ArrayList<>();
1✔
42
                QueryAttributeType type = QueryAttributeType.STRING;
1✔
43
                String currentAttribute = attributePath.get(0);
1✔
44
                if (currentAttribute.equals(ID_PROPERTY)) {
1✔
45
                        ngsiAttributePath.add(ID_PROPERTY);
1✔
46
                        return new NgsiLdAttribute(ngsiAttributePath, QueryAttributeType.STRING);
1✔
47
                }
48
                if (isMappingEnabled(tClass).isPresent()) {
1✔
49
                        // for mapped properties, we have to climb down the property names
50
                        // we need the setter in case of type-erasure and the getter in all other cases
51
                        Optional<Method> setter = getSetterMethodByName(tClass, currentAttribute)
1✔
52
                                        .filter(m -> getAttributeSetter(m.getAnnotations()).isPresent())
1✔
53
                                        .findFirst();
1✔
54
                        Optional<Method> getter = getGetterMethodByName(tClass, currentAttribute)
1✔
55
                                        .filter(m -> getAttributeGetter(m.getAnnotations()).isPresent())
1✔
56
                                        .findFirst();
1✔
57
                        if (setter.isPresent() && getter.isPresent()) {
1✔
58
                                Method setterMethod = setter.get();
1✔
59
                                Method getterMethod = getter.get();
1✔
60
                                // no need to check again
61
                                AttributeSetter setterAnnotation = getAttributeSetter(setterMethod.getAnnotations()).get();
1✔
62
                                ngsiAttributePath.add(setterAnnotation.targetName());
1✔
63
                                type = fromClass(getterMethod.getReturnType());
1✔
64
                                if (attributePath.size() > 1) {
1✔
65
                                        List<String> subPaths = attributePath.subList(1, attributePath.size());
1✔
66
                                        if (setterAnnotation.targetClass() != Object.class) {
1✔
67
                                                NgsiLdAttribute subAttribute = getNGSIAttributePath(subPaths, setterAnnotation.targetClass());
1✔
68
                                                ngsiAttributePath.addAll(subAttribute.path());
1✔
69
                                                type = subAttribute.type();
1✔
70
                                        } else {
1✔
71
                                                NgsiLdAttribute subAttribute = getNGSIAttributePath(subPaths, getterMethod.getReturnType());
1✔
72
                                                ngsiAttributePath.addAll(subAttribute.path());
1✔
73
                                                type = subAttribute.type();
1✔
74
                                        }
75
                                }
76
                        } else {
1✔
77
                                log.warn("No corresponding field does exist for attribute {} on {}.", currentAttribute,
×
78
                                                tClass.getCanonicalName());
×
79
                        }
80
                } else {
1✔
81
                        // we can use the "plain" object field-names, no additional mapping happens anymore
82
                        ngsiAttributePath.addAll(attributePath);
1✔
83
                        type = evaluateType(attributePath, tClass);
1✔
84
                }
85
                return new NgsiLdAttribute(ngsiAttributePath, type);
1✔
86
        }
87

88
        private static QueryAttributeType evaluateType(List<String> path, Class<?> tClass) {
89
                Class<?> currentClass = tClass;
1✔
90
                for (String s : path) {
1✔
91
                        try {
92
                                Optional<Class<?>> optionalReturn = getGetterMethodByName(currentClass, s).findAny()
1✔
93
                                                .map(Method::getReturnType);
1✔
94
                                if (optionalReturn.isPresent()) {
1✔
95
                                        currentClass = optionalReturn.get();
1✔
96
                                } else {
97
                                        currentClass = currentClass.getField(s).getType();
×
98
                                }
99
                        } catch (NoSuchFieldException e) {
×
100
                                throw new MappingException(String.format("No field %s exists for %s.", s, tClass.getCanonicalName()),
×
101
                                                e);
102
                        }
1✔
103
                }
1✔
104
                return fromClass(currentClass);
1✔
105
        }
106

107
        private static QueryAttributeType fromClass(Class<?> tClass) {
108
                if (Number.class.isAssignableFrom(tClass)) {
1✔
109
                        return QueryAttributeType.NUMBER;
1✔
110
                } else if (Boolean.class.isAssignableFrom(tClass)) {
1✔
111
                        return QueryAttributeType.BOOLEAN;
1✔
112
                }
113
                return QueryAttributeType.STRING;
1✔
114

115
        }
116

117
        public static <T> Stream<Method> getSetterMethodByName(Class<T> tClass, String propertyName) {
118
                return Arrays.stream(tClass.getMethods())
1✔
119
                                .filter(m -> getCorrespondingSetterFieldName(m.getName()).equals(propertyName));
1✔
120
        }
121

122
        public static <T> Stream<Method> getGetterMethodByName(Class<T> tClass, String propertyName) {
123
                return Arrays.stream(tClass.getMethods())
1✔
124
                                .filter(m -> getCorrespondingGetterFieldName(m.getName()).equals(propertyName));
1✔
125
        }
126

127
        private static String getCorrespondingGetterFieldName(String methodName) {
128
                var fieldName = "";
1✔
129
                if (methodName.matches("^get[A-Z].*")) {
1✔
130
                        fieldName = methodName.replaceFirst("get", "");
1✔
131
                } else if (methodName.matches("^is[A-Z].*")) {
1✔
132
                        fieldName = methodName.replaceFirst("is", "");
×
133
                } else {
134
                        log.debug("The method {} is neither a get or is.", methodName);
1✔
135
                        return fieldName;
1✔
136
                }
137
                return fieldName.substring(0, 1).toLowerCase() + fieldName.substring(1);
1✔
138
        }
139

140
        private static String getCorrespondingSetterFieldName(String methodName) {
141
                var fieldName = "";
1✔
142
                if (methodName.matches("^set[A-Z].*")) {
1✔
143
                        fieldName = methodName.replaceFirst("set", "");
1✔
144
                } else if (methodName.matches("^is[A-Z].*")) {
1✔
145
                        fieldName = methodName.replaceFirst("is", "");
×
146
                } else {
147
                        log.debug("The method {} is neither a set or is.", methodName);
1✔
148
                        return fieldName;
1✔
149
                }
150
                return fieldName.substring(0, 1).toLowerCase() + fieldName.substring(1);
1✔
151
        }
152

153
        /**
154
         * Translate the given object into an Entity.
155
         *
156
         * @param entity the object representing the entity
157
         * @param <T>    class of the entity
158
         * @return the NGSI-LD entity object
159
         */
160
        public <T> EntityVO toEntityVO(T entity) {
161
                isMappingEnabled(entity.getClass())
1✔
162
                                .orElseThrow(() -> new UnsupportedOperationException(
1✔
163
                                                String.format("Generic mapping to NGSI-LD entities is not supported for object %s",
1✔
164
                                                                entity)));
165

166
                List<Method> entityIdMethod = new ArrayList<>();
1✔
167
                List<Method> entityTypeMethod = new ArrayList<>();
1✔
168
                List<Method> propertyMethods = new ArrayList<>();
1✔
169
                List<Method> propertyListMethods = new ArrayList<>();
1✔
170
                List<Method> relationshipMethods = new ArrayList<>();
1✔
171
                List<Method> relationshipListMethods = new ArrayList<>();
1✔
172
                List<Method> geoPropertyMethods = new ArrayList<>();
1✔
173
                List<Method> geoPropertyListMethods = new ArrayList<>();
1✔
174
                List<Method> unmappedPropertiesGetterMethods = new ArrayList<>();
1✔
175

176
                Arrays.stream(entity.getClass().getMethods()).forEach(method -> {
1✔
177
                        if (isEntityIdMethod(method)) {
1✔
178
                                entityIdMethod.add(method);
1✔
179
                        } else if (isEntityTypeMethod(method)) {
1✔
180
                                entityTypeMethod.add(method);
1✔
181
                        } else if (isUnmappedPropertiesGetter(method)) {
1✔
182
                                unmappedPropertiesGetterMethods.add(method);
1✔
183
                        } else {
184
                                getAttributeGetter(method.getAnnotations()).ifPresent(annotation -> {
1✔
185
                                        switch (annotation.value()) {
1✔
186
                                                case PROPERTY -> propertyMethods.add(method);
1✔
187
                                                // We handle property lists the same way as properties, since it is mapped as a property which value is a json array.
188
                                                // A real NGSI-LD property list would require a datasetId, that is not provided here.
189
                                                case PROPERTY_LIST -> propertyMethods.add(method);
1✔
190
                                                case GEO_PROPERTY -> geoPropertyMethods.add(method);
×
191
                                                case RELATIONSHIP -> relationshipMethods.add(method);
1✔
192
                                                case GEO_PROPERTY_LIST -> geoPropertyListMethods.add(method);
×
193
                                                case RELATIONSHIP_LIST -> relationshipListMethods.add(method);
1✔
194
                                                default -> throw new UnsupportedOperationException(
×
195
                                                                String.format("Mapping target %s is not supported.", annotation.value()));
×
196
                                        }
197
                                });
1✔
198
                        }
199
                });
1✔
200

201
                if (entityIdMethod.size() != 1) {
1✔
202
                        throw new MappingException(
1✔
203
                                        String.format("The provided object declares %s id methods, exactly one is expected.",
1✔
204
                                                        entityIdMethod.size()));
1✔
205
                }
206
                if (entityTypeMethod.size() != 1) {
1✔
207
                        throw new MappingException(
1✔
208
                                        String.format("The provided object declares %s type methods, exactly one is expected.",
1✔
209
                                                        entityTypeMethod.size()));
1✔
210

211
                }
212

213
                if (unmappedPropertiesGetterMethods.isEmpty()) {
1✔
214
                        return buildEntity(entity, entityIdMethod.get(0), entityTypeMethod.get(0), Optional.empty(), propertyMethods,
1✔
215
                                        propertyListMethods,
216
                                        geoPropertyMethods, relationshipMethods, relationshipListMethods);
217
                } else {
218
                        return buildEntity(entity, entityIdMethod.get(0), entityTypeMethod.get(0), Optional.of(unmappedPropertiesGetterMethods.get(0)), propertyMethods,
1✔
219
                                        propertyListMethods,
220
                                        geoPropertyMethods, relationshipMethods, relationshipListMethods);
221
                }
222
        }
223

224
        /**
225
         * Build the entity from its declared methods.
226
         */
227
        private <T> EntityVO buildEntity(T entity, Method entityIdMethod, Method entityTypeMethod, Optional<Method> unmappedPropertiesMethod,
228
                                                                         List<Method> propertyMethods, List<Method> propertyListMethods,
229
                                                                         List<Method> geoPropertyMethods,
230
                                                                         List<Method> relationshipMethods, List<Method> relationshipListMethods) {
231

232
                EntityVO entityVO = new EntityVO();
1✔
233
                entityVO.setAtContext(mappingProperties.getContextUrl());
1✔
234

235
                // TODO: include extraction via annotation for all well-known attributes
236
                entityVO.setOperationSpace(null);
1✔
237
                entityVO.setObservationSpace(null);
1✔
238
                entityVO.setLocation(null);
1✔
239

240
                try {
241
                        Object entityIdObject = entityIdMethod.invoke(entity);
1✔
242
                        if (!(entityIdObject instanceof URI)) {
1✔
243
                                throw new MappingException(
1✔
244
                                                String.format("The entityId method does not return a valid URI for entity %s.", entity));
1✔
245
                        }
246
                        entityVO.id((URI) entityIdObject);
1✔
247

248
                        Object entityTypeObject = entityTypeMethod.invoke(entity);
1✔
249
                        if (!(entityTypeObject instanceof String)) {
1✔
250
                                throw new MappingException("The entityType method does not return a valid String.");
1✔
251
                        }
252
                        entityVO.setType((String) entityTypeObject);
1✔
253
                } catch (IllegalAccessException | InvocationTargetException e) {
1✔
254
                        throw new MappingException(String.format(WAS_NOT_ABLE_INVOKE_METHOD_TEMPLATE, "unknown-method", entity),
1✔
255
                                        e);
256
                }
1✔
257

258
                Map<String, AdditionalPropertyVO> additionalProperties = new LinkedHashMap<>();
1✔
259
                additionalProperties.putAll(buildProperties(entity, propertyMethods));
1✔
260
                additionalProperties.putAll(buildPropertyList(entity, propertyListMethods));
1✔
261
                additionalProperties.putAll(buildGeoProperties(entity, geoPropertyMethods));
1✔
262
                if (unmappedPropertiesMethod.isPresent()) {
1✔
263
                        additionalProperties.putAll(buildUnmappedProperties(entity, unmappedPropertiesMethod.get()));
1✔
264
                }
265
                Map<String, RelationshipVO> relationshipVOMap = buildRelationships(entity, relationshipMethods);
1✔
266
                Map<String, RelationshipListVO> relationshipListVOMap = buildRelationshipList(entity,
1✔
267
                                relationshipListMethods);
268
                // we need to post-process the relationships, since orion-ld only accepts dataset-ids for lists > 1
269
                relationshipVOMap.entrySet().stream().forEach(e -> e.getValue().setDatasetId(null));
1✔
270
                relationshipListVOMap.entrySet().stream().forEach(e -> {
1✔
271
                        if (e.getValue().size() == 1) {
1✔
272
                                e.getValue().get(0).setDatasetId(null);
1✔
273
                        }
274
                });
1✔
275

276
                additionalProperties.putAll(relationshipVOMap);
1✔
277
                additionalProperties.putAll(relationshipListVOMap);
1✔
278

279
                additionalProperties.forEach(entityVO::setAdditionalProperties);
1✔
280

281
                return entityVO;
1✔
282
        }
283

284
        /**
285
         * Check if the given method defines the entity type
286
         */
287
        private boolean isEntityTypeMethod(Method method) {
288
                return Arrays.stream(method.getAnnotations()).anyMatch(EntityType.class::isInstance);
1✔
289
        }
290

291
        /**
292
         * Check if the given method defines the entity id
293
         */
294
        private boolean isEntityIdMethod(Method method) {
295
                return Arrays.stream(method.getAnnotations()).anyMatch(EntityId.class::isInstance);
1✔
296
        }
297

298
        /**
299
         * Check if the given method handles access to the unmapped properties
300
         */
301
        private boolean isUnmappedPropertiesGetter(Method method) {
302
                return Arrays.stream(method.getAnnotations()).anyMatch(UnmappedPropertiesGetter.class::isInstance);
1✔
303
        }
304

305
        /**
306
         * Build a relationship from the declared methods
307
         */
308
        private <T> Map<String, RelationshipVO> buildRelationships(T entity, List<Method> relationshipMethods) {
309
                return relationshipMethods.stream()
1✔
310
                                .map(method -> methodToRelationshipEntry(entity, method))
1✔
311
                                .filter(Optional::isPresent)
1✔
312
                                .map(Optional::get)
1✔
313
                                .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
1✔
314
        }
315

316
        /**
317
         * Build a list of relationships from the declared methods
318
         */
319
        private <
320
                        T> Map<String, RelationshipListVO> buildRelationshipList(T entity, List<Method> relationshipListMethods) {
321
                return relationshipListMethods.stream()
1✔
322
                                .map(relationshipMethod -> methodToRelationshipListEntry(entity, relationshipMethod))
1✔
323
                                .filter(Optional::isPresent)
1✔
324
                                .map(Optional::get)
1✔
325
                                .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
1✔
326
        }
327

328
        /*
329
         * Build a list of properties from the declared methods
330
         */
331
        private <T> Map<String, PropertyListVO> buildPropertyList(T entity, List<Method> propertyListMethods) {
332
                return propertyListMethods.stream()
1✔
333
                                .map(propertyListMethod -> methodToPropertyListEntry(entity, propertyListMethod))
1✔
334
                                .filter(Optional::isPresent)
1✔
335
                                .map(Optional::get)
1✔
336
                                .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
1✔
337
        }
338

339
        /**
340
         * Build geoproperties from the declared methods
341
         */
342
        private <T> Map<String, GeoPropertyVO> buildGeoProperties(T entity, List<Method> geoPropertyMethods) {
343
                return geoPropertyMethods.stream()
1✔
344
                                .map(geoPropertyMethod -> methodToGeoPropertyEntry(entity, geoPropertyMethod))
1✔
345
                                .filter(Optional::isPresent)
1✔
346
                                .map(Optional::get)
1✔
347
                                .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
1✔
348
        }
349

350
        private Map.Entry<String, AdditionalPropertyVO> unmappedPropertyToAdditionalProperty(UnmappedProperty unmappedProperty) {
351
                AdditionalPropertyVO additionalPropertyVO = objectToAdditionalProperty(unmappedProperty.getValue());
1✔
352
                return new AbstractMap.SimpleEntry<>(unmappedProperty.getName(), additionalPropertyVO);
1✔
353

354
        }
355

356
        private AdditionalPropertyVO mapToRelationship(Map<?, ?> objectMap) {
357
                RelationshipVO relationshipVO = new RelationshipVO();
1✔
358
                if (objectMap.get(ID_PROPERTY) instanceof String id) {
1✔
359
                        relationshipVO.setObject(URI.create(id));
1✔
360
                } else {
361
                        throw new MappingException(String.format("The id is not a valid id. Object is %s", objectMap.get(ID_PROPERTY)));
×
362
                }
363
                objectMap.forEach((key, value) -> {
1✔
364
                        if (key instanceof String name) {
1✔
365
                                if (name.equals(ID_PROPERTY)) {
1✔
366
                                        return;
1✔
367
                                }
368
                                relationshipVO.setAdditionalProperties(name, objectToAdditionalProperty(value));
1✔
369
                        } else {
370
                                throw new MappingException(String.format("Only string keys are supported, but was %s", key));
×
371
                        }
372
                });
1✔
373
                return relationshipVO;
1✔
374
        }
375

376
        private AdditionalPropertyVO objectToAdditionalProperty(Object o) {
377
                if (o instanceof Map<?, ?> objectMap) {
1✔
378
                        if (objectMap.containsKey(ID_PROPERTY)) {
1✔
379
                                // contains key "id" -> relationship
380
                                return mapToRelationship(objectMap);
1✔
381
                        } else {
382
                                PropertyVO propertyVO = new PropertyVO();
1✔
383
                                Map<String, AdditionalPropertyVO> values = new HashMap<>();
1✔
384
                                objectMap.forEach((key, value) -> {
1✔
385
                                        if (key instanceof String name) {
1✔
386
                                                propertyVO.setAdditionalProperties(name, objectToAdditionalProperty(value));
1✔
387
                                                values.put(name, objectToAdditionalProperty(value));
1✔
388
                                        } else {
389
                                                throw new MappingException(String.format("Only string keys are supported, but was %s", key));
×
390
                                        }
391
                                });
1✔
392
                                return propertyVO.value(values);
1✔
393
                        }
394
                } else if (o instanceof List<?> objectList && !objectList.isEmpty()) {
1✔
395
                        if (isPlain(objectList.get(0))) {
1✔
396
                                return new PropertyVO().value(objectList);
1✔
397
                        } else {
NEW
398
                                PropertyListVO propertyVOS = new PropertyListVO();
×
NEW
399
                                RelationshipListVO relationshipVOS = new RelationshipListVO();
×
400
                                // as of now, we don't support property lists of property lists
NEW
401
                                objectList.stream()
×
NEW
402
                                                .map(this::objectToAdditionalProperty)
×
NEW
403
                                                .forEach(apvo -> {
×
NEW
404
                                                        if (apvo instanceof PropertyVO pvo) {
×
NEW
405
                                                                propertyVOS.add(pvo);
×
406
                                                        }
NEW
407
                                                        if (apvo instanceof RelationshipVO rvo) {
×
NEW
408
                                                                relationshipVOS.add(rvo);
×
409
                                                        }
NEW
410
                                                });
×
NEW
411
                                if (!propertyVOS.isEmpty() && !relationshipVOS.isEmpty()) {
×
NEW
412
                                        throw new MappingException("Mixed lists are not supported");
×
413
                                }
NEW
414
                                if (!propertyVOS.isEmpty()) {
×
NEW
415
                                        return propertyVOS;
×
416
                                }
NEW
417
                                if (!relationshipVOS.isEmpty()) {
×
NEW
418
                                        return relationshipVOS;
×
419
                                }
UNCOV
420
                                return propertyVOS;
×
421
                        }
422
                } else {
423
                        PropertyVO propertyVO = new PropertyVO().value(o);
1✔
424
                        return propertyVO;
1✔
425
                }
426
        }
427

428
        private boolean isPlain(Object o) {
429
                if (o instanceof Number) {
1✔
430
                        return true;
1✔
431
                }
NEW
432
                if (o instanceof String) {
×
NEW
433
                        return true;
×
434
                }
NEW
435
                if (o instanceof Boolean) {
×
NEW
436
                        return true;
×
437
                }
NEW
438
                return false;
×
439
        }
440

441
        private <T> Map<String, AdditionalPropertyVO> buildUnmappedProperties(T entity, Method method) {
442
                try {
443
                        Object unmappedProperties = method.invoke(entity);
1✔
444
                        if (unmappedProperties == null) {
1✔
445
                                return Map.of();
×
446
                        } else if (unmappedProperties instanceof List<?> unmappedPropertiesList) {
1✔
447
                                List<UnmappedProperty> theList = unmappedPropertiesList
1✔
448
                                                .stream()
1✔
449
                                                .filter(UnmappedProperty.class::isInstance)
1✔
450
                                                .map(UnmappedProperty.class::cast)
1✔
451
                                                .toList();
1✔
452
                                return theList.stream()
1✔
453
                                                .map(this::unmappedPropertyToAdditionalProperty)
1✔
454
                                                .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
1✔
455
                        } else {
456
                                throw new MappingException("Only lists of additional Properties are supported.");
×
457
                        }
458

459
                } catch (IllegalAccessException | InvocationTargetException e) {
×
460
                        throw new MappingException(String.format(WAS_NOT_ABLE_INVOKE_METHOD_TEMPLATE, method, entity));
×
461
                }
462
        }
463

464

465
        /**
466
         * Build properties from the declared methods
467
         */
468
        private <T> Map<String, PropertyVO> buildProperties(T entity, List<Method> propertyMethods) {
469
                return propertyMethods.stream()
1✔
470
                                .map(propertyMethod -> methodToPropertyEntry(entity, propertyMethod))
1✔
471
                                .filter(Optional::isPresent)
1✔
472
                                .map(Optional::get)
1✔
473
                                .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
1✔
474
        }
475

476
        /**
477
         * Return method defining the object of the relationship for the given entity, if exists.
478
         */
479
        private <T> Optional<Method> getRelationshipObjectMethod(T entity) {
480
                return Arrays.stream(entity.getClass().getMethods()).filter(this::isRelationShipObject).findFirst();
1✔
481
        }
482

483
        /**
484
         * Return method defining the datasetid for the given entity, if exists.
485
         */
486
        private <T> Optional<Method> getDatasetIdMethod(T entity) {
487
                return Arrays.stream(entity.getClass().getMethods()).filter(this::isDatasetId).findFirst();
1✔
488
        }
489

490
        /**
491
         * Get all methods declared as attribute getters.
492
         */
493
        private <T> List<Method> getAttributeGettersMethods(T entity) {
494
                return Arrays.stream(entity.getClass().getMethods())
1✔
495
                                .filter(m -> getAttributeGetterAnnotation(m).isPresent())
1✔
496
                                .toList();
1✔
497
        }
498

499
        /**
500
         * return the {@link  AttributeGetter} annotation for the method if there is such.
501
         */
502
        private Optional<AttributeGetter> getAttributeGetterAnnotation(Method m) {
503
                return Arrays.stream(m.getAnnotations()).filter(AttributeGetter.class::isInstance).findFirst()
1✔
504
                                .map(AttributeGetter.class::cast);
1✔
505
        }
506

507
        /**
508
         * Find the attribute getter from all the annotations.
509
         */
510
        private static Optional<AttributeGetter> getAttributeGetter(Annotation[] annotations) {
511
                return Arrays.stream(annotations).filter(AttributeGetter.class::isInstance).map(AttributeGetter.class::cast)
1✔
512
                                .findFirst();
1✔
513
        }
514

515
        /**
516
         * Find the attribute setter from all the annotations.
517
         */
518
        private static Optional<AttributeSetter> getAttributeSetter(Annotation[] annotations) {
519
                return Arrays.stream(annotations).filter(AttributeSetter.class::isInstance).map(AttributeSetter.class::cast)
1✔
520
                                .findFirst();
1✔
521
        }
522

523
        /**
524
         * Check if the given method is declared to be used as object of a relationship
525
         */
526
        private boolean isRelationShipObject(Method m) {
527
                return Arrays.stream(m.getAnnotations()).anyMatch(RelationshipObject.class::isInstance);
1✔
528
        }
529

530
        /**
531
         * Check if the given method is declared to be used as datasetId
532
         */
533
        private boolean isDatasetId(Method m) {
534
                return Arrays.stream(m.getAnnotations()).anyMatch(DatasetId.class::isInstance);
1✔
535
        }
536

537
        /**
538
         * Build a property entry from the given method on the entity
539
         */
540
        private <T> Optional<Map.Entry<String, PropertyVO>> methodToPropertyEntry(T entity, Method method) {
541
                try {
542
                        Object propertyObject = method.invoke(entity);
1✔
543
                        if (propertyObject == null) {
1✔
544
                                return Optional.empty();
×
545
                        }
546
                        AttributeGetter attributeMapping = getAttributeGetter(method.getAnnotations()).orElseThrow(
1✔
547
                                        () -> new MappingException(String.format(NO_MAPPING_DEFINED_FOR_METHOD_TEMPLATE, method)));
×
548

549
                        PropertyVO propertyVO = new PropertyVO();
1✔
550
                        propertyVO.setValue(propertyObject);
1✔
551

552
                        return Optional.of(new AbstractMap.SimpleEntry<>(attributeMapping.targetName(), propertyVO));
1✔
553
                } catch (IllegalAccessException | InvocationTargetException e) {
1✔
554
                        throw new MappingException(String.format(WAS_NOT_ABLE_INVOKE_METHOD_TEMPLATE, method, entity));
1✔
555
                }
556
        }
557

558
        /**
559
         * Build a geo-property entry from the given method on the entity
560
         */
561
        private <T> Optional<Map.Entry<String, GeoPropertyVO>> methodToGeoPropertyEntry(T entity, Method method) {
562
                try {
563
                        Object o = method.invoke(entity);
×
564
                        if (o == null) {
×
565
                                return Optional.empty();
×
566
                        }
567
                        AttributeGetter attributeMapping = getAttributeGetter(method.getAnnotations()).orElseThrow(
×
568
                                        () -> new MappingException(String.format(NO_MAPPING_DEFINED_FOR_METHOD_TEMPLATE, method)));
×
569
                        GeoPropertyVO geoPropertyVO = new GeoPropertyVO();
×
570
                        geoPropertyVO.setValue(o);
×
571
                        return Optional.of(new AbstractMap.SimpleEntry<>(attributeMapping.targetName(), geoPropertyVO));
×
572
                } catch (IllegalAccessException | InvocationTargetException e) {
×
573
                        throw new MappingException(String.format(WAS_NOT_ABLE_INVOKE_METHOD_TEMPLATE, method, entity));
×
574
                }
575
        }
576

577
        /**
578
         * Build a relationship entry from the given method on the entity
579
         */
580
        private <T> Optional<Map.Entry<String, RelationshipVO>> methodToRelationshipEntry(T entity, Method method) {
581
                try {
582
                        Object relationShipObject = method.invoke(entity);
1✔
583
                        if (relationShipObject == null) {
1✔
584
                                return Optional.empty();
×
585
                        }
586
                        RelationshipVO relationshipVO = getRelationshipVO(method, relationShipObject);
1✔
587
                        AttributeGetter attributeMapping = getAttributeGetter(method.getAnnotations()).orElseThrow(
1✔
588
                                        () -> new MappingException(String.format(NO_MAPPING_DEFINED_FOR_METHOD_TEMPLATE, method)));
×
589
                        return Optional.of(new AbstractMap.SimpleEntry<>(attributeMapping.targetName(), relationshipVO));
1✔
590
                } catch (IllegalAccessException | InvocationTargetException e) {
1✔
591
                        throw new MappingException(String.format(WAS_NOT_ABLE_INVOKE_METHOD_TEMPLATE, method, entity));
1✔
592
                }
593
        }
594

595
        /**
596
         * Build a relationship list entry from the given method on the entity
597
         */
598
        private <T> Optional<Map.Entry<String, RelationshipListVO>> methodToRelationshipListEntry(T entity, Method method) {
599
                try {
600
                        Object o = method.invoke(entity);
1✔
601
                        if (o == null) {
1✔
602
                                return Optional.empty();
1✔
603
                        }
604
                        if (!(o instanceof List)) {
1✔
605
                                throw new MappingException(
1✔
606
                                                String.format("Relationship list method %s::%s did not return a List.", entity, method));
1✔
607
                        }
608
                        List<Object> entityObjects = (List) o;
1✔
609

610
                        AttributeGetter attributeGetter = getAttributeGetter(method.getAnnotations()).orElseThrow(
1✔
611
                                        () -> new MappingException(String.format(NO_MAPPING_DEFINED_FOR_METHOD_TEMPLATE, method)));
×
612
                        RelationshipListVO relationshipVOS = new RelationshipListVO();
1✔
613

614
                        relationshipVOS.addAll(entityObjects.stream()
1✔
615
                                        .filter(Objects::nonNull)
1✔
616
                                        .map(entityObject -> getRelationshipVO(method, entityObject))
1✔
617
                                        .toList());
1✔
618
                        return Optional.of(new AbstractMap.SimpleEntry<>(attributeGetter.targetName(), relationshipVOS));
1✔
619
                } catch (IllegalAccessException | InvocationTargetException e) {
1✔
620
                        throw new MappingException(String.format(WAS_NOT_ABLE_INVOKE_METHOD_TEMPLATE, method, entity));
1✔
621
                }
622
        }
623

624
        /**
625
         * Get the relationship for the given method and relationship object
626
         */
627
        private RelationshipVO getRelationshipVO(Method method, Object relationShipObject) {
628
                try {
629

630
                        Method objectMethod = getRelationshipObjectMethod(relationShipObject).orElseThrow(
1✔
631
                                        () -> new MappingException(
1✔
632
                                                        String.format("The relationship %s-%s does not provide an object method.",
1✔
633
                                                                        relationShipObject, method)));
634
                        Object objectObject = objectMethod.invoke(relationShipObject);
1✔
635
                        if (!(objectObject instanceof URI)) {
1✔
636
                                throw new MappingException(
1✔
637
                                                String.format("The object %s of the relationship is not a URI.", relationShipObject));
1✔
638
                        }
639

640
                        Method datasetIdMethod = getDatasetIdMethod(relationShipObject).orElseThrow(() -> new MappingException(
1✔
641
                                        String.format("The relationship %s-%s does not provide a datasetId method.", relationShipObject,
×
642
                                                        method)));
643
                        Object datasetIdObject = datasetIdMethod.invoke(relationShipObject);
1✔
644
                        if (!(datasetIdObject instanceof URI)) {
1✔
645
                                throw new MappingException(
1✔
646
                                                String.format("The datasetId %s of the relationship is not a URI.", relationShipObject));
1✔
647
                        }
648
                        RelationshipVO relationshipVO = new RelationshipVO();
1✔
649
                        relationshipVO.setObject((URI) objectObject);
1✔
650
                        relationshipVO.setDatasetId((URI) datasetIdObject);
1✔
651

652
                        // get additional properties. We do not support more depth/complexity for now
653
                        Map<String, AdditionalPropertyVO> additionalProperties = getAttributeGettersMethods(
1✔
654
                                        relationShipObject).stream()
1✔
655
                                        .map(getterMethod -> getAdditionalPropertyEntryFromMethod(relationShipObject, getterMethod))
1✔
656
                                        .filter(Optional::isPresent)
1✔
657
                                        .map(Optional::get)
1✔
658
                                        .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
1✔
659

660
                        additionalProperties.forEach(relationshipVO::setAdditionalProperties);
1✔
661

662
                        return relationshipVO;
1✔
663
                } catch (IllegalAccessException | InvocationTargetException e) {
×
664
                        throw new MappingException(
×
665
                                        String.format(WAS_NOT_ABLE_INVOKE_METHOD_TEMPLATE, method, relationShipObject));
×
666
                }
667
        }
668

669
        /**
670
         * Get all additional properties for the object of the relationship
671
         */
672
        private Optional<Map.Entry<String, PropertyVO>> getAdditionalPropertyEntryFromMethod(Object relationShipObject,
673
                                                                                                                                                                                 Method getterMethod) {
674
                Optional<AttributeGetter> optionalAttributeGetter = getAttributeGetter(getterMethod.getAnnotations());
1✔
675
                if (optionalAttributeGetter.isEmpty() || !optionalAttributeGetter.get().embedProperty()) {
1✔
676
                        return Optional.empty();
1✔
677
                }
678
                if (optionalAttributeGetter.get().value().equals(AttributeType.PROPERTY)) {
1✔
679
                        return methodToPropertyEntry(relationShipObject, getterMethod);
1✔
680
                } else {
681
                        return Optional.empty();
×
682
                }
683
        }
684

685
        /**
686
         * Build a property list entry from the given method on the entity
687
         */
688
        private <T> Optional<Map.Entry<String, PropertyListVO>> methodToPropertyListEntry(T entity, Method method) {
689
                try {
690
                        Object o = method.invoke(entity);
×
691
                        if (o == null) {
×
692
                                return Optional.empty();
×
693
                        }
694
                        if (!(o instanceof List)) {
×
695
                                throw new MappingException(
×
696
                                                String.format("Property list method %s::%s did not return a List.", entity, method));
×
697
                        }
698
                        AttributeGetter attributeMapping = getAttributeGetter(method.getAnnotations()).orElseThrow(
×
699
                                        () -> new MappingException(String.format(NO_MAPPING_DEFINED_FOR_METHOD_TEMPLATE, method)));
×
700
                        List<Object> entityObjects = (List) o;
×
701

702
                        PropertyListVO propertyVOS = new PropertyListVO();
×
703

704
                        propertyVOS.addAll(entityObjects.stream()
×
705
                                        .map(propertyObject -> {
×
706
                                                PropertyVO propertyVO = new PropertyVO();
×
707
                                                propertyVO.setValue(propertyObject);
×
708
                                                return propertyVO;
×
709
                                        })
710
                                        .toList());
×
711

712
                        return Optional.of(new AbstractMap.SimpleEntry<>(attributeMapping.targetName(), propertyVOS));
×
713
                } catch (IllegalAccessException | InvocationTargetException e) {
×
714
                        throw new MappingException(String.format(WAS_NOT_ABLE_INVOKE_METHOD_TEMPLATE, method, entity));
×
715
                }
716
        }
717

718
}
719

720

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