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

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

07 Dec 2023 12:08PM UTC coverage: 79.483% (-0.4%) from 79.859%
#153

Pull #46

wistefan
testing and empty handling
Pull Request #46: allow mappings with non-existent relationships

249 of 312 new or added lines in 3 files covered. (79.81%)

2 existing lines in 1 file now uncovered.

461 of 580 relevant lines covered (79.48%)

0.79 hits per line

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

82.08
/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
1✔
43
public class JavaObjectMapper extends Mapper {
44

45
    private final MappingProperties mappingProperties;
46

47
    public static final String NO_MAPPING_DEFINED_FOR_METHOD_TEMPLATE = "No mapping defined for method %s";
48
    public static final String WAS_NOT_ABLE_INVOKE_METHOD_TEMPLATE = "Was not able invoke method %s on %s";
49

50

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

102
    private static QueryAttributeType evaluateType(List<String> path, Class<?> tClass) {
103
        Class<?> currentClass = tClass;
1✔
104
        for (String s : path) {
1✔
105
            try {
106
                Optional<Class<?>> optionalReturn = getGetterMethodByName(currentClass, s).findAny()
1✔
107
                        .map(Method::getReturnType);
1✔
108
                if (optionalReturn.isPresent()) {
1✔
109
                    currentClass = optionalReturn.get();
1✔
110
                } else {
NEW
111
                    currentClass = currentClass.getField(s).getType();
×
112
                }
NEW
113
            } catch (NoSuchFieldException e) {
×
NEW
114
                throw new MappingException(String.format("No field %s exists for %s.", s, tClass.getCanonicalName()),
×
115
                        e);
116
            }
1✔
117
        }
1✔
118
        return fromClass(currentClass);
1✔
119
    }
120

121
    private static QueryAttributeType fromClass(Class<?> tClass) {
122
        if (Number.class.isAssignableFrom(tClass)) {
1✔
123
            return QueryAttributeType.NUMBER;
1✔
124
        } else if (Boolean.class.isAssignableFrom(tClass)) {
1✔
125
            return QueryAttributeType.BOOLEAN;
1✔
126
        }
127
        return QueryAttributeType.STRING;
1✔
128

129
    }
130

131
    public static <T> Stream<Method> getSetterMethodByName(Class<T> tClass, String propertyName) {
132
        return Arrays.stream(tClass.getMethods())
1✔
133
                .filter(m -> getCorrespondingSetterFieldName(m.getName()).equals(propertyName));
1✔
134
    }
135

136
    public static <T> Stream<Method> getGetterMethodByName(Class<T> tClass, String propertyName) {
137
        return Arrays.stream(tClass.getMethods())
1✔
138
                .filter(m -> getCorrespondingGetterFieldName(m.getName()).equals(propertyName));
1✔
139
    }
140

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

154
    private static String getCorrespondingSetterFieldName(String methodName) {
155
        var fieldName = "";
1✔
156
        if (methodName.matches("^set[A-Z].*")) {
1✔
157
            fieldName = methodName.replaceFirst("set", "");
1✔
158
        } else if (methodName.matches("^is[A-Z].*")) {
1✔
NEW
159
            fieldName = methodName.replaceFirst("is", "");
×
160
        } else {
161
            log.debug("The method {} is neither a set or is.", methodName);
1✔
162
            return fieldName;
1✔
163
        }
164
        return fieldName.substring(0, 1).toLowerCase() + fieldName.substring(1);
1✔
165
    }
166

167
    /**
168
     * Translate the given object into an Entity.
169
     *
170
     * @param entity the object representing the entity
171
     * @param <T>    class of the entity
172
     * @return the NGIS-LD entity objet
173
     */
174
    public <T> EntityVO toEntityVO(T entity) {
175
        isMappingEnabled(entity.getClass())
1✔
176
                .orElseThrow(() -> new UnsupportedOperationException(
1✔
177
                        String.format("Generic mapping to NGSI-LD entities is not supported for object %s",
1✔
178
                                entity)));
179

180
        List<Method> entityIdMethod = new ArrayList<>();
1✔
181
        List<Method> entityTypeMethod = new ArrayList<>();
1✔
182
        List<Method> propertyMethods = new ArrayList<>();
1✔
183
        List<Method> propertyListMethods = new ArrayList<>();
1✔
184
        List<Method> relationshipMethods = new ArrayList<>();
1✔
185
        List<Method> relationshipListMethods = new ArrayList<>();
1✔
186
        List<Method> geoPropertyMethods = new ArrayList<>();
1✔
187
        List<Method> geoPropertyListMethods = new ArrayList<>();
1✔
188

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

212
        if (entityIdMethod.size() != 1) {
1✔
213
            throw new MappingException(
1✔
214
                    String.format("The provided object declares %s id methods, exactly one is expected.",
1✔
215
                            entityIdMethod.size()));
1✔
216
        }
217
        if (entityTypeMethod.size() != 1) {
1✔
218
            throw new MappingException(
1✔
219
                    String.format("The provided object declares %s type methods, exactly one is expected.",
1✔
220
                            entityTypeMethod.size()));
1✔
221

222
        }
223

224
        return buildEntity(entity, entityIdMethod.get(0), entityTypeMethod.get(0), propertyMethods,
1✔
225
                propertyListMethods,
226
                geoPropertyMethods, relationshipMethods, relationshipListMethods);
227
    }
228

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

237
        EntityVO entityVO = new EntityVO();
1✔
238
        entityVO.setAtContext(mappingProperties.getContextUrl());
1✔
239

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

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

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

263
        Map<String, AdditionalPropertyVO> additionalProperties = new LinkedHashMap<>();
1✔
264
        additionalProperties.putAll(buildProperties(entity, propertyMethods));
1✔
265
        additionalProperties.putAll(buildPropertyList(entity, propertyListMethods));
1✔
266
        additionalProperties.putAll(buildGeoProperties(entity, geoPropertyMethods));
1✔
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
     * Build a relationship from the declared methods
302
     */
303
    private <T> Map<String, RelationshipVO> buildRelationships(T entity, List<Method> relationshipMethods) {
304
        return relationshipMethods.stream()
1✔
305
                .map(method -> methodToRelationshipEntry(entity, method))
1✔
306
                .filter(Optional::isPresent)
1✔
307
                .map(Optional::get)
1✔
308
                .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
1✔
309
    }
310

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

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

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

345
    /**
346
     * Build properties from the declared methods
347
     */
348
    private <T> Map<String, PropertyVO> buildProperties(T entity, List<Method> propertyMethods) {
349
        return propertyMethods.stream()
1✔
350
                .map(propertyMethod -> methodToPropertyEntry(entity, propertyMethod))
1✔
351
                .filter(Optional::isPresent)
1✔
352
                .map(Optional::get)
1✔
353
                .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
1✔
354
    }
355

356
    /**
357
     * Return method defining the object of the relationship for the given entity, if exists.
358
     */
359
    private <T> Optional<Method> getRelationshipObjectMethod(T entity) {
360
        return Arrays.stream(entity.getClass().getMethods()).filter(this::isRelationShipObject).findFirst();
1✔
361
    }
362

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

370
    /**
371
     * Get all methods declared as attribute getters.
372
     */
373
    private <T> List<Method> getAttributeGettersMethods(T entity) {
374
        return Arrays.stream(entity.getClass().getMethods())
1✔
375
                .filter(m -> getAttributeGetterAnnotation(m).isPresent())
1✔
376
                .toList();
1✔
377
    }
378

379
    /**
380
     * return the {@link  AttributeGetter} annotation for the method if there is such.
381
     */
382
    private Optional<AttributeGetter> getAttributeGetterAnnotation(Method m) {
383
        return Arrays.stream(m.getAnnotations()).filter(AttributeGetter.class::isInstance).findFirst()
1✔
384
                .map(AttributeGetter.class::cast);
1✔
385
    }
386

387
    /**
388
     * Find the attribute getter from all the annotations.
389
     */
390
    private static Optional<AttributeGetter> getAttributeGetter(Annotation[] annotations) {
391
        return Arrays.stream(annotations).filter(AttributeGetter.class::isInstance).map(AttributeGetter.class::cast)
1✔
392
                .findFirst();
1✔
393
    }
394

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

403
    /**
404
     * Check if the given method is declared to be used as object of a relationship
405
     */
406
    private boolean isRelationShipObject(Method m) {
407
        return Arrays.stream(m.getAnnotations()).anyMatch(RelationshipObject.class::isInstance);
1✔
408
    }
409

410
    /**
411
     * Check if the given method is declared to be used as datasetId
412
     */
413
    private boolean isDatasetId(Method m) {
414
        return Arrays.stream(m.getAnnotations()).anyMatch(DatasetId.class::isInstance);
1✔
415
    }
416

417
    /**
418
     * Build a property entry from the given method on the entity
419
     */
420
    private <T> Optional<Map.Entry<String, PropertyVO>> methodToPropertyEntry(T entity, Method method) {
421
        try {
422
            Object propertyObject = method.invoke(entity);
1✔
423
            if (propertyObject == null) {
1✔
NEW
424
                return Optional.empty();
×
425
            }
426
            AttributeGetter attributeMapping = getAttributeGetter(method.getAnnotations()).orElseThrow(
1✔
NEW
427
                    () -> new MappingException(String.format(NO_MAPPING_DEFINED_FOR_METHOD_TEMPLATE, method)));
×
428

429
            PropertyVO propertyVO = new PropertyVO();
1✔
430
            propertyVO.setValue(propertyObject);
1✔
431

432
            return Optional.of(new AbstractMap.SimpleEntry<>(attributeMapping.targetName(), propertyVO));
1✔
433
        } catch (IllegalAccessException | InvocationTargetException e) {
1✔
434
            throw new MappingException(String.format(WAS_NOT_ABLE_INVOKE_METHOD_TEMPLATE, method, entity));
1✔
435
        }
436
    }
437

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

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

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

491
            AttributeGetter attributeGetter = getAttributeGetter(method.getAnnotations()).orElseThrow(
1✔
NEW
492
                    () -> new MappingException(String.format(NO_MAPPING_DEFINED_FOR_METHOD_TEMPLATE, method)));
×
493
            RelationshipListVO relationshipVOS = new RelationshipListVO();
1✔
494

495
            relationshipVOS.addAll(entityObjects.stream()
1✔
496
                    .filter(Objects::nonNull)
1✔
497
                    .map(entityObject -> getRelationshipVO(method, entityObject))
1✔
498
                    .toList());
1✔
499
            return Optional.of(new AbstractMap.SimpleEntry<>(attributeGetter.targetName(), relationshipVOS));
1✔
500
        } catch (IllegalAccessException | InvocationTargetException e) {
1✔
501
            throw new MappingException(String.format(WAS_NOT_ABLE_INVOKE_METHOD_TEMPLATE, method, entity));
1✔
502
        }
503
    }
504

505
    /**
506
     * Get the relationship for the given method and relationship object
507
     */
508
    private RelationshipVO getRelationshipVO(Method method, Object relationShipObject) {
509
        try {
510

511
            Method objectMethod = getRelationshipObjectMethod(relationShipObject).orElseThrow(
1✔
512
                    () -> new MappingException(
1✔
513
                            String.format("The relationship %s-%s does not provide an object method.",
1✔
514
                                    relationShipObject, method)));
515
            Object objectObject = objectMethod.invoke(relationShipObject);
1✔
516
            if (!(objectObject instanceof URI)) {
1✔
517
                throw new MappingException(
1✔
518
                        String.format("The object %s of the relationship is not a URI.", relationShipObject));
1✔
519
            }
520

521
            Method datasetIdMethod = getDatasetIdMethod(relationShipObject).orElseThrow(() -> new MappingException(
1✔
NEW
522
                    String.format("The relationship %s-%s does not provide a datasetId method.", relationShipObject,
×
523
                            method)));
524
            Object datasetIdObject = datasetIdMethod.invoke(relationShipObject);
1✔
525
            if (!(datasetIdObject instanceof URI)) {
1✔
526
                throw new MappingException(
1✔
527
                        String.format("The datasetId %s of the relationship is not a URI.", relationShipObject));
1✔
528
            }
529
            RelationshipVO relationshipVO = new RelationshipVO();
1✔
530
            relationshipVO.setObject((URI) objectObject);
1✔
531
            relationshipVO.setDatasetId((URI) datasetIdObject);
1✔
532

533
            // get additional properties. We do not support more depth/complexity for now
534
            Map<String, AdditionalPropertyVO> additionalProperties = getAttributeGettersMethods(
1✔
535
                    relationShipObject).stream()
1✔
536
                    .map(getterMethod -> getAdditionalPropertyEntryFromMethod(relationShipObject, getterMethod))
1✔
537
                    .filter(Optional::isPresent)
1✔
538
                    .map(Optional::get)
1✔
539
                    .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
1✔
540

541
            additionalProperties.forEach(relationshipVO::setAdditionalProperties);
1✔
542

543
            return relationshipVO;
1✔
NEW
544
        } catch (IllegalAccessException | InvocationTargetException e) {
×
NEW
545
            throw new MappingException(
×
NEW
546
                    String.format(WAS_NOT_ABLE_INVOKE_METHOD_TEMPLATE, method, relationShipObject));
×
547
        }
548
    }
549

550
    /**
551
     * Get all additional properties for the object of the relationship
552
     */
553
    private Optional<Map.Entry<String, PropertyVO>> getAdditionalPropertyEntryFromMethod(Object relationShipObject,
554
                                                                                         Method getterMethod) {
555
        Optional<AttributeGetter> optionalAttributeGetter = getAttributeGetter(getterMethod.getAnnotations());
1✔
556
        if (optionalAttributeGetter.isEmpty() || !optionalAttributeGetter.get().embedProperty()) {
1✔
557
            return Optional.empty();
1✔
558
        }
559
        if (optionalAttributeGetter.get().value().equals(AttributeType.PROPERTY)) {
1✔
560
            return methodToPropertyEntry(relationShipObject, getterMethod);
1✔
561
        } else {
NEW
562
            return Optional.empty();
×
563
        }
564
    }
565

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

NEW
583
            PropertyListVO propertyVOS = new PropertyListVO();
×
584

NEW
585
            propertyVOS.addAll(entityObjects.stream()
×
NEW
586
                    .map(propertyObject -> {
×
NEW
587
                        PropertyVO propertyVO = new PropertyVO();
×
NEW
588
                        propertyVO.setValue(propertyObject);
×
NEW
589
                        return propertyVO;
×
590
                    })
NEW
591
                    .toList());
×
592

NEW
593
            return Optional.of(new AbstractMap.SimpleEntry<>(attributeMapping.targetName(), propertyVOS));
×
NEW
594
        } catch (IllegalAccessException | InvocationTargetException e) {
×
NEW
595
            throw new MappingException(String.format(WAS_NOT_ABLE_INVOKE_METHOD_TEMPLATE, method, entity));
×
596
        }
597
    }
598

599
}
600

601

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