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

wistefan / tmforum-api / #186

20 May 2026 12:29PM UTC coverage: 31.038%. First build
#186

push

web-flow
Merge 5b7218a10 into 16b0cdbdb

423 of 5804 branches covered (7.29%)

Branch coverage included in aggregate %.

22 of 29 new or added lines in 1 file covered. (75.86%)

2800 of 4580 relevant lines covered (61.14%)

0.61 hits per line

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

48.19
/common/src/main/java/org/fiware/tmforum/common/mapping/BaseMapper.java
1
package org.fiware.tmforum.common.mapping;
2

3
import com.fasterxml.jackson.databind.JsonNode;
4
import com.fasterxml.jackson.databind.node.TextNode;
5
import com.networknt.schema.*;
6
import com.networknt.schema.resource.ClasspathSchemaLoader;
7
import com.networknt.schema.resource.UriSchemaLoader;
8
import lombok.extern.slf4j.Slf4j;
9
import org.fiware.tmforum.common.domain.Characteristic;
10
import org.fiware.tmforum.common.domain.Entity;
11
import org.fiware.tmforum.common.domain.Money;
12
import org.mapstruct.AfterMapping;
13
import org.mapstruct.Mapping;
14
import org.mapstruct.MappingTarget;
15

16
import java.util.ArrayList;
17
import java.util.LinkedHashMap;
18
import java.util.List;
19
import java.util.Map;
20
import java.util.Optional;
21

22
/**
23
 * Extension for the tmforum-api mappers, to handle unknown properties to extend model-vos.
24
 */
25
@Slf4j
1✔
26
public abstract class BaseMapper {
×
27

28

29
        private static final String PROPERTIES_KEY = "properties";
30
        private static final String ITEMS_KEY = "items";
31
        private static final String TYPE_KEY = "type";
32
        private static final String ARRAY_TYPE = "array";
33
        private static final String OBJECT_TYPE = "object";
34

35
        @AfterMapping
36
        public void afterMappingToEntity(UnknownPreservingBase source, @MappingTarget Entity e) {
37
                if (source.getAtSchemaLocation() != null && source.getUnknownProperties() != null) {
×
38
                        source.getUnknownProperties().forEach(e::addAdditionalProperties);
×
39
                }
40
        }
×
41

42
        @AfterMapping
43
        public void afterMappingFromEntity(Entity source, @MappingTarget UnknownPreservingBase target) {
44
                if (source.getAtSchemaLocation() != null && source.getAdditionalProperties() != null) {
×
45
                        try {
46

47
                                JsonSchemaFactory jsonSchemaFactory = JsonSchemaFactory.getInstance(
×
48
                                                SpecVersion.VersionFlag.V202012,
49
                                                builder -> builder.schemaLoaders(sb -> {
×
50
                                                        sb.add(new ClasspathSchemaLoader());
×
51
                                                        sb.add(new UriSchemaLoader());
×
52
                                                })
×
53
                                );
54

55
                                SchemaValidatorsConfig.Builder validatorConfigBuilder = SchemaValidatorsConfig.builder();
×
56
                                SchemaValidatorsConfig schemaValidatorsConfig = validatorConfigBuilder.build();
×
57
                                JsonSchema schema = jsonSchemaFactory.getSchema(SchemaLocation.of(source.getAtSchemaLocation().toString()), schemaValidatorsConfig);
×
NEW
58
                                JsonNode rootSchemaNode = schema.getSchemaNode();
×
NEW
59
                                var propertiesNode = rootSchemaNode.get(PROPERTIES_KEY);
×
60

61
                                source.getAdditionalProperties()
×
62
                                                .forEach(additionalProperty -> {
×
NEW
63
                                                        JsonNode propertySchemaNode = propertiesNode != null ? propertiesNode.get(additionalProperty.getName()) : null;
×
NEW
64
                                                        Object coerced = coerceToSchema(additionalProperty.getValue(), propertySchemaNode);
×
NEW
65
                                                        target.setUnknownProperties(additionalProperty.getName(), coerced);
×
66
                                                });
×
67
                        } catch (Exception e) {
×
68
                                log.warn("Was not able to get the schema. Will not apply special handling.", e);
×
69
                                source.getAdditionalProperties()
×
70
                                                .forEach(additionalProperty -> target.setUnknownProperties(additionalProperty.getName(), additionalProperty.getValue()))
×
71
                                ;
72
                        }
×
73
                }
74
        }
×
75

76
        /**
77
         * Walk the value/schema trees in lock-step and undo the JSON-LD
78
         * single-element-array compaction that NGSI-LD brokers apply to nested
79
         * data: when the schema declares an attribute as {@code "type": "array"}
80
         * but what we got back is a scalar (or any non-list value), wrap it into
81
         * a one-element list. Recurses through both arrays ({@code items}) and
82
         * objects ({@code properties}) so that the fix applies at every depth —
83
         * not just the top-level attributes of the entity (which was the only
84
         * level handled before).
85
         */
86
        private static Object coerceToSchema(Object value, JsonNode schemaNode) {
87
                if (value == null || schemaNode == null) {
1!
NEW
88
                        return value;
×
89
                }
90
                String typeText = Optional.ofNullable(schemaNode.get(TYPE_KEY))
1✔
91
                                .filter(TextNode.class::isInstance)
1✔
92
                                .map(TextNode.class::cast)
1✔
93
                                .map(TextNode::textValue)
1✔
94
                                .orElse(null);
1✔
95

96
                if (ARRAY_TYPE.equals(typeText)) {
1✔
97
                        JsonNode itemsNode = schemaNode.get(ITEMS_KEY);
1✔
98
                        if (value instanceof List<?> list) {
1✔
99
                                List<Object> rebuilt = new ArrayList<>(list.size());
1✔
100
                                for (Object item : list) {
1✔
101
                                        rebuilt.add(coerceToSchema(item, itemsNode));
1✔
102
                                }
1✔
103
                                return rebuilt;
1✔
104
                        }
105
                        // schema says array, value is not a list → broker compacted a
106
                        // single-element array to a scalar; wrap it back.
107
                        return List.of(coerceToSchema(value, itemsNode));
1✔
108
                }
109

110
                if (OBJECT_TYPE.equals(typeText) && value instanceof Map<?, ?> map) {
1!
111
                        JsonNode nestedProperties = schemaNode.get(PROPERTIES_KEY);
1✔
112
                        if (nestedProperties == null) {
1!
NEW
113
                                return value;
×
114
                        }
115
                        Map<String, Object> rebuilt = new LinkedHashMap<>(map.size());
1✔
116
                        for (Map.Entry<?, ?> e : map.entrySet()) {
1✔
117
                                String key = String.valueOf(e.getKey());
1✔
118
                                rebuilt.put(key, coerceToSchema(e.getValue(), nestedProperties.get(key)));
1✔
119
                        }
1✔
120
                        return rebuilt;
1✔
121
                }
122

123
                return value;
1✔
124
        }
125
}
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

© 2026 Coveralls, Inc