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

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

20 Mar 2025 03:17PM UTC coverage: 77.455% (-0.6%) from 78.079%
#276

push

web-flow
Merge pull request #81 from wistefan/query-in-existing

Query in existing

20 of 27 new or added lines in 2 files covered. (74.07%)

19 existing lines in 2 files now uncovered.

639 of 825 relevant lines covered (77.45%)

0.77 hits per line

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

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

3
import com.fasterxml.jackson.core.type.TypeReference;
4
import com.fasterxml.jackson.databind.ObjectMapper;
5
import io.github.wistefan.mapping.annotations.*;
6
import lombok.RequiredArgsConstructor;
7
import lombok.extern.slf4j.Slf4j;
8
import org.fiware.ngsi.model.*;
9

10
import javax.inject.Singleton;
11
import java.lang.annotation.Annotation;
12
import java.lang.reflect.InvocationTargetException;
13
import java.lang.reflect.Method;
14
import java.net.URI;
15
import java.time.Instant;
16
import java.util.*;
17
import java.util.stream.Collectors;
18
import java.util.stream.Stream;
19

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

28
        // name of the property containing the ID
29
        private static final String ID_PROPERTY = "id";
30
        private final MappingProperties mappingProperties;
31
        private final ObjectMapper objectMapper;
32

33
        public static final String NO_MAPPING_DEFINED_FOR_METHOD_TEMPLATE = "No mapping defined for method %s";
34
        public static final String WAS_NOT_ABLE_INVOKE_METHOD_TEMPLATE = "Was not able invoke method %s on %s";
35

36

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

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

111
        private static QueryAttributeType fromClass(Class<?> tClass) {
112
                if (Number.class.isAssignableFrom(tClass)) {
1✔
113
                        return QueryAttributeType.NUMBER;
1✔
114
                } else if (Boolean.class.isAssignableFrom(tClass)) {
1✔
115
                        return QueryAttributeType.BOOLEAN;
1✔
116
                }
117
                return QueryAttributeType.STRING;
1✔
118

119
        }
120

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

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

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

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

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

170
                List<Method> entityIdMethod = new ArrayList<>();
1✔
171
                List<Method> entityTypeMethod = new ArrayList<>();
1✔
172
                List<Method> propertyMethods = new ArrayList<>();
1✔
173
                List<Method> propertyListMethods = new ArrayList<>();
1✔
174
                List<Method> relationshipMethods = new ArrayList<>();
1✔
175
                List<Method> relationshipListMethods = new ArrayList<>();
1✔
176
                List<Method> geoPropertyMethods = new ArrayList<>();
1✔
177
                List<Method> geoPropertyListMethods = new ArrayList<>();
1✔
178
                List<Method> unmappedPropertiesGetterMethods = new ArrayList<>();
1✔
179

180
                Arrays.stream(entity.getClass().getMethods()).forEach(method -> {
1✔
181
                        if (isEntityIdMethod(method)) {
1✔
182
                                entityIdMethod.add(method);
1✔
183
                        } else if (isEntityTypeMethod(method)) {
1✔
184
                                entityTypeMethod.add(method);
1✔
185
                        } else if (isUnmappedPropertiesGetter(method)) {
1✔
186
                                unmappedPropertiesGetterMethods.add(method);
1✔
187
                        } else {
188
                                getAttributeGetter(method.getAnnotations()).ifPresent(annotation -> {
1✔
189
                                        switch (annotation.value()) {
1✔
190
                                                case PROPERTY -> propertyMethods.add(method);
1✔
191
                                                case PROPERTY_LIST -> propertyListMethods.add(method);
1✔
UNCOV
192
                                                case GEO_PROPERTY -> geoPropertyMethods.add(method);
×
193
                                                case RELATIONSHIP -> relationshipMethods.add(method);
1✔
194
                                                case GEO_PROPERTY_LIST -> geoPropertyListMethods.add(method);
×
195
                                                case RELATIONSHIP_LIST -> relationshipListMethods.add(method);
1✔
196
                                                default -> throw new UnsupportedOperationException(
×
197
                                                                String.format("Mapping target %s is not supported.", annotation.value()));
×
198
                                        }
199
                                });
1✔
200
                        }
201
                });
1✔
202

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

213
                }
214

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

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

234
                EntityVO entityVO = new EntityVO();
1✔
235
                entityVO.setAtContext(mappingProperties.getContextUrl());
1✔
236

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

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

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

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

278
                additionalProperties.putAll(relationshipVOMap);
1✔
279
                additionalProperties.putAll(relationshipListVOMap);
1✔
280

281
                additionalProperties.forEach(entityVO::setAdditionalProperties);
1✔
282

283
                return entityVO;
1✔
284
        }
285

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

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

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

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

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

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

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

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

356
        }
357

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

378
        private AdditionalPropertyVO objectToAdditionalProperty(Object o) {
379
                if (o instanceof List<?> objectList && !objectList.isEmpty()) {
1✔
380
                        if (isPlain(objectList.get(0))) {
1✔
381
                                return new PropertyVO().value(objectList);
1✔
382
                        } else {
UNCOV
383
                                PropertyListVO propertyVOS = new PropertyListVO();
×
UNCOV
384
                                RelationshipListVO relationshipVOS = new RelationshipListVO();
×
385
                                // as of now, we don't support property lists of property lists
UNCOV
386
                                objectList.stream()
×
UNCOV
387
                                                .map(this::objectToAdditionalProperty)
×
UNCOV
388
                                                .forEach(apvo -> {
×
UNCOV
389
                                                        if (apvo instanceof PropertyVO pvo) {
×
UNCOV
390
                                                                propertyVOS.add(pvo);
×
391
                                                        }
UNCOV
392
                                                        if (apvo instanceof RelationshipVO rvo) {
×
393
                                                                relationshipVOS.add(rvo);
×
394
                                                        }
UNCOV
395
                                                });
×
UNCOV
396
                                if (!propertyVOS.isEmpty() && !relationshipVOS.isEmpty()) {
×
397
                                        throw new MappingException("Mixed lists are not supported");
×
398
                                }
UNCOV
399
                                if (!propertyVOS.isEmpty()) {
×
UNCOV
400
                                        return propertyVOS;
×
401
                                }
402
                                if (!relationshipVOS.isEmpty()) {
×
403
                                        return relationshipVOS;
×
404
                                }
405
                                return propertyVOS;
×
406
                        }
407
                } else if (isPlain(o)) {
1✔
408
                        PropertyVO propertyVO = new PropertyVO().value(o);
1✔
409
                        return propertyVO;
1✔
410
                } else {
411
                        Map<?, ?> objectMap = new HashMap<>();
1✔
412
                        if (o instanceof Map<?, ?> om) {
1✔
413
                                objectMap = om;
1✔
414
                        } else {
UNCOV
415
                                objectMap = toMap(o);
×
416
                        }
417

418
                        if (objectMap.containsKey(ID_PROPERTY)) {
1✔
419
                                // contains key "id" -> relationship
420
                                return mapToRelationship(objectMap);
1✔
421
                        } else {
422
                                PropertyVO propertyVO = new PropertyVO();
1✔
423
                                Map<String, AdditionalPropertyVO> values = new HashMap<>();
1✔
424
                                objectMap.forEach((key, value) -> {
1✔
425
                                        if (key instanceof String name) {
1✔
426
                                                propertyVO.setAdditionalProperties(name, objectToAdditionalProperty(value));
1✔
427
                                                values.put(name, objectToAdditionalProperty(value));
1✔
428
                                        } else {
429
                                                throw new MappingException(String.format("Only string keys are supported, but was %s", key));
×
430
                                        }
431
                                });
1✔
432
                                return propertyVO.value(values);
1✔
433
                        }
434
                }
435
        }
436

437
        private boolean isPlain(Object o) {
438
                if (o instanceof Number) {
1✔
439
                        return true;
1✔
440
                }
441
                if (o instanceof String) {
1✔
442
                        return true;
1✔
443
                }
444
                if (o instanceof Boolean) {
1✔
445
                        return true;
×
446
                }
447
                if (o instanceof URI) {
1✔
NEW
448
                        return true;
×
449
                }
450
                if (o instanceof Instant) {
1✔
NEW
451
                        return true;
×
452
                }
453
                if(o instanceof Enum<?>) {
1✔
NEW
454
                        return true;
×
455
                }
456

457
                return false;
1✔
458
        }
459

460
        private <T> Map<String, AdditionalPropertyVO> buildUnmappedProperties(T entity, Method method) {
461
                try {
462
                        Object unmappedProperties = method.invoke(entity);
1✔
463
                        if (unmappedProperties == null) {
1✔
464
                                return Map.of();
×
465
                        } else if (unmappedProperties instanceof List<?> unmappedPropertiesList) {
1✔
466
                                List<UnmappedProperty> theList = unmappedPropertiesList
1✔
467
                                                .stream()
1✔
468
                                                .filter(UnmappedProperty.class::isInstance)
1✔
469
                                                .map(UnmappedProperty.class::cast)
1✔
470
                                                .toList();
1✔
471
                                return theList.stream()
1✔
472
                                                .map(this::unmappedPropertyToAdditionalProperty)
1✔
473
                                                .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
1✔
474
                        } else {
475
                                throw new MappingException("Only lists of additional Properties are supported.");
×
476
                        }
477

478
                } catch (IllegalAccessException | InvocationTargetException e) {
×
479
                        throw new MappingException(String.format(WAS_NOT_ABLE_INVOKE_METHOD_TEMPLATE, method, entity));
×
480
                }
481
        }
482

483

484
        /**
485
         * Build properties from the declared methods
486
         */
487
        private <T> Map<String, AdditionalPropertyVO> buildProperties(T entity, List<Method> propertyMethods) {
488
                return propertyMethods.stream()
1✔
489
                                .map(propertyMethod -> methodToPropertyEntry(entity, propertyMethod))
1✔
490
                                .filter(Optional::isPresent)
1✔
491
                                .map(Optional::get)
1✔
492
                                .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
1✔
493
        }
494

495
        /**
496
         * Return method defining the object of the relationship for the given entity, if exists.
497
         */
498
        private <T> Optional<Method> getRelationshipObjectMethod(T entity) {
499
                return Arrays.stream(entity.getClass().getMethods()).filter(this::isRelationShipObject).findFirst();
1✔
500
        }
501

502
        /**
503
         * Return method defining the datasetid for the given entity, if exists.
504
         */
505
        private <T> Optional<Method> getDatasetIdMethod(T entity) {
506
                return Arrays.stream(entity.getClass().getMethods()).filter(this::isDatasetId).findFirst();
1✔
507
        }
508

509
        /**
510
         * Get all methods declared as attribute getters.
511
         */
512
        private <T> List<Method> getAttributeGettersMethods(T entity) {
513
                return Arrays.stream(entity.getClass().getMethods())
1✔
514
                                .filter(m -> getAttributeGetterAnnotation(m).isPresent())
1✔
515
                                .toList();
1✔
516
        }
517

518
        /**
519
         * return the {@link  AttributeGetter} annotation for the method if there is such.
520
         */
521
        private Optional<AttributeGetter> getAttributeGetterAnnotation(Method m) {
522
                return Arrays.stream(m.getAnnotations()).filter(AttributeGetter.class::isInstance).findFirst()
1✔
523
                                .map(AttributeGetter.class::cast);
1✔
524
        }
525

526
        /**
527
         * Find the attribute getter from all the annotations.
528
         */
529
        private static Optional<AttributeGetter> getAttributeGetter(Annotation[] annotations) {
530
                return Arrays.stream(annotations).filter(AttributeGetter.class::isInstance).map(AttributeGetter.class::cast)
1✔
531
                                .findFirst();
1✔
532
        }
533

534
        /**
535
         * Find the attribute setter from all the annotations.
536
         */
537
        private static Optional<AttributeSetter> getAttributeSetter(Annotation[] annotations) {
538
                return Arrays.stream(annotations).filter(AttributeSetter.class::isInstance).map(AttributeSetter.class::cast)
1✔
539
                                .findFirst();
1✔
540
        }
541

542
        /**
543
         * Check if the given method is declared to be used as object of a relationship
544
         */
545
        private boolean isRelationShipObject(Method m) {
546
                return Arrays.stream(m.getAnnotations()).anyMatch(RelationshipObject.class::isInstance);
1✔
547
        }
548

549
        /**
550
         * Check if the given method is declared to be used as datasetId
551
         */
552
        private boolean isDatasetId(Method m) {
553
                return Arrays.stream(m.getAnnotations()).anyMatch(DatasetId.class::isInstance);
1✔
554
        }
555

556
        /**
557
         * Build a property entry from the given method on the entity
558
         */
559
        private <T> Optional<Map.Entry<String, AdditionalPropertyVO>> methodToPropertyEntry(T entity, Method method) {
560
                try {
561
                        Object propertyObject = method.invoke(entity);
1✔
562
                        if (propertyObject == null) {
1✔
563
                                return Optional.empty();
×
564
                        }
565
                        AttributeGetter attributeMapping = getAttributeGetter(method.getAnnotations()).orElseThrow(
1✔
566
                                        () -> new MappingException(String.format(NO_MAPPING_DEFINED_FOR_METHOD_TEMPLATE, method)));
×
567

568
                        if (isPlain(propertyObject)) {
1✔
569
                                PropertyVO propertyVO = new PropertyVO();
1✔
570
                                propertyVO.setValue(propertyObject);
1✔
571
                                return Optional.of(new AbstractMap.SimpleEntry<>(attributeMapping.targetName(), propertyVO));
1✔
572
                        } else if (propertyObject instanceof List) {
1✔
UNCOV
573
                                AdditionalPropertyVO additionalProperty = objectToAdditionalProperty(propertyObject);
×
UNCOV
574
                                return Optional.of(new AbstractMap.SimpleEntry<>(attributeMapping.targetName(), additionalProperty));
×
575
                        } else {
576
                                Map<String, Object> propertyObjectMap = toMap(propertyObject);
1✔
577
                                if (propertyObjectMap.isEmpty()) {
1✔
NEW
578
                                        return Optional.empty();
×
579
                                }
580
                                AdditionalPropertyVO additionalProperty = objectToAdditionalProperty(toMap(propertyObject));
1✔
581
                                if (additionalProperty instanceof PropertyVO) {
1✔
582
                                        ((PropertyVO) additionalProperty).setValue(propertyObject);
1✔
583
                                }
584
                                return Optional.of(new AbstractMap.SimpleEntry<>(attributeMapping.targetName(), additionalProperty));
1✔
585
                        }
586

587
                } catch (IllegalAccessException | InvocationTargetException e) {
1✔
588
                        throw new MappingException(String.format(WAS_NOT_ABLE_INVOKE_METHOD_TEMPLATE, method, entity));
1✔
589
                }
590
        }
591

592
        public Map<String, Object> toMap(Object obj) {
593
                if (obj == null || obj instanceof Collection<?>) {
1✔
NEW
594
                        return Map.of();
×
595
                }
596
                return objectMapper.convertValue(obj, new TypeReference<Map<String, Object>>() {});
1✔
597
        }
598

599
        /**
600
         * Build a geo-property entry from the given method on the entity
601
         */
602
        private <T> Optional<Map.Entry<String, GeoPropertyVO>> methodToGeoPropertyEntry(T entity, Method method) {
603
                try {
604
                        Object o = method.invoke(entity);
×
605
                        if (o == null) {
×
606
                                return Optional.empty();
×
607
                        }
608
                        AttributeGetter attributeMapping = getAttributeGetter(method.getAnnotations()).orElseThrow(
×
609
                                        () -> new MappingException(String.format(NO_MAPPING_DEFINED_FOR_METHOD_TEMPLATE, method)));
×
610
                        GeoPropertyVO geoPropertyVO = new GeoPropertyVO();
×
611
                        geoPropertyVO.setValue(o);
×
612
                        return Optional.of(new AbstractMap.SimpleEntry<>(attributeMapping.targetName(), geoPropertyVO));
×
613
                } catch (IllegalAccessException | InvocationTargetException e) {
×
614
                        throw new MappingException(String.format(WAS_NOT_ABLE_INVOKE_METHOD_TEMPLATE, method, entity));
×
615
                }
616
        }
617

618
        /**
619
         * Build a relationship entry from the given method on the entity
620
         */
621
        private <T> Optional<Map.Entry<String, RelationshipVO>> methodToRelationshipEntry(T entity, Method method) {
622
                try {
623
                        Object relationShipObject = method.invoke(entity);
1✔
624
                        if (relationShipObject == null) {
1✔
625
                                return Optional.empty();
×
626
                        }
627
                        RelationshipVO relationshipVO = getRelationshipVO(method, relationShipObject);
1✔
628
                        AttributeGetter attributeMapping = getAttributeGetter(method.getAnnotations()).orElseThrow(
1✔
629
                                        () -> new MappingException(String.format(NO_MAPPING_DEFINED_FOR_METHOD_TEMPLATE, method)));
×
630
                        return Optional.of(new AbstractMap.SimpleEntry<>(attributeMapping.targetName(), relationshipVO));
1✔
631
                } catch (IllegalAccessException | InvocationTargetException e) {
1✔
632
                        throw new MappingException(String.format(WAS_NOT_ABLE_INVOKE_METHOD_TEMPLATE, method, entity));
1✔
633
                }
634
        }
635

636
        /**
637
         * Build a relationship list entry from the given method on the entity
638
         */
639
        private <T> Optional<Map.Entry<String, RelationshipListVO>> methodToRelationshipListEntry(T entity, Method method) {
640
                try {
641
                        Object o = method.invoke(entity);
1✔
642
                        if (o == null) {
1✔
643
                                return Optional.empty();
1✔
644
                        }
645
                        if (!(o instanceof List)) {
1✔
646
                                throw new MappingException(
1✔
647
                                                String.format("Relationship list method %s::%s did not return a List.", entity, method));
1✔
648
                        }
649
                        List<Object> entityObjects = (List) o;
1✔
650

651
                        AttributeGetter attributeGetter = getAttributeGetter(method.getAnnotations()).orElseThrow(
1✔
652
                                        () -> new MappingException(String.format(NO_MAPPING_DEFINED_FOR_METHOD_TEMPLATE, method)));
×
653
                        RelationshipListVO relationshipVOS = new RelationshipListVO();
1✔
654

655
                        relationshipVOS.addAll(entityObjects.stream()
1✔
656
                                        .filter(Objects::nonNull)
1✔
657
                                        .map(entityObject -> getRelationshipVO(method, entityObject))
1✔
658
                                        .toList());
1✔
659
                        return Optional.of(new AbstractMap.SimpleEntry<>(attributeGetter.targetName(), relationshipVOS));
1✔
660
                } catch (IllegalAccessException | InvocationTargetException e) {
1✔
661
                        throw new MappingException(String.format(WAS_NOT_ABLE_INVOKE_METHOD_TEMPLATE, method, entity));
1✔
662
                }
663
        }
664

665
        /**
666
         * Get the relationship for the given method and relationship object
667
         */
668
        private RelationshipVO getRelationshipVO(Method method, Object relationShipObject) {
669
                try {
670

671
                        Method objectMethod = getRelationshipObjectMethod(relationShipObject).orElseThrow(
1✔
672
                                        () -> new MappingException(
1✔
673
                                                        String.format("The relationship %s-%s does not provide an object method.",
1✔
674
                                                                        relationShipObject, method)));
675
                        Object objectObject = objectMethod.invoke(relationShipObject);
1✔
676
                        if (!(objectObject instanceof URI)) {
1✔
677
                                throw new MappingException(
1✔
678
                                                String.format("The object %s of the relationship is not a URI.", relationShipObject));
1✔
679
                        }
680

681
                        Method datasetIdMethod = getDatasetIdMethod(relationShipObject).orElseThrow(() -> new MappingException(
1✔
682
                                        String.format("The relationship %s-%s does not provide a datasetId method.", relationShipObject,
×
683
                                                        method)));
684
                        Object datasetIdObject = datasetIdMethod.invoke(relationShipObject);
1✔
685
                        if (!(datasetIdObject instanceof URI)) {
1✔
686
                                throw new MappingException(
1✔
687
                                                String.format("The datasetId %s of the relationship is not a URI.", relationShipObject));
1✔
688
                        }
689
                        RelationshipVO relationshipVO = new RelationshipVO();
1✔
690
                        relationshipVO.setObject((URI) objectObject);
1✔
691
                        relationshipVO.setDatasetId((URI) datasetIdObject);
1✔
692

693

694
                        // get additional properties. We do not support more depth/complexity for now
695
                        Map<String, AdditionalPropertyVO> additionalProperties = getAttributeGettersMethods(relationShipObject)
1✔
696
                                        .stream()
1✔
697
                                        .map(getterMethod -> getAdditionalPropertyEntryFromMethod(relationShipObject, getterMethod))
1✔
698
                                        .filter(Optional::isPresent)
1✔
699
                                        .map(Optional::get)
1✔
700
                                        .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
1✔
701

702
                        additionalProperties.forEach(relationshipVO::setAdditionalProperties);
1✔
703

704
                        return relationshipVO;
1✔
705
                } catch (IllegalAccessException | InvocationTargetException e) {
×
706
                        throw new MappingException(
×
707
                                        String.format(WAS_NOT_ABLE_INVOKE_METHOD_TEMPLATE, method, relationShipObject));
×
708
                }
709
        }
710

711
        /**
712
         * Get all additional properties for the object of the relationship
713
         */
714
        private Optional<Map.Entry<String, AdditionalPropertyVO>> getAdditionalPropertyEntryFromMethod(Object relationShipObject,
715
                                                                                                                                                                                                   Method getterMethod) {
716
                Optional<AttributeGetter> optionalAttributeGetter = getAttributeGetter(getterMethod.getAnnotations());
1✔
717
                if (optionalAttributeGetter.isEmpty() || !optionalAttributeGetter.get().embedProperty()) {
1✔
718
                        return Optional.empty();
1✔
719
                }
720
                if (optionalAttributeGetter.get().value().equals(AttributeType.PROPERTY)) {
1✔
721
                        return methodToPropertyEntry(relationShipObject, getterMethod);
1✔
722
                } else {
723
                        return Optional.empty();
×
724
                }
725
        }
726

727
        /**
728
         * Build a property list entry from the given method on the entity
729
         */
730
        private <T> Optional<Map.Entry<String, PropertyListVO>> methodToPropertyListEntry(T entity, Method method) {
731
                try {
732
                        Object o = method.invoke(entity);
1✔
733
                        if (o == null) {
1✔
734
                                return Optional.empty();
×
735
                        }
736
                        if (!(o instanceof List)) {
1✔
737
                                throw new MappingException(
×
738
                                                String.format("Property list method %s::%s did not return a List.", entity, method));
×
739
                        }
740
                        AttributeGetter attributeMapping = getAttributeGetter(method.getAnnotations()).orElseThrow(
1✔
741
                                        () -> new MappingException(String.format(NO_MAPPING_DEFINED_FOR_METHOD_TEMPLATE, method)));
×
742
                        List<Object> entityObjects = (List) o;
1✔
743

744
                        PropertyListVO propertyVOS = new PropertyListVO();
1✔
745
                        entityObjects.stream()
1✔
746
                                        .map(propertyObject -> {
1✔
747
                                                PropertyVO propertyVO = new PropertyVO();
1✔
748
                                                if (isPlain(propertyObject)) {
1✔
749
                                                        propertyVO.setValue(propertyObject);
1✔
750
                                                } else {
751
                                                        Map<String, Object> propertyObjectMap = toMap(propertyObject);
1✔
752
                                                        if (propertyObjectMap.isEmpty()) {
1✔
NEW
753
                                                                return null;
×
754
                                                        }
755
                                                        AdditionalPropertyVO additionalProperty = objectToAdditionalProperty(propertyObjectMap);
1✔
756
                                                        if (additionalProperty instanceof PropertyVO pvo) {
1✔
757
                                                                propertyVO = pvo.value(propertyObject);
1✔
758
                                                        }
759
                                                }
760
                                                return propertyVO;
1✔
761
                                        })
762
                                        .filter(Objects::nonNull)
1✔
763
                                        .forEach(propertyVOS::add);
1✔
764
                        if (propertyVOS.size() > 1) {
1✔
765
                                propertyVOS.forEach(propertyVO -> propertyVO.setDatasetId(URI.create("urn:uuid:" + UUID.randomUUID())));
1✔
766
                        }
767
                        return Optional.of(new AbstractMap.SimpleEntry<>(attributeMapping.targetName(), propertyVOS));
1✔
768
                } catch (IllegalAccessException | InvocationTargetException e) {
1✔
769
                        throw new MappingException(String.format(WAS_NOT_ABLE_INVOKE_METHOD_TEMPLATE, method, entity));
1✔
770
                }
771
        }
772

773
}
774

775

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