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

wistefan / tmforum-api / #162

22 Sep 2025 12:54PM UTC coverage: 29.558%. First build
#162

push

web-flow
Merge 5f29228a7 into b23235f45

389 of 5758 branches covered (6.76%)

Branch coverage included in aggregate %.

178 of 211 new or added lines in 1 file covered. (84.36%)

2633 of 4466 relevant lines covered (58.96%)

0.59 hits per line

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

80.5
/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.ReservedWordHandler;
7
import io.github.wistefan.mapping.annotations.AttributeGetter;
8
import io.github.wistefan.mapping.annotations.AttributeType;
9
import io.github.wistefan.mapping.annotations.RelationshipObject;
10
import io.micronaut.context.annotation.Bean;
11
import lombok.RequiredArgsConstructor;
12

13
import lombok.extern.slf4j.Slf4j;
14
import org.fiware.tmforum.common.configuration.GeneralProperties;
15
import org.fiware.tmforum.common.domain.Entity;
16
import org.fiware.tmforum.common.exception.QueryException;
17

18
import javax.smartcardio.ATR;
19
import java.lang.annotation.Annotation;
20
import java.lang.reflect.Method;
21
import java.util.*;
22
import java.util.stream.Collectors;
23
import java.util.stream.Stream;
24

25
import static io.github.wistefan.mapping.JavaObjectMapper.getGetterMethodByName;
26

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

33
@Slf4j
1✔
34
@Bean
35
@RequiredArgsConstructor
1✔
36
public class QueryParser {
37

38
    private static final List<String> RESERVED_WORDS = List.of("id", "@id", "value", "@value", "type", "@type", "context", "@context");
1✔
39

40
    protected final GeneralProperties generalProperties;
41

42
    // Keys for the "well-known" fields
43
    public static final String OFFSET_KEY = "offset";
44
    public static final String LIMIT_KEY = "limit";
45
    public static final String FIELDS_KEY = "fields";
46
    public static final String SORT_KEY = "sort";
47

48
    public static final String NGSI_LD_AND = ";";
49

50
    // the "," in tm-forum values is an or
51
    public static final String TMFORUM_OR_VALUE = ",";
52

53
    // the ";" in tm-forum parameters is an or
54
    public static final String TMFORUM_OR_KEY = ";";
55
    public static final String TMFORUM_AND = "&";
56

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

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

84
    public QueryParams toNgsiLdQuery(Class<?> queryClass, String queryString) {
85
        queryString = removeWellKnownParameters(queryString);
1✔
86

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

104
        Stream<QueryPart> queryPartsStream = parameters
1✔
105
                .stream()
1✔
106
                .map(this::parseParameter);
1✔
107

108
        // collect the or values to single entries if they use the same key
109
        if (logicalOperator == LogicalOperator.OR) {
1✔
110
            Map<String, List<QueryPart>> collectedParts = queryPartsStream.collect(
1✔
111
                    Collectors.toMap(QueryPart::attribute, qp -> new ArrayList<>(List.of(qp)),
1✔
112
                            (qp1, qp2) -> {
113
                                qp1.addAll(qp2);
1✔
114
                                return qp1;
1✔
115
                            }));
116
            queryPartsStream = collectedParts.entrySet().stream()
1✔
117
                    .flatMap(entry -> combineParts(entry.getKey(), entry.getValue()).stream());
1✔
118
        }
119
        List<String> ids = new ArrayList<>();
1✔
120
        List<String> types = new ArrayList<>();
1✔
121
        // translate the attributes
122
        Stream<String> queryStrings = queryPartsStream.map(qp -> {
1✔
123
                    NgsiLdAttribute attribute = JavaObjectMapper.getNGSIAttributePath(
1✔
124
                            Arrays.asList(qp.attribute().split("\\.")),
1✔
125
                            queryClass);
126
                    if (attribute.path().isEmpty()) {
1✔
127
                        log.info("Attribute {} does not have a path in the base class. Get path to additional attributes.", qp.attribute());
1✔
128
                        attribute = getPathToAdditionalAttributes(qp);
1✔
129
                    }
130
                    if (attribute.path().size() == 1 && attribute.path().contains("id")) {
1✔
131
                        ids.add(qp.value());
1✔
132
                        return null;
1✔
133
                    }
134
                    if (attribute.path().size() == 1 && attribute.path().contains("type")) {
1!
NEW
135
                        types.add(qp.value());
×
NEW
136
                        return null;
×
137
                    }
138

139
                    List<String> cleanedPath = attribute.path()
1✔
140
                            .stream()
1✔
141
                            .map(ReservedWordHandler::escapeReservedWords)
1✔
142
                            .toList();
1✔
143
                    attribute = new NgsiLdAttribute(new ArrayList<>(cleanedPath), attribute.type());
1✔
144
                    return toQueryString(getQueryPart(attribute, qp, isRelationship(queryClass, attribute)), attribute.type());
1✔
145
                })
146
                .filter(Objects::nonNull);
1✔
147

148

149
        String ngsidOrKey = generalProperties.getNgsildOrQueryKey();
1✔
150
        String query = switch (logicalOperator) {
1✔
151
            case AND -> queryStrings.collect(Collectors.joining(NGSI_LD_AND));
1✔
152
            case OR -> queryStrings.collect(Collectors.joining(ngsidOrKey));
1✔
153
        };
154

155
        String idList = null;
1✔
156
        if (!ids.isEmpty()) {
1✔
157
            idList = String.join(",", ids);
1✔
158
        }
159
        String typeList = null;
1✔
160
        if (!types.isEmpty()) {
1!
NEW
161
            typeList = String.join(",", types);
×
162
        }
163
        if (query.isEmpty()) {
1✔
164
            query = null;
1✔
165
        }
166
        return new QueryParams(idList, typeList, query);
1✔
167
    }
168

169
    private NgsiLdAttribute getPathToAdditionalAttributes(QueryPart queryPart) {
170
        List<String> path = new ArrayList<>(Arrays.asList(queryPart.attribute().split("\\.")));
1✔
171
        if (isBoolean(queryPart.value())) {
1!
NEW
172
            return new NgsiLdAttribute(path, QueryAttributeType.BOOLEAN);
×
173
        }
174
        if (isNumber(queryPart.value())) {
1!
NEW
175
            return new NgsiLdAttribute(path, QueryAttributeType.NUMBER);
×
176
        }
177
        return new NgsiLdAttribute(path, QueryAttributeType.STRING);
1✔
178

179
    }
180

181
    private static boolean isNumber(String theValue) {
182
        try {
NEW
183
            Double.parseDouble(theValue);
×
NEW
184
            return true;
×
185
        } catch (NumberFormatException e) {
1✔
186
            return false;
1✔
187
        }
188
    }
189

190
    private static boolean isBoolean(String theValue) {
191
        if (theValue.equals("true") || theValue.equals("false")) {
1!
NEW
192
            return true;
×
193
        }
194
        return false;
1✔
195
    }
196

197
    private static boolean isRelationship(Class<?> queryClass, NgsiLdAttribute attribute) {
198
        log.warn("Is relationship? {}", attribute);
1✔
199

200
        Optional<Annotation> relevantAnnotation = getGetterMethodByName(queryClass, attribute.path().get(0))
1✔
201
                .flatMap(m -> Arrays.stream(m.getAnnotations()))
1✔
202
                .filter(AttributeGetter.class::isInstance)
1✔
203
                .filter(annotation -> (annotation instanceof AttributeGetter attributeGetter || annotation instanceof RelationshipObject))
1!
204
                .findFirst();
1✔
205
        if (relevantAnnotation.isEmpty()) {
1✔
206
            return false;
1✔
207
        }
208
        return relevantAnnotation.map(annotation -> {
1✔
209
            if (annotation instanceof AttributeGetter attributeGetter) {
1!
210
                return attributeGetter.value().equals(AttributeType.RELATIONSHIP)
1✔
211
                        || attributeGetter.value().equals(AttributeType.RELATIONSHIP_LIST);
1✔
212
            }
NEW
213
            if (annotation instanceof RelationshipObject) {
×
NEW
214
                return true;
×
215
            }
NEW
216
            return false;
×
217
        }).get();
1✔
218
    }
219

220
    private QueryPart getQueryPart(NgsiLdAttribute attribute, QueryPart qp, boolean isRel) {
221
        // The query part will depend on the type of query
222
        // if the query is to a relationship subproperties will be joined with .
223
        // if the query is to a property with structured values the path will be
224
        // added between brackets
225

226
        String attrPath;
227
        if (isRel) {
1✔
228
            attrPath = String.join(".", attribute.path());
1✔
229
            // remove .id, since it will be added in case of referenced entities
230
            if (attrPath.endsWith(".id")) {
1!
NEW
231
                attrPath = attrPath.substring(0, attrPath.length() - 3);
×
232
            }
233
        } else {
234
            String first = attribute.path().remove(0);
1✔
235
            attrPath = first + String.join("", attribute.path()
1✔
236
                    .stream()
1✔
237
                    .map(this::mapPathPart)
1✔
238
                    .toList());
1✔
239
        }
240

241
        return new QueryPart(
1✔
242
                attrPath,
243
                qp.operator(),
1✔
244
                qp.value());
1✔
245
    }
246

247
    private String mapPathPart(String part) {
248
        if (generalProperties.getUseDotSeperator()) {
1✔
249
            return "." + part;
1✔
250
        } else {
251
            return "[" + part + "]";
1✔
252
        }
253
    }
254

255
    private String encodeValue(String value, QueryAttributeType type) {
256
        value = switch (type) {
1!
257
            case STRING -> encodeStringValue(value);
1✔
NEW
258
            case BOOLEAN -> value;
×
259
            case NUMBER -> value;
1✔
260
        };
261
        return value;
1✔
262
    }
263

264
    private String encodeStringValue(String value) {
265
        String ngsildOrValue = generalProperties.getNgsildOrQueryValue();
1✔
266
        if (value.contains(ngsildOrValue)) {
1!
267
            // remove the beginning ( and ending )
268
            // String noBraces = value.substring(1, value.length() - 1);
NEW
269
            String format = "(%s)";
×
270

NEW
271
            if (!generalProperties.getEncloseQuery()) {
×
NEW
272
                format = "%s";
×
273
            }
274

NEW
275
            return String.format(format, Arrays.stream(value.split(String.format("\\%s", ngsildOrValue)))
×
NEW
276
                    .map(v -> String.format("\"%s\"", v))
×
NEW
277
                    .collect(Collectors.joining(ngsildOrValue)));
×
278

279
        } else if (value.contains(NGSI_LD_AND)) {
1!
280
            // remove the beginning ( and ending )
281
            //String noBraces = value.substring(1, value.length() - 1);
NEW
282
            return String.format("(%s)", Arrays.stream(value.split(String.format("\\%s", NGSI_LD_AND)))
×
NEW
283
                    .map(v -> String.format("\"%s\"", v))
×
NEW
284
                    .collect(Collectors.joining(NGSI_LD_AND)));
×
285
        } else {
286
            return String.format("\"%s\"", value);
1✔
287
        }
288
    }
289

290
    private List<QueryPart> combineParts(String attribute, List<QueryPart> uncombinedParts) {
291
        Map<String, List<QueryPart>> collectedParts = uncombinedParts.stream()
1✔
292
                .collect(
1✔
293
                        Collectors.toMap(QueryPart::operator, qp -> new ArrayList<>(List.of(qp)),
1✔
294
                                (qp1, qp2) -> {
295
                                    qp1.addAll(qp2);
1✔
296
                                    return qp1;
1✔
297
                                }));
298
        return collectedParts
1✔
299
                .entrySet()
1✔
300
                .stream()
1✔
301
                .map(entry -> {
1✔
302
                    String value = entry.getValue()
1✔
303
                            .stream()
1✔
304
                            .map(QueryPart::value)
1✔
305
                            .collect(Collectors.joining(TMFORUM_OR_VALUE));
1✔
306

307
                    return new QueryPart(attribute, entry.getKey(), value);
1✔
308
                })
309
                .collect(Collectors.toList());
1✔
310
    }
311

312
    private String toQueryString(QueryPart queryPart, QueryAttributeType queryAttributeType) {
313

314
        if (queryPart.value().contains(TMFORUM_OR_VALUE)) {
1✔
315
            String theQuery = "";
1✔
316
            List<String> encodedValues = new ArrayList<>(Arrays.stream(queryPart.value().split(TMFORUM_OR_VALUE))
1✔
317
                    .map(v -> encodeValue(v, queryAttributeType))
1✔
318
                    .toList());
1✔
319

320
            if (generalProperties.getIncludeAttributeInList()) {
1✔
321
                theQuery = encodedValues
1✔
322
                        .stream()
1✔
323
                        .map(v -> String.format("%s%s%s", queryPart.attribute(), queryPart.operator(), v))
1✔
324
                        .collect(Collectors.joining(generalProperties.getNgsildOrQueryValue()));
1✔
325
                if (generalProperties.getEncloseQuery()) {
1!
326
                    return "(" + theQuery + ")";
1✔
327
                }
328
            } else {
329
                if (generalProperties.getEncloseQuery()) {
1✔
330
                    return String.format("%s%s(%s)", queryPart.attribute(), queryPart.operator(), encodedValues
1✔
331
                            .stream()
1✔
332
                            .collect(Collectors.joining(generalProperties.getNgsildOrQueryValue())));
1✔
333
                } else {
334
                    return String.format("%s%s%s", queryPart.attribute(), queryPart.operator(), encodedValues
1✔
335
                            .stream()
1✔
336
                            .collect(Collectors.joining(generalProperties.getNgsildOrQueryValue())));
1✔
337
                }
338
            }
339

NEW
340
            return theQuery;
×
341
        }
342

343
        return String.format("%s%s%s", queryPart.attribute(), queryPart.operator(), encodeValue(queryPart.value(), queryAttributeType));
1✔
344
    }
345

346
    private QueryPart paramsToQueryPart(String parameter, Operator operator) {
347
        String[] parameterParts = parameter.split(operator.getTmForumOperator().operator());
1✔
348
        if (parameterParts.length != 2) {
1!
NEW
349
            throw new QueryException(String.format("%s is not a valid %s parameter.",
×
350
                    parameter,
NEW
351
                    operator.getTmForumOperator().operator()));
×
352
        }
353
        return new QueryPart(
1✔
354
                parameterParts[0],
355
                operator.getNgsiLdOperator(),
1✔
356
                parameterParts[1]);
357
    }
358

359
    private QueryPart getQueryFromEquals(String parameter) {
360

361
        // equals could also contain a textual operator, f.e. key.gt=value -> key>value
362
        Optional<Operator> containedOperator = getOperator(parameter);
1✔
363
        if (containedOperator.isEmpty()) {
1✔
364
            // its a plain equals
365
            return paramsToQueryPart(parameter, Operator.EQUALS);
1✔
366
        }
367

368
        QueryPart uncleanedQueryPart = paramsToQueryPart(parameter, Operator.EQUALS);
1✔
369
        String uncleanedAttribute = uncleanedQueryPart.attribute();
1✔
370
        String cleanAttribute = uncleanedAttribute.substring(0,
1✔
371
                uncleanedAttribute.length() - containedOperator.get().getTmForumOperator().textRepresentation()
1✔
372
                        .length());
1✔
373
        return new QueryPart(cleanAttribute, containedOperator.get().getNgsiLdOperator(), uncleanedQueryPart.value());
1✔
374

375
    }
376

377
    private QueryPart parseParameter(String parameter) {
378

379
        Operator operator = getOperatorFromParam(parameter);
1✔
380
        return switch (operator) {
1!
381
            case GREATER_THAN -> paramsToQueryPart(parameter, GREATER_THAN);
1✔
382
            case GREATER_THAN_EQUALS -> paramsToQueryPart(parameter, GREATER_THAN_EQUALS);
1✔
383
            case LESS_THAN_EQUALS -> paramsToQueryPart(parameter, LESS_THAN_EQUALS);
1✔
384
            case LESS_THAN -> paramsToQueryPart(parameter, LESS_THAN);
1✔
NEW
385
            case REGEX -> paramsToQueryPart(parameter, REGEX);
×
386
            case EQUALS -> getQueryFromEquals(parameter);
1✔
387
        };
388

389
    }
390

391
    private static Operator getOperatorFromParam(String parameter) {
392
        if (parameter.contains(GREATER_THAN_EQUALS.getTmForumOperator().operator())) {
1✔
393
            return GREATER_THAN_EQUALS;
1✔
394
        }
395
        if (parameter.contains(Operator.LESS_THAN_EQUALS.getTmForumOperator().operator())) {
1✔
396
            return Operator.LESS_THAN_EQUALS;
1✔
397
        }
398
        if (parameter.contains(Operator.REGEX.getTmForumOperator().operator())) {
1!
NEW
399
            return Operator.REGEX;
×
400
        }
401
        if (parameter.contains(GREATER_THAN.getTmForumOperator().operator())) {
1✔
402
            return GREATER_THAN;
1✔
403
        }
404
        if (parameter.contains(LESS_THAN.getTmForumOperator().operator())) {
1✔
405
            return LESS_THAN;
1✔
406
        }
407
        return Operator.EQUALS;
1✔
408
    }
409

410
    private static Optional<Operator> getOperator(String partToParse) {
411
        String[] parts = partToParse.split(Operator.EQUALS.getTmForumOperator().operator());
1✔
412
        return Arrays.stream(Operator.values())
1✔
413
                .filter(operator -> {
1✔
414
                    TMForumOperator tmForumOperator = operator.getTmForumOperator();
1✔
415
                    if (parts[0].endsWith(tmForumOperator.textRepresentation())) {
1✔
416
                        return true;
1✔
417
                    }
418
                    return false;
1✔
419
                })
420
                .findAny();
1✔
421
    }
422
}
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