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

wistefan / tmforum-api / #61

18 Oct 2023 12:35PM UTC coverage: 67.631% (+0.1%) from 67.488%
#61

push

web-flow
Configurable NGSI-LD Query format (#32)

* Build different NGSI queries depending if the attr is relationship or property

* Add tests to validate query parser with relationships

* Add missing testing class

* Fix bug removing well known attributes in query parser using fixed-size list

* Use scorpio OR queries

* Fix the type of product spec bundle references

* Update the type of product specs relationships

* Fix errors mapping categories and product specs as references

* Fix config issues with ProductOfferingTerm

* Allow to configure NGSI-LD query options

* Add default NGSI-LD query configuration

* fix config

* fix test call

* fix it

---------

Co-authored-by: Stefan Wiedemann <wistefan@googlemail.com>

65 of 65 new or added lines in 47 files covered. (100.0%)

2804 of 4146 relevant lines covered (67.63%)

0.68 hits per line

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

90.54
/common/src/main/java/org/fiware/tmforum/common/querying/QueryParser.java
1
package org.fiware.tmforum.common.querying;
2

3
import io.github.wistefan.mapping.JavaObjectMapper;
4
import io.github.wistefan.mapping.NgsiLdAttribute;
5
import io.github.wistefan.mapping.QueryAttributeType;
6
import io.github.wistefan.mapping.annotations.AttributeGetter;
7
import io.github.wistefan.mapping.annotations.AttributeType;
8
import io.micronaut.context.annotation.Bean;
9
import lombok.RequiredArgsConstructor;
10

11
import org.fiware.tmforum.common.configuration.GeneralProperties;
12
import org.fiware.tmforum.common.exception.QueryException;
13

14
import java.util.ArrayList;
15
import java.util.Arrays;
16
import java.util.LinkedList;
17
import java.util.List;
18
import java.util.Map;
19
import java.util.Optional;
20
import java.util.stream.Collectors;
21
import java.util.stream.Stream;
22

23
import static io.github.wistefan.mapping.JavaObjectMapper.getGetterMethodByName;
24

25
import static org.fiware.tmforum.common.querying.Operator.GREATER_THAN;
26
import static org.fiware.tmforum.common.querying.Operator.GREATER_THAN_EQUALS;
27
import static org.fiware.tmforum.common.querying.Operator.LESS_THAN;
28
import static org.fiware.tmforum.common.querying.Operator.LESS_THAN_EQUALS;
29
import static org.fiware.tmforum.common.querying.Operator.REGEX;
30

31
@Bean
32
@RequiredArgsConstructor
1✔
33
public class QueryParser {
34

35
        protected final GeneralProperties generalProperties;
36

37
        // Keys for the "well-known" fields
38
        public static final String OFFSET_KEY = "offset";
39
        public static final String LIMIT_KEY = "limit";
40
        public static final String FIELDS_KEY = "fields";
41
        public static final String SORT_KEY = "sort";
42

43
        public static final String NGSI_LD_AND = ";";
44

45
        // the "," in tm-forum values is an or
46
        public static final String TMFORUM_OR_VALUE = ",";
47

48
        // the ";" in tm-forum parameters is an or
49
        public static final String TMFORUM_OR_KEY = ";";
50
        public static final String TMFORUM_AND = "&";
51

52
        public static boolean hasFilter(Map<String, List<String>> values) {
53
                //remove the "non-filtering" keys
54
                values.remove(OFFSET_KEY);
×
55
                values.remove(LIMIT_KEY);
×
56
                values.remove(FIELDS_KEY);
×
57
                values.remove(SORT_KEY);
×
58
                // if something is left, we have filter
59
                return !values.isEmpty();
×
60
        }
61

62
        private static String removeWellKnownParameters(String queryString) {
63
                // Using linked list as the list returned by asList method is fixed-size
64
                // so the remove method raises a non implemented exception
65
                List<String> parameters = new LinkedList<>(Arrays.asList(queryString.split(TMFORUM_AND)));
1✔
66
                List<String> wellKnownParams = parameters
1✔
67
                                .stream()
1✔
68
                                .filter(p -> p.startsWith(LIMIT_KEY)
1✔
69
                                                || p.startsWith(FIELDS_KEY)
1✔
70
                                                || p.startsWith(OFFSET_KEY)
1✔
71
                                                || p.startsWith(SORT_KEY)
1✔
72
                                )
73
                                .toList();
1✔
74
                // not part of the query
75
                parameters.removeAll(wellKnownParams);
1✔
76
                return String.join(TMFORUM_AND, parameters);
1✔
77
        }
78

79
        public String toNgsiLdQuery(Class<?> queryClass, String queryString) {
80
                queryString = removeWellKnownParameters(queryString);
1✔
81

82
                List<String> parameters;
83
                LogicalOperator logicalOperator = LogicalOperator.AND;
1✔
84
                // tmforum does not define queries combining AND and OR
85
                if(queryString.contains(TMFORUM_AND) && queryString.contains(TMFORUM_OR_KEY)) {
1✔
86
                        throw new QueryException("Combining AND(&) and OR(;) on query level is not supported by the TMForum API.");
×
87
                }
88
                if (queryString.contains(TMFORUM_AND)) {
1✔
89
                        parameters = Arrays.asList(queryString.split(TMFORUM_AND));
1✔
90
                        logicalOperator = LogicalOperator.AND;
1✔
91
                } else if (queryString.contains(TMFORUM_OR_KEY)) {
1✔
92
                        parameters = Arrays.asList(queryString.split(TMFORUM_OR_KEY));
1✔
93
                        logicalOperator = LogicalOperator.OR;
1✔
94
                } else {
95
                        //query is just a single parameter query
96
                        parameters = List.of(queryString);
1✔
97
                }
98

99
                Stream<QueryPart> queryPartsStream = parameters
1✔
100
                                .stream()
1✔
101
                                .map(this::parseParameter);
1✔
102

103
                // collect the or values to single entries if they use the same key
104
                if (logicalOperator == LogicalOperator.OR) {
1✔
105
                        Map<String, List<QueryPart>> collectedParts = queryPartsStream.collect(
1✔
106
                                        Collectors.toMap(QueryPart::attribute, qp -> new ArrayList<QueryPart>(List.of(qp)),
1✔
107
                                                        (qp1, qp2) -> {
108
                                                                qp1.addAll(qp2);
1✔
109
                                                                return qp1;
1✔
110
                                                        }));
111
                        queryPartsStream = collectedParts.entrySet().stream()
1✔
112
                                        .flatMap(entry -> combineParts(entry.getKey(), entry.getValue()).stream());
1✔
113
                }
114

115
                // translate the attributes
116
                Stream<String> queryStrings = queryPartsStream.map(qp -> {
1✔
117
                                        NgsiLdAttribute attribute = JavaObjectMapper.getNGSIAttributePath(
1✔
118
                                                        Arrays.asList(qp.attribute().split("\\.")),
1✔
119
                                                        queryClass);
120

121
                                        return getQueryPart(attribute, qp, isRelationship(queryClass, attribute));
1✔
122
                                })
123
                                .map(QueryParser::toQueryString);
1✔
124

125
                String ngsidOrKey = generalProperties.getNgsildOrQueryKey();
1✔
126
                return switch (logicalOperator) {
1✔
127
                        case AND -> queryStrings.collect(Collectors.joining(NGSI_LD_AND));
1✔
128
                        case OR -> queryStrings.collect(Collectors.joining(ngsidOrKey));
1✔
129
                };
130
        }
131

132
        private static boolean isRelationship(Class<?> queryClass, NgsiLdAttribute attribute) {
133
                Optional<AttributeGetter> getter = getGetterMethodByName(queryClass, attribute.path().get(0))
1✔
134
                        .map(m -> {
1✔
135
                                return Arrays.stream(m.getAnnotations())
1✔
136
                                        .filter(AttributeGetter.class::isInstance)
1✔
137
                                        .map(AttributeGetter.class::cast)
1✔
138
                                        .findFirst();
1✔
139
                        })
140
                        .filter(a -> a.isPresent())
1✔
141
                        .map(a -> a.get())
1✔
142
                        .findFirst();
1✔
143

144
                return getter.isPresent() &&
1✔
145
                        (getter.get().value().equals(AttributeType.RELATIONSHIP) ||
1✔
146
                        getter.get().value().equals(AttributeType.RELATIONSHIP_LIST));
1✔
147
        }
148

149
        private QueryPart getQueryPart(NgsiLdAttribute attribute, QueryPart qp, boolean isRel) {
150
                // The query part will depend on the type of query
151
                // if the query is to a relationship subproperties will be joined with .
152
                // if the query is to a property with structured values the path will be
153
                // added between brackets
154

155
                String attrPath;
156
                if (isRel) {
1✔
157
                        attrPath = String.join(".", attribute.path());
1✔
158
                } else {
159
                        String first = attribute.path().remove(0);
1✔
160
                        attrPath = first + String.join("", attribute.path()
1✔
161
                                .stream()
1✔
162
                                .map(a -> "[" + a + "]")
1✔
163
                                .toList());
1✔
164
                }
165

166
                return new QueryPart(
1✔
167
                                attrPath,
168
                                qp.operator(),
1✔
169
                                encodeValue(qp.value(), attribute.type()));
1✔
170
        }
171

172
        private String encodeValue(String value, QueryAttributeType type) {
173
                return switch (type) {
1✔
174
                        case STRING -> encodeStringValue(value);
1✔
175
                        case BOOLEAN -> value;
×
176
                        case NUMBER -> value;
1✔
177
                };
178
        }
179

180
        private String encodeStringValue(String value) {
181
                String ngsildOrValue = generalProperties.getNgsildOrQueryValue();
1✔
182
                if (value.contains(ngsildOrValue)) {
1✔
183
                        // remove the beginning ( and ending )
184
                        // String noBraces = value.substring(1, value.length() - 1);
185
                        String format = "(%s)";
1✔
186

187
                        if (!generalProperties.getEncloseQuery()) {
1✔
188
                                format = "%s";
1✔
189
                        }
190

191
                        return String.format(format, Arrays.stream(value.split(String.format("\\%s", ngsildOrValue)))
1✔
192
                                        .map(v -> String.format("\"%s\"", v))
1✔
193
                                        .collect(Collectors.joining(ngsildOrValue)));
1✔
194

195
                } else if (value.contains(NGSI_LD_AND)) {
1✔
196
                        // remove the beginning ( and ending )
197
                        //String noBraces = value.substring(1, value.length() - 1);
198
                        return String.format("(%s)", Arrays.stream(value.split(String.format("\\%s", NGSI_LD_AND)))
×
199
                                        .map(v -> String.format("\"%s\"", v))
×
200
                                        .collect(Collectors.joining(NGSI_LD_AND)));
×
201
                } else {
202
                        return String.format("\"%s\"", value);
1✔
203
                }
204
        }
205

206
        private List<QueryPart> combineParts(String attribute, List<QueryPart> uncombinedParts) {
207
                String ngsildOrValue = generalProperties.getNgsildOrQueryValue();
1✔
208
                Map<String, List<QueryPart>> collectedParts = uncombinedParts.stream()
1✔
209
                                .collect(
1✔
210
                                                Collectors.toMap(QueryPart::operator, qp -> new ArrayList<>(List.of(qp)),
1✔
211
                                                                (qp1, qp2) -> {
212
                                                                        qp1.addAll(qp2);
1✔
213
                                                                        return qp1;
1✔
214
                                                                }));
215
                return collectedParts
1✔
216
                                .entrySet()
1✔
217
                                .stream()
1✔
218
                                .map(entry -> {
1✔
219
                                        String value = entry.getValue()
1✔
220
                                                        .stream()
1✔
221
                                                        .map(QueryPart::value)
1✔
222
                                                        .collect(Collectors.joining(ngsildOrValue));
1✔
223

224
                                        //if (entry.getValue().size() > 1) {
225
                                        //        value = String.format("(%s)", value);
226
                                        //}
227

228
                                        return new QueryPart(attribute, entry.getKey(), value);
1✔
229
                                })
230
                                .collect(Collectors.toList());
1✔
231
        }
232

233
        private static String toQueryString(QueryPart queryPart) {
234
                return String.format("%s%s%s", queryPart.attribute(), queryPart.operator(), queryPart.value());
1✔
235
        }
236

237
        private QueryPart paramsToQueryPart(String parameter, Operator operator) {
238
                String ngsildOrValue = generalProperties.getNgsildOrQueryValue();
1✔
239
                String[] parameterParts = parameter.split(operator.getTmForumOperator().operator());
1✔
240
                if (parameterParts.length != 2) {
1✔
241
                        throw new QueryException(String.format("%s is not a valid %s parameter.",
×
242
                                        parameter,
243
                                        operator.getTmForumOperator().operator()));
×
244
                }
245
                String value = parameterParts[1];
1✔
246
                if (value.contains(TMFORUM_OR_VALUE)) {
1✔
247
                        value = String.format("%s", value.replace(TMFORUM_OR_VALUE, ngsildOrValue));
1✔
248
                }
249

250
                return new QueryPart(
1✔
251
                                parameterParts[0],
252
                                operator.getNgsiLdOperator(),
1✔
253
                                value);
254
        }
255

256
        private QueryPart getQueryFromEquals(String parameter) {
257

258
                // equals could also contain a textual operator, f.e. key.gt=value -> key>value
259
                Optional<Operator> containedOperator = getOperator(parameter);
1✔
260
                if (containedOperator.isEmpty()) {
1✔
261
                        // its a plain equals
262
                        return paramsToQueryPart(parameter, Operator.EQUALS);
1✔
263
                }
264

265
                QueryPart uncleanedQueryPart = paramsToQueryPart(parameter, Operator.EQUALS);
1✔
266
                String uncleanedAttribute = uncleanedQueryPart.attribute();
1✔
267
                String cleanAttribute = uncleanedAttribute.substring(0,
1✔
268
                                uncleanedAttribute.length() - containedOperator.get().getTmForumOperator().textRepresentation()
1✔
269
                                                .length());
1✔
270
                return new QueryPart(cleanAttribute, containedOperator.get().getNgsiLdOperator(), uncleanedQueryPart.value());
1✔
271

272
        }
273

274
        private QueryPart parseParameter(String parameter) {
275

276
                Operator operator = getOperatorFromParam(parameter);
1✔
277
                return switch (operator) {
1✔
278
                        case GREATER_THAN -> paramsToQueryPart(parameter, GREATER_THAN);
1✔
279
                        case GREATER_THAN_EQUALS -> paramsToQueryPart(parameter, GREATER_THAN_EQUALS);
1✔
280
                        case LESS_THAN_EQUALS -> paramsToQueryPart(parameter, LESS_THAN_EQUALS);
1✔
281
                        case LESS_THAN -> paramsToQueryPart(parameter, LESS_THAN);
1✔
282
                        case REGEX -> paramsToQueryPart(parameter, REGEX);
×
283
                        case EQUALS -> getQueryFromEquals(parameter);
1✔
284
                };
285

286
        }
287

288
        private static Operator getOperatorFromParam(String parameter) {
289
                if (parameter.contains(GREATER_THAN_EQUALS.getTmForumOperator().operator())) {
1✔
290
                        return GREATER_THAN_EQUALS;
1✔
291
                }
292
                if (parameter.contains(Operator.LESS_THAN_EQUALS.getTmForumOperator().operator())) {
1✔
293
                        return Operator.LESS_THAN_EQUALS;
1✔
294
                }
295
                if (parameter.contains(Operator.REGEX.getTmForumOperator().operator())) {
1✔
296
                        return Operator.REGEX;
×
297
                }
298
                if (parameter.contains(GREATER_THAN.getTmForumOperator().operator())) {
1✔
299
                        return GREATER_THAN;
1✔
300
                }
301
                if (parameter.contains(LESS_THAN.getTmForumOperator().operator())) {
1✔
302
                        return LESS_THAN;
1✔
303
                }
304
                return Operator.EQUALS;
1✔
305
        }
306

307
        private static Optional<Operator> getOperator(String partToParse) {
308
                String[] parts = partToParse.split(Operator.EQUALS.getTmForumOperator().operator());
1✔
309
                return Arrays.stream(Operator.values())
1✔
310
                                .filter(operator -> {
1✔
311
                                        TMForumOperator tmForumOperator = operator.getTmForumOperator();
1✔
312
                                        if (parts[0].endsWith(tmForumOperator.textRepresentation())) {
1✔
313
                                                return true;
1✔
314
                                        }
315
                                        return false;
1✔
316
                                })
317
                                .findAny();
1✔
318
        }
319
}
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