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

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

26 Sep 2023 01:05PM UTC coverage: 82.364% (-0.09%) from 82.453%
#131

push

web-flow
Merge pull request #39 from wistefan/make-context-configurable

allow config

4 of 4 new or added lines in 1 file covered. (100.0%)

439 of 533 relevant lines covered (82.36%)

0.82 hits per line

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

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

3
import com.fasterxml.jackson.databind.ObjectMapper;
4
import io.github.wistefan.mapping.annotations.AttributeGetter;
5
import io.github.wistefan.mapping.annotations.AttributeSetter;
6
import io.github.wistefan.mapping.annotations.AttributeType;
7
import io.github.wistefan.mapping.annotations.DatasetId;
8
import io.github.wistefan.mapping.annotations.EntityId;
9
import io.github.wistefan.mapping.annotations.EntityType;
10
import io.github.wistefan.mapping.annotations.RelationshipObject;
11
import lombok.RequiredArgsConstructor;
12
import lombok.extern.slf4j.Slf4j;
13
import org.fiware.ngsi.model.AdditionalPropertyVO;
14
import org.fiware.ngsi.model.EntityVO;
15
import org.fiware.ngsi.model.GeoPropertyVO;
16
import org.fiware.ngsi.model.PropertyListVO;
17
import org.fiware.ngsi.model.PropertyVO;
18
import org.fiware.ngsi.model.RelationshipListVO;
19
import org.fiware.ngsi.model.RelationshipVO;
20

21
import javax.inject.Singleton;
22
import java.lang.annotation.Annotation;
23
import java.lang.reflect.InvocationTargetException;
24
import java.lang.reflect.Method;
25
import java.net.URI;
26
import java.util.AbstractMap;
27
import java.util.ArrayList;
28
import java.util.Arrays;
29
import java.util.LinkedHashMap;
30
import java.util.List;
31
import java.util.Map;
32
import java.util.Objects;
33
import java.util.Optional;
34
import java.util.stream.Collectors;
35
import java.util.stream.Stream;
36

37
/**
38
 * Mapper to handle translation from Java-Objects into NGSI-LD entities.
39
 */
40
@Slf4j
1✔
41
@Singleton
42
@RequiredArgsConstructor
×
43
public class JavaObjectMapper extends Mapper {
44

45
        private static final String DEFAULT_CONTEXT = "https://smartdatamodels.org/context.jsonld";
46

47
        /**
48
         * Context to be used for the entities.
49
         */
50
        private final String entityContext;
51

52
        public static final String NO_MAPPING_DEFINED_FOR_METHOD_TEMPLATE = "No mapping defined for method %s";
53
        public static final String WAS_NOT_ABLE_INVOKE_METHOD_TEMPLATE = "Was not able invoke method %s on %s";
54

55
        public JavaObjectMapper() {
1✔
56
                this.entityContext = DEFAULT_CONTEXT;
1✔
57
        }
1✔
58

59
        /**
60
         * Translate the attribute path for the given object into the path in the ngsi-ld model.
61
         *
62
         * @param attributePath the original path
63
         * @param tClass        class to use for translation
64
         * @return the path in ngsi-ld and the type of the target attribute
65
         */
66
        public static <T> NgsiLdAttribute getNGSIAttributePath(List<String> attributePath, Class<T> tClass) {
67
                List<String> ngsiAttributePath = new ArrayList<>();
1✔
68
                QueryAttributeType type = QueryAttributeType.STRING;
1✔
69
                String currentAttribute = attributePath.get(0);
1✔
70
                if (isMappingEnabled(tClass).isPresent()) {
1✔
71
                        // for mapped properties, we have to climb down the property names
72
                        // we need the setter in case of type-erasure and the getter in all other cases
73
                        Optional<Method> setter = getSetterMethodByName(tClass, currentAttribute)
1✔
74
                                        .filter(m -> getAttributeSetter(m.getAnnotations()).isPresent())
1✔
75
                                        .findFirst();
1✔
76
                        Optional<Method> getter = getGetterMethodByName(tClass, currentAttribute)
1✔
77
                                        .filter(m -> getAttributeGetter(m.getAnnotations()).isPresent())
1✔
78
                                        .findFirst();
1✔
79
                        if (setter.isPresent() && getter.isPresent()) {
1✔
80
                                Method setterMethod = setter.get();
1✔
81
                                Method getterMethod = getter.get();
1✔
82
                                // no need to check again
83
                                AttributeSetter setterAnnotation = getAttributeSetter(setterMethod.getAnnotations()).get();
1✔
84
                                ngsiAttributePath.add(setterAnnotation.targetName());
1✔
85
                                type = fromClass(getterMethod.getReturnType());
1✔
86
                                if (attributePath.size() > 1) {
1✔
87
                                        List<String> subPaths = attributePath.subList(1, attributePath.size());
1✔
88
                                        if (setterAnnotation.targetClass() != Object.class) {
1✔
89
                                                NgsiLdAttribute subAttribute = getNGSIAttributePath(subPaths, setterAnnotation.targetClass());
1✔
90
                                                ngsiAttributePath.addAll(subAttribute.path());
1✔
91
                                                type = subAttribute.type();
1✔
92
                                        } else {
1✔
93
                                                NgsiLdAttribute subAttribute = getNGSIAttributePath(subPaths, getterMethod.getReturnType());
1✔
94
                                                ngsiAttributePath.addAll(subAttribute.path());
1✔
95
                                                type = subAttribute.type();
1✔
96
                                        }
97
                                }
98
                        } else {
1✔
99
                                log.warn("No corresponding field does exist for attribute {} on {}.", currentAttribute,
×
100
                                                tClass.getCanonicalName());
×
101
                        }
102
                } else {
1✔
103
                        // we can use the "plain" object field-names, no additional mapping happens anymore
104
                        ngsiAttributePath.addAll(attributePath);
1✔
105
                        type = evaluateType(attributePath, tClass);
1✔
106
                }
107
                return new NgsiLdAttribute(ngsiAttributePath, type);
1✔
108
        }
109

110
        private static QueryAttributeType evaluateType(List<String> path, Class<?> tClass) {
111
                Class<?> currentClass = tClass;
1✔
112
                for (String s : path) {
1✔
113
                        try {
114
                                Optional<Class<?>> optionalReturn = getGetterMethodByName(currentClass, s).findAny()
1✔
115
                                                .map(Method::getReturnType);
1✔
116
                                if (optionalReturn.isPresent()) {
1✔
117
                                        currentClass = optionalReturn.get();
1✔
118
                                } else {
119
                                        currentClass = currentClass.getField(s).getType();
×
120
                                }
121
                        } catch (NoSuchFieldException e) {
×
122
                                throw new MappingException(String.format("No field %s exists for %s.", s, tClass.getCanonicalName()),
×
123
                                                e);
124
                        }
1✔
125
                }
1✔
126
                return fromClass(currentClass);
1✔
127
        }
128

129
        private static QueryAttributeType fromClass(Class<?> tClass) {
130
                if (Number.class.isAssignableFrom(tClass)) {
1✔
131
                        return QueryAttributeType.NUMBER;
1✔
132
                } else if (Boolean.class.isAssignableFrom(tClass)) {
1✔
133
                        return QueryAttributeType.BOOLEAN;
1✔
134
                }
135
                return QueryAttributeType.STRING;
1✔
136

137
        }
138

139
        public static <T> Stream<Method> getSetterMethodByName(Class<T> tClass, String propertyName) {
140
                return Arrays.stream(tClass.getMethods())
1✔
141
                                .filter(m -> getCorrespondingSetterFieldName(m.getName()).equals(propertyName));
1✔
142
        }
143

144
        public static <T> Stream<Method> getGetterMethodByName(Class<T> tClass, String propertyName) {
145
                return Arrays.stream(tClass.getMethods())
1✔
146
                                .filter(m -> getCorrespondingGetterFieldName(m.getName()).equals(propertyName));
1✔
147
        }
148

149
        private static String getCorrespondingGetterFieldName(String methodName) {
150
                var fieldName = "";
1✔
151
                if (methodName.matches("^get[A-Z].*")) {
1✔
152
                        fieldName = methodName.replaceFirst("get", "");
1✔
153
                } else if (methodName.matches("^is[A-Z].*")) {
1✔
154
                        fieldName = methodName.replaceFirst("is", "");
×
155
                } else {
156
                        log.debug("The method {} is neither a get or is.", methodName);
1✔
157
                        return fieldName;
1✔
158
                }
159
                return fieldName.substring(0, 1).toLowerCase() + fieldName.substring(1);
1✔
160
        }
161

162
        private static String getCorrespondingSetterFieldName(String methodName) {
163
                var fieldName = "";
1✔
164
                if (methodName.matches("^set[A-Z].*")) {
1✔
165
                        fieldName = methodName.replaceFirst("set", "");
1✔
166
                } else if (methodName.matches("^is[A-Z].*")) {
1✔
167
                        fieldName = methodName.replaceFirst("is", "");
×
168
                } else {
169
                        log.debug("The method {} is neither a set or is.", methodName);
1✔
170
                        return fieldName;
1✔
171
                }
172
                return fieldName.substring(0, 1).toLowerCase() + fieldName.substring(1);
1✔
173
        }
174

175
        /**
176
         * Translate the given object into an Entity.
177
         *
178
         * @param entity the object representing the entity
179
         * @param <T>    class of the entity
180
         * @return the NGIS-LD entity objet
181
         */
182
        public <T> EntityVO toEntityVO(T entity) {
183
                isMappingEnabled(entity.getClass())
1✔
184
                                .orElseThrow(() -> new UnsupportedOperationException(
1✔
185
                                                String.format("Generic mapping to NGSI-LD entities is not supported for object %s",
1✔
186
                                                                entity)));
187

188
                List<Method> entityIdMethod = new ArrayList<>();
1✔
189
                List<Method> entityTypeMethod = new ArrayList<>();
1✔
190
                List<Method> propertyMethods = new ArrayList<>();
1✔
191
                List<Method> propertyListMethods = new ArrayList<>();
1✔
192
                List<Method> relationshipMethods = new ArrayList<>();
1✔
193
                List<Method> relationshipListMethods = new ArrayList<>();
1✔
194
                List<Method> geoPropertyMethods = new ArrayList<>();
1✔
195
                List<Method> geoPropertyListMethods = new ArrayList<>();
1✔
196

197
                Arrays.stream(entity.getClass().getMethods()).forEach(method -> {
1✔
198
                        if (isEntityIdMethod(method)) {
1✔
199
                                entityIdMethod.add(method);
1✔
200
                        } else if (isEntityTypeMethod(method)) {
1✔
201
                                entityTypeMethod.add(method);
1✔
202
                        } else {
203
                                getAttributeGetter(method.getAnnotations()).ifPresent(annotation -> {
1✔
204
                                        switch (annotation.value()) {
1✔
205
                                                case PROPERTY -> propertyMethods.add(method);
1✔
206
                                                // We handle property lists the same way as properties, since it is mapped as a property which value is a json array.
207
                                                // A real NGSI-LD property list would require a datasetId, that is not provided here.
208
                                                case PROPERTY_LIST -> propertyMethods.add(method);
1✔
209
                                                case GEO_PROPERTY -> geoPropertyMethods.add(method);
×
210
                                                case RELATIONSHIP -> relationshipMethods.add(method);
1✔
211
                                                case GEO_PROPERTY_LIST -> geoPropertyListMethods.add(method);
×
212
                                                case RELATIONSHIP_LIST -> relationshipListMethods.add(method);
1✔
213
                                                default -> throw new UnsupportedOperationException(
×
214
                                                                String.format("Mapping target %s is not supported.", annotation.value()));
×
215
                                        }
216
                                });
1✔
217
                        }
218
                });
1✔
219

220
                if (entityIdMethod.size() != 1) {
1✔
221
                        throw new MappingException(
1✔
222
                                        String.format("The provided object declares %s id methods, exactly one is expected.",
1✔
223
                                                        entityIdMethod.size()));
1✔
224
                }
225
                if (entityTypeMethod.size() != 1) {
1✔
226
                        throw new MappingException(
1✔
227
                                        String.format("The provided object declares %s type methods, exactly one is expected.",
1✔
228
                                                        entityTypeMethod.size()));
1✔
229

230
                }
231

232
                return buildEntity(entity, entityIdMethod.get(0), entityTypeMethod.get(0), propertyMethods,
1✔
233
                                propertyListMethods,
234
                                geoPropertyMethods, relationshipMethods, relationshipListMethods);
235
        }
236

237
        /**
238
         * Build the entity from its declared methods.
239
         */
240
        private <T> EntityVO buildEntity(T entity, Method entityIdMethod, Method entityTypeMethod,
241
                        List<Method> propertyMethods, List<Method> propertyListMethods,
242
                        List<Method> geoPropertyMethods,
243
                        List<Method> relationshipMethods, List<Method> relationshipListMethods) {
244

245
                EntityVO entityVO = new EntityVO();
1✔
246
                entityVO.setAtContext(entityContext);
1✔
247

248
                // TODO: include extraction via annotation for all well-known attributes
249
                entityVO.setOperationSpace(null);
1✔
250
                entityVO.setObservationSpace(null);
1✔
251
                entityVO.setLocation(null);
1✔
252

253
                try {
254
                        Object entityIdObject = entityIdMethod.invoke(entity);
1✔
255
                        if (!(entityIdObject instanceof URI)) {
1✔
256
                                throw new MappingException(
1✔
257
                                                String.format("The entityId method does not return a valid URI for entity %s.", entity));
1✔
258
                        }
259
                        entityVO.id((URI) entityIdObject);
1✔
260

261
                        Object entityTypeObject = entityTypeMethod.invoke(entity);
1✔
262
                        if (!(entityTypeObject instanceof String)) {
1✔
263
                                throw new MappingException("The entityType method does not return a valid String.");
1✔
264
                        }
265
                        entityVO.setType((String) entityTypeObject);
1✔
266
                } catch (IllegalAccessException | InvocationTargetException e) {
1✔
267
                        throw new MappingException(String.format(WAS_NOT_ABLE_INVOKE_METHOD_TEMPLATE, "unknown-method", entity),
1✔
268
                                        e);
269
                }
1✔
270

271
                Map<String, AdditionalPropertyVO> additionalProperties = new LinkedHashMap<>();
1✔
272
                additionalProperties.putAll(buildProperties(entity, propertyMethods));
1✔
273
                additionalProperties.putAll(buildPropertyList(entity, propertyListMethods));
1✔
274
                additionalProperties.putAll(buildGeoProperties(entity, geoPropertyMethods));
1✔
275
                Map<String, RelationshipVO> relationshipVOMap = buildRelationships(entity, relationshipMethods);
1✔
276
                Map<String, RelationshipListVO> relationshipListVOMap = buildRelationshipList(entity,
1✔
277
                                relationshipListMethods);
278
                // we need to post-process the relationships, since orion-ld only accepts dataset-ids for lists > 1
279
                relationshipVOMap.entrySet().stream().forEach(e -> e.getValue().setDatasetId(null));
1✔
280
                relationshipListVOMap.entrySet().stream().forEach(e -> {
1✔
281
                        if (e.getValue().size() == 1) {
1✔
282
                                e.getValue().get(0).setDatasetId(null);
1✔
283
                        }
284
                });
1✔
285

286
                additionalProperties.putAll(relationshipVOMap);
1✔
287
                additionalProperties.putAll(relationshipListVOMap);
1✔
288

289
                additionalProperties.forEach(entityVO::setAdditionalProperties);
1✔
290

291
                return entityVO;
1✔
292
        }
293

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

301
        /**
302
         * Check if the given method defines the entity id
303
         */
304
        private boolean isEntityIdMethod(Method method) {
305
                return Arrays.stream(method.getAnnotations()).anyMatch(EntityId.class::isInstance);
1✔
306
        }
307

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

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

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

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

353
        /**
354
         * Build properties from the declared methods
355
         */
356
        private <T> Map<String, PropertyVO> buildProperties(T entity, List<Method> propertyMethods) {
357
                return propertyMethods.stream()
1✔
358
                                .map(propertyMethod -> methodToPropertyEntry(entity, propertyMethod))
1✔
359
                                .filter(Optional::isPresent)
1✔
360
                                .map(Optional::get)
1✔
361
                                .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
1✔
362
        }
363

364
        /**
365
         * Return method defining the object of the relationship for the given entity, if exists.
366
         */
367
        private <T> Optional<Method> getRelationshipObjectMethod(T entity) {
368
                return Arrays.stream(entity.getClass().getMethods()).filter(this::isRelationShipObject).findFirst();
1✔
369
        }
370

371
        /**
372
         * Return method defining the datasetid for the given entity, if exists.
373
         */
374
        private <T> Optional<Method> getDatasetIdMethod(T entity) {
375
                return Arrays.stream(entity.getClass().getMethods()).filter(this::isDatasetId).findFirst();
1✔
376
        }
377

378
        /**
379
         * Get all methods declared as attribute getters.
380
         */
381
        private <T> List<Method> getAttributeGettersMethods(T entity) {
382
                return Arrays.stream(entity.getClass().getMethods())
1✔
383
                                .filter(m -> getAttributeGetterAnnotation(m).isPresent())
1✔
384
                                .toList();
1✔
385
        }
386

387
        /**
388
         * return the {@link  AttributeGetter} annotation for the method if there is such.
389
         */
390
        private Optional<AttributeGetter> getAttributeGetterAnnotation(Method m) {
391
                return Arrays.stream(m.getAnnotations()).filter(AttributeGetter.class::isInstance).findFirst()
1✔
392
                                .map(AttributeGetter.class::cast);
1✔
393
        }
394

395
        /**
396
         * Find the attribute getter from all the annotations.
397
         */
398
        private static Optional<AttributeGetter> getAttributeGetter(Annotation[] annotations) {
399
                return Arrays.stream(annotations).filter(AttributeGetter.class::isInstance).map(AttributeGetter.class::cast)
1✔
400
                                .findFirst();
1✔
401
        }
402

403
        /**
404
         * Find the attribute setter from all the annotations.
405
         */
406
        private static Optional<AttributeSetter> getAttributeSetter(Annotation[] annotations) {
407
                return Arrays.stream(annotations).filter(AttributeSetter.class::isInstance).map(AttributeSetter.class::cast)
1✔
408
                                .findFirst();
1✔
409
        }
410

411
        /**
412
         * Check if the given method is declared to be used as object of a relationship
413
         */
414
        private boolean isRelationShipObject(Method m) {
415
                return Arrays.stream(m.getAnnotations()).anyMatch(RelationshipObject.class::isInstance);
1✔
416
        }
417

418
        /**
419
         * Check if the given method is declared to be used as datasetId
420
         */
421
        private boolean isDatasetId(Method m) {
422
                return Arrays.stream(m.getAnnotations()).anyMatch(DatasetId.class::isInstance);
1✔
423
        }
424

425
        /**
426
         * Build a property entry from the given method on the entity
427
         */
428
        private <T> Optional<Map.Entry<String, PropertyVO>> methodToPropertyEntry(T entity, Method method) {
429
                try {
430
                        Object propertyObject = method.invoke(entity);
1✔
431
                        if (propertyObject == null) {
1✔
432
                                return Optional.empty();
×
433
                        }
434
                        AttributeGetter attributeMapping = getAttributeGetter(method.getAnnotations()).orElseThrow(
1✔
435
                                        () -> new MappingException(String.format(NO_MAPPING_DEFINED_FOR_METHOD_TEMPLATE, method)));
×
436

437
                        PropertyVO propertyVO = new PropertyVO();
1✔
438
                        propertyVO.setValue(propertyObject);
1✔
439

440
                        return Optional.of(new AbstractMap.SimpleEntry<>(attributeMapping.targetName(), propertyVO));
1✔
441
                } catch (IllegalAccessException | InvocationTargetException e) {
1✔
442
                        throw new MappingException(String.format(WAS_NOT_ABLE_INVOKE_METHOD_TEMPLATE, method, entity));
1✔
443
                }
444
        }
445

446
        /**
447
         * Build a geo-property entry from the given method on the entity
448
         */
449
        private <T> Optional<Map.Entry<String, GeoPropertyVO>> methodToGeoPropertyEntry(T entity, Method method) {
450
                try {
451
                        Object o = method.invoke(entity);
×
452
                        if (o == null) {
×
453
                                return Optional.empty();
×
454
                        }
455
                        AttributeGetter attributeMapping = getAttributeGetter(method.getAnnotations()).orElseThrow(
×
456
                                        () -> new MappingException(String.format(NO_MAPPING_DEFINED_FOR_METHOD_TEMPLATE, method)));
×
457
                        GeoPropertyVO geoPropertyVO = new GeoPropertyVO();
×
458
                        geoPropertyVO.setValue(o);
×
459
                        return Optional.of(new AbstractMap.SimpleEntry<>(attributeMapping.targetName(), geoPropertyVO));
×
460
                } catch (IllegalAccessException | InvocationTargetException e) {
×
461
                        throw new MappingException(String.format(WAS_NOT_ABLE_INVOKE_METHOD_TEMPLATE, method, entity));
×
462
                }
463
        }
464

465
        /**
466
         * Build a relationship entry from the given method on the entity
467
         */
468
        private <T> Optional<Map.Entry<String, RelationshipVO>> methodToRelationshipEntry(T entity, Method method) {
469
                try {
470
                        Object relationShipObject = method.invoke(entity);
1✔
471
                        if (relationShipObject == null) {
1✔
472
                                return Optional.empty();
×
473
                        }
474
                        RelationshipVO relationshipVO = getRelationshipVO(method, relationShipObject);
1✔
475
                        AttributeGetter attributeMapping = getAttributeGetter(method.getAnnotations()).orElseThrow(
1✔
476
                                        () -> new MappingException(String.format(NO_MAPPING_DEFINED_FOR_METHOD_TEMPLATE, method)));
×
477
                        return Optional.of(new AbstractMap.SimpleEntry<>(attributeMapping.targetName(), relationshipVO));
1✔
478
                } catch (IllegalAccessException | InvocationTargetException e) {
1✔
479
                        throw new MappingException(String.format(WAS_NOT_ABLE_INVOKE_METHOD_TEMPLATE, method, entity));
1✔
480
                }
481
        }
482

483
        /**
484
         * Build a relationship list entry from the given method on the entity
485
         */
486
        private <
487
                        T> Optional<Map.Entry<String, RelationshipListVO>> methodToRelationshipListEntry(T entity, Method method) {
488
                try {
489
                        Object o = method.invoke(entity);
1✔
490
                        if (o == null) {
1✔
491
                                return Optional.empty();
1✔
492
                        }
493
                        if (!(o instanceof List)) {
1✔
494
                                throw new MappingException(
1✔
495
                                                String.format("Relationship list method %s::%s did not return a List.", entity, method));
1✔
496
                        }
497
                        List<Object> entityObjects = (List) o;
1✔
498

499
                        AttributeGetter attributeGetter = getAttributeGetter(method.getAnnotations()).orElseThrow(
1✔
500
                                        () -> new MappingException(String.format(NO_MAPPING_DEFINED_FOR_METHOD_TEMPLATE, method)));
×
501
                        RelationshipListVO relationshipVOS = new RelationshipListVO();
1✔
502

503
                        relationshipVOS.addAll(entityObjects.stream()
1✔
504
                                        .filter(Objects::nonNull)
1✔
505
                                        .map(entityObject -> getRelationshipVO(method, entityObject))
1✔
506
                                        .toList());
1✔
507
                        return Optional.of(new AbstractMap.SimpleEntry<>(attributeGetter.targetName(), relationshipVOS));
1✔
508
                } catch (IllegalAccessException | InvocationTargetException e) {
1✔
509
                        throw new MappingException(String.format(WAS_NOT_ABLE_INVOKE_METHOD_TEMPLATE, method, entity));
1✔
510
                }
511
        }
512

513
        /**
514
         * Get the relationship for the given method and relationship object
515
         */
516
        private RelationshipVO getRelationshipVO(Method method, Object relationShipObject) {
517
                try {
518

519
                        Method objectMethod = getRelationshipObjectMethod(relationShipObject).orElseThrow(
1✔
520
                                        () -> new MappingException(
1✔
521
                                                        String.format("The relationship %s-%s does not provide an object method.",
1✔
522
                                                                        relationShipObject, method)));
523
                        Object objectObject = objectMethod.invoke(relationShipObject);
1✔
524
                        if (!(objectObject instanceof URI)) {
1✔
525
                                throw new MappingException(
1✔
526
                                                String.format("The object %s of the relationship is not a URI.", relationShipObject));
1✔
527
                        }
528

529
                        Method datasetIdMethod = getDatasetIdMethod(relationShipObject).orElseThrow(() -> new MappingException(
1✔
530
                                        String.format("The relationship %s-%s does not provide a datasetId method.", relationShipObject,
×
531
                                                        method)));
532
                        Object datasetIdObject = datasetIdMethod.invoke(relationShipObject);
1✔
533
                        if (!(datasetIdObject instanceof URI)) {
1✔
534
                                throw new MappingException(
1✔
535
                                                String.format("The datasetId %s of the relationship is not a URI.", relationShipObject));
1✔
536
                        }
537
                        RelationshipVO relationshipVO = new RelationshipVO();
1✔
538
                        relationshipVO.setObject((URI) objectObject);
1✔
539
                        relationshipVO.setDatasetId((URI) datasetIdObject);
1✔
540

541
                        // get additional properties. We do not support more depth/complexity for now
542
                        Map<String, AdditionalPropertyVO> additionalProperties = getAttributeGettersMethods(
1✔
543
                                        relationShipObject).stream()
1✔
544
                                        .map(getterMethod -> getAdditionalPropertyEntryFromMethod(relationShipObject, getterMethod))
1✔
545
                                        .filter(Optional::isPresent)
1✔
546
                                        .map(Optional::get)
1✔
547
                                        .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
1✔
548

549
                        additionalProperties.forEach(relationshipVO::setAdditionalProperties);
1✔
550

551
                        return relationshipVO;
1✔
552
                } catch (IllegalAccessException | InvocationTargetException e) {
×
553
                        throw new MappingException(
×
554
                                        String.format(WAS_NOT_ABLE_INVOKE_METHOD_TEMPLATE, method, relationShipObject));
×
555
                }
556
        }
557

558
        /**
559
         * Get all additional properties for the object of the relationship
560
         */
561
        private Optional<Map.Entry<String, PropertyVO>> getAdditionalPropertyEntryFromMethod(Object relationShipObject,
562
                        Method getterMethod) {
563
                Optional<AttributeGetter> optionalAttributeGetter = getAttributeGetter(getterMethod.getAnnotations());
1✔
564
                if (optionalAttributeGetter.isEmpty() || !optionalAttributeGetter.get().embedProperty()) {
1✔
565
                        return Optional.empty();
1✔
566
                }
567
                if (optionalAttributeGetter.get().value().equals(AttributeType.PROPERTY)) {
1✔
568
                        return methodToPropertyEntry(relationShipObject, getterMethod);
1✔
569
                } else {
570
                        return Optional.empty();
×
571
                }
572
        }
573

574
        /**
575
         * Build a property list entry from the given method on the entity
576
         */
577
        private <T> Optional<Map.Entry<String, PropertyListVO>> methodToPropertyListEntry(T entity, Method method) {
578
                try {
579
                        Object o = method.invoke(entity);
×
580
                        if (o == null) {
×
581
                                return Optional.empty();
×
582
                        }
583
                        if (!(o instanceof List)) {
×
584
                                throw new MappingException(
×
585
                                                String.format("Property list method %s::%s did not return a List.", entity, method));
×
586
                        }
587
                        AttributeGetter attributeMapping = getAttributeGetter(method.getAnnotations()).orElseThrow(
×
588
                                        () -> new MappingException(String.format(NO_MAPPING_DEFINED_FOR_METHOD_TEMPLATE, method)));
×
589
                        List<Object> entityObjects = (List) o;
×
590

591
                        PropertyListVO propertyVOS = new PropertyListVO();
×
592

593
                        propertyVOS.addAll(entityObjects.stream()
×
594
                                        .map(propertyObject -> {
×
595
                                                PropertyVO propertyVO = new PropertyVO();
×
596
                                                propertyVO.setValue(propertyObject);
×
597
                                                return propertyVO;
×
598
                                        })
599
                                        .toList());
×
600

601
                        return Optional.of(new AbstractMap.SimpleEntry<>(attributeMapping.targetName(), propertyVOS));
×
602
                } catch (IllegalAccessException | InvocationTargetException e) {
×
603
                        throw new MappingException(String.format(WAS_NOT_ABLE_INVOKE_METHOD_TEMPLATE, method, entity));
×
604
                }
605
        }
606

607
}
608

609

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