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

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

pending completion
#120

push

beknazaresenbek
Object mapper was updated

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

504 of 680 relevant lines covered (74.12%)

0.74 hits per line

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

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

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

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

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

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

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

31
        private static final String PROPERTY_NAME_Q = "q";
32
        private static final String PROPERTY_NAME_EXPIRES = "expires";
33

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

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

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

112
        }
113

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

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

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

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

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

163
                List<Method> entityIdMethod = new ArrayList<>();
1✔
164
                List<Method> entityTypeMethod = new ArrayList<>();
1✔
165
                List<Method> propertyMethods = new ArrayList<>();
1✔
166
                List<Method> propertyListMethods = new ArrayList<>();
1✔
167
                List<Method> relationshipMethods = new ArrayList<>();
1✔
168
                List<Method> relationshipListMethods = new ArrayList<>();
1✔
169
                List<Method> geoPropertyMethods = new ArrayList<>();
1✔
170
                List<Method> geoPropertyListMethods = new ArrayList<>();
1✔
171

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

195
                if (entityIdMethod.size() != 1) {
1✔
196
                        throw new MappingException(
1✔
197
                                        String.format("The provided object declares %s id methods, exactly one is expected.",
1✔
198
                                                        entityIdMethod.size()));
1✔
199
                }
200
                if (entityTypeMethod.size() != 1) {
1✔
201
                        throw new MappingException(
1✔
202
                                        String.format("The provided object declares %s type methods, exactly one is expected.",
1✔
203
                                                        entityTypeMethod.size()));
1✔
204

205
                }
206

207
                return buildEntity(entity, entityIdMethod.get(0), entityTypeMethod.get(0), propertyMethods,
1✔
208
                                propertyListMethods,
209
                                geoPropertyMethods, relationshipMethods, relationshipListMethods);
210
        }
211

212
        /**
213
         * Build the entity from its declared methods.
214
         */
215
        private <T> EntityVO buildEntity(T entity, Method entityIdMethod, Method entityTypeMethod,
216
                        List<Method> propertyMethods, List<Method> propertyListMethods,
217
                        List<Method> geoPropertyMethods,
218
                        List<Method> relationshipMethods, List<Method> relationshipListMethods) {
219

220
                EntityVO entityVO = new EntityVO();
1✔
221
                // TODO: Check if we need that configurable
222
                entityVO.setAtContext(DEFAULT_CONTEXT);
1✔
223

224
                // TODO: include extraction via annotation for all well-known attributes
225
                entityVO.setOperationSpace(null);
1✔
226
                entityVO.setObservationSpace(null);
1✔
227
                entityVO.setLocation(null);
1✔
228

229
                try {
230
                        Object entityIdObject = entityIdMethod.invoke(entity);
1✔
231
                        if (!(entityIdObject instanceof URI)) {
1✔
232
                                throw new MappingException(
1✔
233
                                                String.format("The entityId method does not return a valid URI for entity %s.", entity));
1✔
234
                        }
235
                        entityVO.id((URI) entityIdObject);
1✔
236

237
                        Object entityTypeObject = entityTypeMethod.invoke(entity);
1✔
238
                        if (!(entityTypeObject instanceof String)) {
1✔
239
                                throw new MappingException("The entityType method does not return a valid String.");
1✔
240
                        }
241
                        entityVO.setType((String) entityTypeObject);
1✔
242
                } catch (IllegalAccessException | InvocationTargetException e) {
1✔
243
                        throw new MappingException(String.format(WAS_NOT_ABLE_INVOKE_METHOD_TEMPLATE, "unknown-method", entity),
1✔
244
                                        e);
245
                }
1✔
246

247
                Map<String, AdditionalPropertyVO> additionalProperties = new LinkedHashMap<>();
1✔
248
                additionalProperties.putAll(buildProperties(entity, propertyMethods));
1✔
249
                additionalProperties.putAll(buildPropertyList(entity, propertyListMethods));
1✔
250
                additionalProperties.putAll(buildGeoProperties(entity, geoPropertyMethods));
1✔
251
                Map<String, RelationshipVO> relationshipVOMap = buildRelationships(entity, relationshipMethods);
1✔
252
                Map<String, RelationshipListVO> relationshipListVOMap = buildRelationshipList(entity,
1✔
253
                                relationshipListMethods);
254
                // we need to post-process the relationships, since orion-ld only accepts dataset-ids for lists > 1
255
                relationshipVOMap.entrySet().stream().forEach(e -> e.getValue().setDatasetId(null));
1✔
256
                relationshipListVOMap.entrySet().stream().forEach(e -> {
1✔
257
                        if (e.getValue().size() == 1) {
1✔
258
                                e.getValue().get(0).setDatasetId(null);
1✔
259
                        }
260
                });
1✔
261

262
                additionalProperties.putAll(relationshipVOMap);
1✔
263
                additionalProperties.putAll(relationshipListVOMap);
1✔
264

265
                additionalProperties.forEach(entityVO::setAdditionalProperties);
1✔
266

267
                return entityVO;
1✔
268
        }
269

270
        public <T> SubscriptionVO toSubscriptionVO(T subscription) {
271
                isMappingEnabled(subscription.getClass())
×
272
                                .orElseThrow(() -> new UnsupportedOperationException(
×
273
                                                String.format("Generic mapping to NGSI-LD subscriptions is not supported for object %s",
×
274
                                                                subscription)));
275

276
                List<Method> subscriptionIdMethod = new ArrayList<>();
×
277
                List<Method> subscriptionTypeMethod = new ArrayList<>();
×
278
                Map<String, Method> propertyMethods = new HashMap<>();
×
279
                List<Method> propertySetMethods = new ArrayList<>();
×
280
                List<Method> geoQueryMethods = new ArrayList<>();
×
281
                List<Method> entityInfoMethods = new ArrayList<>();
×
282
                List<Method> notificationMethods = new ArrayList<>();
×
283

284
                Arrays.stream(subscription.getClass().getMethods()).forEach(method -> {
×
285
                        if (isSubscriptionIdMethod(method)) {
×
286
                                subscriptionIdMethod.add(method);
×
287
                        } else if (isSubscriptionTypeMethod(method)) {
×
288
                                subscriptionTypeMethod.add(method);
×
289
                        } else {
290
                                getAttributeGetter(method.getAnnotations()).ifPresent(annotation -> {
×
291
                                        switch (annotation.value()) {
×
292
                                                case PROPERTY -> propertyMethods.put(annotation.targetName(), method);
×
293
                                                case PROPERTY_SET -> propertySetMethods.add(method);
×
294
                                                case GEO_QUERY -> geoQueryMethods.add(method);
×
295
                                                case ENTITY_INFO_LIST -> entityInfoMethods.add(method);
×
296
                                                case NOTIFICATION_PARAMS -> notificationMethods.add(method);
×
297
                                                default -> throw new UnsupportedOperationException(
×
298
                                                                String.format("Mapping target %s is not supported.", annotation.value()));
×
299
                                        }
300
                                });
×
301
                        }
302
                });
×
303

304
                if (subscriptionIdMethod.size() != 1) {
×
305
                        throw new MappingException(
×
306
                                        String.format("The provided object declares %s id methods, exactly one is expected.",
×
307
                                                        subscriptionIdMethod.size()));
×
308
                }
309
                if (subscriptionTypeMethod.size() != 1) {
×
310
                        throw new MappingException(
×
311
                                        String.format("The provided object declares %s type methods, exactly one is expected.",
×
312
                                                        subscriptionTypeMethod.size()));
×
313

314
                }
315

316
                return buildSubscription(subscription, subscriptionIdMethod.get(0), subscriptionTypeMethod.get(0),
×
317
                                propertyMethods, geoQueryMethods.get(0), propertySetMethods.get(0), entityInfoMethods.get(0),
×
318
                                notificationMethods.get(0));
×
319
        }
320

321
        private <T> SubscriptionVO buildSubscription(T subscription, Method subscriptionIdMethod, Method subscriptionTypeMethod,
322
                                                                                                 Map<String, Method> propertyMethods, Method geoQueryMethod,
323
                                                                                                 Method propertySetMethod, Method entityInfoMethod,
324
                                                                                                 Method notificationMethod) {
325
                SubscriptionVO subscriptionVO = new SubscriptionVO();
×
326

327
                subscriptionVO.setAtContext(DEFAULT_CONTEXT);
×
328

329
                try {
330
                        Object subscriptionIdObject = subscriptionIdMethod.invoke(subscription);
×
331
                        if (!(subscriptionIdObject instanceof URI)) {
×
332
                                throw new MappingException(
×
333
                                                String.format("The subscriptionId method does not return a valid URI for subscription %s.",
×
334
                                                                subscription));
335
                        }
336
                        subscriptionVO.setId((URI) subscriptionIdObject);
×
337

338
                        Object subscriptionTypeObject = subscriptionTypeMethod.invoke(subscription);
×
339
                        if (!(subscriptionTypeObject instanceof String)) {
×
340
                                throw new MappingException("The subscriptionType method does not return a valid String.");
×
341
                        }
342
                        subscriptionVO.setType(SubscriptionVO.Type.toEnum(subscriptionTypeObject.toString()));
×
343

344
                        Object geoQueryObject = geoQueryMethod.invoke(subscription);
×
345
                        subscriptionVO.setGeoQ((GeoQueryVO) geoQueryObject);
×
346

347
                        Object queryObject = propertyMethods.get(PROPERTY_NAME_Q).invoke(subscription);
×
348
                        if (!(queryObject instanceof String)) {
×
349
                                throw new MappingException("The query method does not return a valid String.");
×
350
                        }
351
                        subscriptionVO.setQ(queryObject.toString());
×
352

353
                        Object expiresObject = propertyMethods.get(PROPERTY_NAME_EXPIRES).invoke(subscription);
×
354
                        if (!(expiresObject instanceof Instant)) {
×
355
                                throw new MappingException("The query method does not return a valid Instant.");
×
356
                        }
357
                        subscriptionVO.setExpires((Instant) expiresObject);
×
358

359
                        Object notificationObject = notificationMethod.invoke(subscription);
×
360
                        subscriptionVO.setNotification((NotificationParamsVO) notificationObject);
×
361

362
                        Object propertySetObject = propertySetMethod.invoke(subscription);
×
363
                        subscriptionVO.setWatchedAttributes((Set<String>) propertySetObject);
×
364

365
                        Object entitiesObject = entityInfoMethod.invoke(subscription);
×
366
                        subscriptionVO.setEntities((List<EntityInfoVO>) entitiesObject);
×
367
                } catch (IllegalAccessException | InvocationTargetException e) {
×
368
                        throw new MappingException(String.format(WAS_NOT_ABLE_INVOKE_METHOD_TEMPLATE, "unknown-method", subscription),
×
369
                                        e);
370
                }
×
371

372
                return subscriptionVO;
×
373
        }
374

375
        /**
376
         * Check if the given method defines the entity type
377
         */
378
        private boolean isEntityTypeMethod(Method method) {
379
                return Arrays.stream(method.getAnnotations()).anyMatch(EntityType.class::isInstance);
1✔
380
        }
381

382
        /**
383
         * Check if the given method defines the entity id
384
         */
385
        private boolean isEntityIdMethod(Method method) {
386
                return Arrays.stream(method.getAnnotations()).anyMatch(EntityId.class::isInstance);
1✔
387
        }
388

389
        /**
390
         * Check if the given method defines the subscription type
391
         */
392
        private boolean isSubscriptionTypeMethod(Method method) {
393
                return Arrays.stream(method.getAnnotations()).anyMatch(SubscriptionType.class::isInstance);
×
394
        }
395

396
        /**
397
         * Check if the given method defines the subscription id
398
         */
399
        private boolean isSubscriptionIdMethod(Method method) {
400
                return Arrays.stream(method.getAnnotations()).anyMatch(SubscriptionId.class::isInstance);
×
401
        }
402

403
        /**
404
         * Build a relationship from the declared methods
405
         */
406
        private <T> Map<String, RelationshipVO> buildRelationships(T entity, List<Method> relationshipMethods) {
407
                return relationshipMethods.stream()
1✔
408
                                .map(method -> methodToRelationshipEntry(entity, method))
1✔
409
                                .filter(Optional::isPresent)
1✔
410
                                .map(Optional::get)
1✔
411
                                .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
1✔
412
        }
413

414
        /**
415
         * Build a list of relationships from the declared methods
416
         */
417
        private <
418
                        T> Map<String, RelationshipListVO> buildRelationshipList(T entity, List<Method> relationshipListMethods) {
419
                return relationshipListMethods.stream()
1✔
420
                                .map(relationshipMethod -> methodToRelationshipListEntry(entity, relationshipMethod))
1✔
421
                                .filter(Optional::isPresent)
1✔
422
                                .map(Optional::get)
1✔
423
                                .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
1✔
424
        }
425

426
        /*
427
         * Build a list of properties from the declared methods
428
         */
429
        private <T> Map<String, PropertyListVO> buildPropertyList(T entity, List<Method> propertyListMethods) {
430
                return propertyListMethods.stream()
1✔
431
                                .map(propertyListMethod -> methodToPropertyListEntry(entity, propertyListMethod))
1✔
432
                                .filter(Optional::isPresent)
1✔
433
                                .map(Optional::get)
1✔
434
                                .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
1✔
435
        }
436

437
        /**
438
         * Build geoproperties from the declared methods
439
         */
440
        private <T> Map<String, GeoPropertyVO> buildGeoProperties(T entity, List<Method> geoPropertyMethods) {
441
                return geoPropertyMethods.stream()
1✔
442
                                .map(geoPropertyMethod -> methodToGeoPropertyEntry(entity, geoPropertyMethod))
1✔
443
                                .filter(Optional::isPresent)
1✔
444
                                .map(Optional::get)
1✔
445
                                .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
1✔
446
        }
447

448
        /**
449
         * Build properties from the declared methods
450
         */
451
        private <T> Map<String, PropertyVO> buildProperties(T entity, List<Method> propertyMethods) {
452
                return propertyMethods.stream()
1✔
453
                                .map(propertyMethod -> methodToPropertyEntry(entity, propertyMethod))
1✔
454
                                .filter(Optional::isPresent)
1✔
455
                                .map(Optional::get)
1✔
456
                                .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
1✔
457
        }
458

459
        /**
460
         * Return method defining the object of the relationship for the given entity, if exists.
461
         */
462
        private <T> Optional<Method> getRelationshipObjectMethod(T entity) {
463
                return Arrays.stream(entity.getClass().getMethods()).filter(this::isRelationShipObject).findFirst();
1✔
464
        }
465

466
        /**
467
         * Return method defining the datasetid for the given entity, if exists.
468
         */
469
        private <T> Optional<Method> getDatasetIdMethod(T entity) {
470
                return Arrays.stream(entity.getClass().getMethods()).filter(this::isDatasetId).findFirst();
1✔
471
        }
472

473
        /**
474
         * Get all methods declared as attribute getters.
475
         */
476
        private <T> List<Method> getAttributeGettersMethods(T entity) {
477
                return Arrays.stream(entity.getClass().getMethods())
1✔
478
                                .filter(m -> getAttributeGetterAnnotation(m).isPresent())
1✔
479
                                .toList();
1✔
480
        }
481

482
        /**
483
         * return the {@link  AttributeGetter} annotation for the method if there is such.
484
         */
485
        private Optional<AttributeGetter> getAttributeGetterAnnotation(Method m) {
486
                return Arrays.stream(m.getAnnotations()).filter(AttributeGetter.class::isInstance).findFirst()
1✔
487
                                .map(AttributeGetter.class::cast);
1✔
488
        }
489

490
        /**
491
         * Find the attribute getter from all the annotations.
492
         */
493
        private static Optional<AttributeGetter> getAttributeGetter(Annotation[] annotations) {
494
                return Arrays.stream(annotations).filter(AttributeGetter.class::isInstance).map(AttributeGetter.class::cast)
1✔
495
                                .findFirst();
1✔
496
        }
497

498
        /**
499
         * Find the attribute setter from all the annotations.
500
         */
501
        private static Optional<AttributeSetter> getAttributeSetter(Annotation[] annotations) {
502
                return Arrays.stream(annotations).filter(AttributeSetter.class::isInstance).map(AttributeSetter.class::cast)
1✔
503
                                .findFirst();
1✔
504
        }
505

506
        /**
507
         * Check if the given method is declared to be used as object of a relationship
508
         */
509
        private boolean isRelationShipObject(Method m) {
510
                return Arrays.stream(m.getAnnotations()).anyMatch(RelationshipObject.class::isInstance);
1✔
511
        }
512

513
        /**
514
         * Check if the given method is declared to be used as datasetId
515
         */
516
        private boolean isDatasetId(Method m) {
517
                return Arrays.stream(m.getAnnotations()).anyMatch(DatasetId.class::isInstance);
1✔
518
        }
519

520
        /**
521
         * Build a property entry from the given method on the entity
522
         */
523
        private <T> Optional<Map.Entry<String, PropertyVO>> methodToPropertyEntry(T entity, Method method) {
524
                try {
525
                        Object propertyObject = method.invoke(entity);
1✔
526
                        if (propertyObject == null) {
1✔
527
                                return Optional.empty();
×
528
                        }
529
                        AttributeGetter attributeMapping = getAttributeGetter(method.getAnnotations()).orElseThrow(
1✔
530
                                        () -> new MappingException(String.format(NO_MAPPING_DEFINED_FOR_METHOD_TEMPLATE, method)));
×
531

532
                        PropertyVO propertyVO = new PropertyVO();
1✔
533
                        propertyVO.setValue(propertyObject);
1✔
534

535
                        return Optional.of(new AbstractMap.SimpleEntry<>(attributeMapping.targetName(), propertyVO));
1✔
536
                } catch (IllegalAccessException | InvocationTargetException e) {
1✔
537
                        throw new MappingException(String.format(WAS_NOT_ABLE_INVOKE_METHOD_TEMPLATE, method, entity));
1✔
538
                }
539
        }
540

541
        /**
542
         * Build a geo-property entry from the given method on the entity
543
         */
544
        private <T> Optional<Map.Entry<String, GeoPropertyVO>> methodToGeoPropertyEntry(T entity, Method method) {
545
                try {
546
                        Object o = method.invoke(entity);
×
547
                        if (o == null) {
×
548
                                return Optional.empty();
×
549
                        }
550
                        AttributeGetter attributeMapping = getAttributeGetter(method.getAnnotations()).orElseThrow(
×
551
                                        () -> new MappingException(String.format(NO_MAPPING_DEFINED_FOR_METHOD_TEMPLATE, method)));
×
552
                        GeoPropertyVO geoPropertyVO = new GeoPropertyVO();
×
553
                        geoPropertyVO.setValue(o);
×
554
                        return Optional.of(new AbstractMap.SimpleEntry<>(attributeMapping.targetName(), geoPropertyVO));
×
555
                } catch (IllegalAccessException | InvocationTargetException e) {
×
556
                        throw new MappingException(String.format(WAS_NOT_ABLE_INVOKE_METHOD_TEMPLATE, method, entity));
×
557
                }
558
        }
559

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

578
        /**
579
         * Build a relationship list entry from the given method on the entity
580
         */
581
        private <
582
                        T> Optional<Map.Entry<String, RelationshipListVO>> methodToRelationshipListEntry(T entity, Method method) {
583
                try {
584
                        Object o = method.invoke(entity);
1✔
585
                        if (o == null) {
1✔
586
                                return Optional.empty();
1✔
587
                        }
588
                        if (!(o instanceof List)) {
1✔
589
                                throw new MappingException(
1✔
590
                                                String.format("Relationship list method %s::%s did not return a List.", entity, method));
1✔
591
                        }
592
                        List<Object> entityObjects = (List) o;
1✔
593

594
                        AttributeGetter attributeGetter = getAttributeGetter(method.getAnnotations()).orElseThrow(
1✔
595
                                        () -> new MappingException(String.format(NO_MAPPING_DEFINED_FOR_METHOD_TEMPLATE, method)));
×
596
                        RelationshipListVO relationshipVOS = new RelationshipListVO();
1✔
597

598
                        relationshipVOS.addAll(entityObjects.stream()
1✔
599
                                        .filter(Objects::nonNull)
1✔
600
                                        .map(entityObject -> getRelationshipVO(method, entityObject))
1✔
601
                                        .toList());
1✔
602
                        return Optional.of(new AbstractMap.SimpleEntry<>(attributeGetter.targetName(), relationshipVOS));
1✔
603
                } catch (IllegalAccessException | InvocationTargetException e) {
1✔
604
                        throw new MappingException(String.format(WAS_NOT_ABLE_INVOKE_METHOD_TEMPLATE, method, entity));
1✔
605
                }
606
        }
607

608
        /**
609
         * Get the relationship for the given method and relationship object
610
         */
611
        private RelationshipVO getRelationshipVO(Method method, Object relationShipObject) {
612
                try {
613

614
                        Method objectMethod = getRelationshipObjectMethod(relationShipObject).orElseThrow(
1✔
615
                                        () -> new MappingException(
1✔
616
                                                        String.format("The relationship %s-%s does not provide an object method.",
1✔
617
                                                                        relationShipObject, method)));
618
                        Object objectObject = objectMethod.invoke(relationShipObject);
1✔
619
                        if (!(objectObject instanceof URI)) {
1✔
620
                                throw new MappingException(
1✔
621
                                                String.format("The object %s of the relationship is not a URI.", relationShipObject));
1✔
622
                        }
623

624
                        Method datasetIdMethod = getDatasetIdMethod(relationShipObject).orElseThrow(() -> new MappingException(
1✔
625
                                        String.format("The relationship %s-%s does not provide a datasetId method.", relationShipObject,
×
626
                                                        method)));
627
                        Object datasetIdObject = datasetIdMethod.invoke(relationShipObject);
1✔
628
                        if (!(datasetIdObject instanceof URI)) {
1✔
629
                                throw new MappingException(
1✔
630
                                                String.format("The datasetId %s of the relationship is not a URI.", relationShipObject));
1✔
631
                        }
632
                        RelationshipVO relationshipVO = new RelationshipVO();
1✔
633
                        relationshipVO.setObject((URI) objectObject);
1✔
634
                        relationshipVO.setDatasetId((URI) datasetIdObject);
1✔
635

636
                        // get additional properties. We do not support more depth/complexity for now
637
                        Map<String, AdditionalPropertyVO> additionalProperties = getAttributeGettersMethods(
1✔
638
                                        relationShipObject).stream()
1✔
639
                                        .map(getterMethod -> getAdditionalPropertyEntryFromMethod(relationShipObject, getterMethod))
1✔
640
                                        .filter(Optional::isPresent)
1✔
641
                                        .map(Optional::get)
1✔
642
                                        .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
1✔
643

644
                        additionalProperties.forEach(relationshipVO::setAdditionalProperties);
1✔
645

646
                        return relationshipVO;
1✔
647
                } catch (IllegalAccessException | InvocationTargetException e) {
×
648
                        throw new MappingException(
×
649
                                        String.format(WAS_NOT_ABLE_INVOKE_METHOD_TEMPLATE, method, relationShipObject));
×
650
                }
651
        }
652

653
        /**
654
         * Get all additional properties for the object of the relationship
655
         */
656
        private Optional<Map.Entry<String, PropertyVO>> getAdditionalPropertyEntryFromMethod(Object relationShipObject,
657
                        Method getterMethod) {
658
                Optional<AttributeGetter> optionalAttributeGetter = getAttributeGetter(getterMethod.getAnnotations());
1✔
659
                if (optionalAttributeGetter.isEmpty() || !optionalAttributeGetter.get().embedProperty()) {
1✔
660
                        return Optional.empty();
1✔
661
                }
662
                if (optionalAttributeGetter.get().value().equals(AttributeType.PROPERTY)) {
1✔
663
                        return methodToPropertyEntry(relationShipObject, getterMethod);
1✔
664
                } else {
665
                        return Optional.empty();
×
666
                }
667
        }
668

669
        /**
670
         * Build a property list entry from the given method on the entity
671
         */
672
        private <T> Optional<Map.Entry<String, PropertyListVO>> methodToPropertyListEntry(T entity, Method method) {
673
                try {
674
                        Object o = method.invoke(entity);
×
675
                        if (o == null) {
×
676
                                return Optional.empty();
×
677
                        }
678
                        if (!(o instanceof List)) {
×
679
                                throw new MappingException(
×
680
                                                String.format("Property list method %s::%s did not return a List.", entity, method));
×
681
                        }
682
                        AttributeGetter attributeMapping = getAttributeGetter(method.getAnnotations()).orElseThrow(
×
683
                                        () -> new MappingException(String.format(NO_MAPPING_DEFINED_FOR_METHOD_TEMPLATE, method)));
×
684
                        List<Object> entityObjects = (List) o;
×
685

686
                        PropertyListVO propertyVOS = new PropertyListVO();
×
687

688
                        propertyVOS.addAll(entityObjects.stream()
×
689
                                        .map(propertyObject -> {
×
690
                                                PropertyVO propertyVO = new PropertyVO();
×
691
                                                propertyVO.setValue(propertyObject);
×
692
                                                return propertyVO;
×
693
                                        })
694
                                        .toList());
×
695

696
                        return Optional.of(new AbstractMap.SimpleEntry<>(attributeMapping.targetName(), propertyVOS));
×
697
                } catch (IllegalAccessException | InvocationTargetException e) {
×
698
                        throw new MappingException(String.format(WAS_NOT_ABLE_INVOKE_METHOD_TEMPLATE, method, entity));
×
699
                }
700
        }
701

702
}
703

704

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