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

wistefan / tmforum-api / #49

29 Sep 2023 06:20AM UTC coverage: 67.488% (-4.3%) from 71.815%
#49

push

web-flow
Notifications (#23)

* Squashed commits

* Added cache invalidation for entity deletion

* Updated error message

618 of 618 new or added lines in 86 files covered. (100.0%)

2794 of 4140 relevant lines covered (67.49%)

0.67 hits per line

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

88.97
/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

9
import org.fiware.tmforum.common.exception.QueryException;
10

11
import java.util.ArrayList;
12
import java.util.Arrays;
13
import java.util.LinkedList;
14
import java.util.List;
15
import java.util.Map;
16
import java.util.Optional;
17
import java.util.stream.Collectors;
18
import java.util.stream.Stream;
19

20
import static io.github.wistefan.mapping.JavaObjectMapper.getGetterMethodByName;
21

22
import static org.fiware.tmforum.common.querying.Operator.GREATER_THAN;
23
import static org.fiware.tmforum.common.querying.Operator.GREATER_THAN_EQUALS;
24
import static org.fiware.tmforum.common.querying.Operator.LESS_THAN;
25
import static org.fiware.tmforum.common.querying.Operator.LESS_THAN_EQUALS;
26
import static org.fiware.tmforum.common.querying.Operator.REGEX;
27

28
public class QueryParser {
×
29

30
        // Keys for the "well-known" fields
31
        public static final String OFFSET_KEY = "offset";
32
        public static final String LIMIT_KEY = "limit";
33
        public static final String FIELDS_KEY = "fields";
34
        public static final String SORT_KEY = "sort";
35

36
        public static final String NGSI_LD_OR = "|";
37
        public static final String NGSI_LD_AND = ";";
38
        // the "," in tm-forum values is an or
39
        public static final String TMFORUM_OR_VALUE = ",";
40
        // the ";" in tm-forum parameters is an or
41
        public static final String TMFORUM_OR_KEY = ";";
42
        public static final String TMFORUM_AND = "&";
43

44
        public static boolean hasFilter(Map<String, List<String>> values) {
45
                //remove the "non-filtering" keys
46
                values.remove(OFFSET_KEY);
×
47
                values.remove(LIMIT_KEY);
×
48
                values.remove(FIELDS_KEY);
×
49
                values.remove(SORT_KEY);
×
50
                // if something is left, we have filter
51
                return !values.isEmpty();
×
52
        }
53

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

71
        public static String toNgsiLdQuery(Class<?> queryClass, String queryString) {
72

73
                queryString = removeWellKnownParameters(queryString);
1✔
74

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

92
                Stream<QueryPart> queryPartsStream = parameters
1✔
93
                                .stream()
1✔
94
                                .map(QueryParser::parseParameter);
1✔
95

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

108
                // translate the attributes
109
                Stream<String> queryStrings = queryPartsStream.map(qp -> {
1✔
110
                                        NgsiLdAttribute attribute = JavaObjectMapper.getNGSIAttributePath(
1✔
111
                                                        Arrays.asList(qp.attribute().split("\\.")),
1✔
112
                                                        queryClass);
113

114
                                        return getQueryPart(attribute, qp, isRelationship(queryClass, attribute));
1✔
115
                                })
116
                                .map(QueryParser::toQueryString);
1✔
117

118
                return switch (logicalOperator) {
1✔
119
                        case AND -> queryStrings.collect(Collectors.joining(NGSI_LD_AND));
1✔
120
                        case OR -> queryStrings.collect(Collectors.joining(NGSI_LD_OR));
1✔
121
                };
122
        }
123

124
        private static boolean isRelationship(Class<?> queryClass, NgsiLdAttribute attribute) {
125
                Optional<AttributeGetter> getter = getGetterMethodByName(queryClass, attribute.path().get(0))
1✔
126
                        .map(m -> {
1✔
127
                                return Arrays.stream(m.getAnnotations())
1✔
128
                                        .filter(AttributeGetter.class::isInstance)
1✔
129
                                        .map(AttributeGetter.class::cast)
1✔
130
                                        .findFirst();
1✔
131
                        })
132
                        .filter(a -> a.isPresent())
1✔
133
                        .map(a -> a.get())
1✔
134
                        .findFirst();
1✔
135

136
                return getter.isPresent() &&
1✔
137
                        (getter.get().value().equals(AttributeType.RELATIONSHIP) ||
1✔
138
                        getter.get().value().equals(AttributeType.RELATIONSHIP_LIST));
1✔
139
        }
140

141
        private static QueryPart getQueryPart(NgsiLdAttribute attribute, QueryPart qp, boolean isRel) {
142
                // The query part will depend on the type of query
143
                // if the query is to a relationship subproperties will be joined with .
144
                // if the query is to a property with structured values the path will be
145
                // added between brackets
146

147
                String attrPath;
148
                if (isRel) {
1✔
149
                        attrPath = String.join(".", attribute.path());
1✔
150
                } else {
151
                        String first = attribute.path().remove(0);
1✔
152
                        attrPath = first + String.join("", attribute.path()
1✔
153
                                .stream()
1✔
154
                                .map(a -> "[" + a + "]")
1✔
155
                                .toList());
1✔
156
                }
157

158
                return new QueryPart(
1✔
159
                                attrPath,
160
                                qp.operator(),
1✔
161
                                encodeValue(qp.value(), attribute.type()));
1✔
162
        }
163

164
        private static String encodeValue(String value, QueryAttributeType type) {
165
                return switch (type) {
1✔
166
                        case STRING -> encodeStringValue(value);
1✔
167
                        case BOOLEAN -> value;
×
168
                        case NUMBER -> value;
1✔
169
                };
170
        }
171

172
        private static String encodeStringValue(String value) {
173
                if (value.contains(NGSI_LD_OR)) {
1✔
174
                        // remove the beginning ( and ending )
175
                        String noBraces = value.substring(1, value.length() - 1);
1✔
176
                        return String.format("(%s)", Arrays.stream(noBraces.split(String.format("\\%s", NGSI_LD_OR)))
1✔
177
                                        .map(v -> String.format("\"%s\"", v))
1✔
178
                                        .collect(Collectors.joining(NGSI_LD_OR)));
1✔
179
                } else if (value.contains(NGSI_LD_AND)) {
1✔
180
                        // remove the beginning ( and ending )
181
                        String noBraces = value.substring(1, value.length() - 1);
×
182
                        return String.format("(%s)", Arrays.stream(noBraces.split(String.format("\\%s", NGSI_LD_AND)))
×
183
                                        .map(v -> String.format("\"%s\"", v))
×
184
                                        .collect(Collectors.joining(NGSI_LD_AND)));
×
185
                } else {
186
                        return String.format("\"%s\"", value);
1✔
187
                }
188
        }
189

190
        private static List<QueryPart> combineParts(String attribute, List<QueryPart> uncombinedParts) {
191
                Map<String, List<QueryPart>> collectedParts = uncombinedParts.stream()
1✔
192
                                .collect(
1✔
193
                                                Collectors.toMap(QueryPart::operator, qp -> new ArrayList<>(List.of(qp)),
1✔
194
                                                                (qp1, qp2) -> {
195
                                                                        qp1.addAll(qp2);
1✔
196
                                                                        return qp1;
1✔
197
                                                                }));
198
                return collectedParts
1✔
199
                                .entrySet()
1✔
200
                                .stream()
1✔
201
                                .map(entry -> {
1✔
202
                                        String value = entry.getValue()
1✔
203
                                                        .stream()
1✔
204
                                                        .map(QueryPart::value)
1✔
205
                                                        .collect(Collectors.joining(NGSI_LD_OR));
1✔
206
                                        if (entry.getValue().size() > 1) {
1✔
207
                                                value = String.format("(%s)", value);
1✔
208
                                        }
209
                                        return new QueryPart(attribute, entry.getKey(), value);
1✔
210
                                })
211
                                .collect(Collectors.toList());
1✔
212
        }
213

214
        private static String toQueryString(QueryPart queryPart) {
215
                return String.format("%s%s%s", queryPart.attribute(), queryPart.operator(), queryPart.value());
1✔
216
        }
217

218
        private static QueryPart paramsToQueryPart(String parameter, Operator operator) {
219
                String[] parameterParts = parameter.split(operator.getTmForumOperator().operator());
1✔
220
                if (parameterParts.length != 2) {
1✔
221
                        throw new QueryException(String.format("%s is not a valid %s parameter.",
×
222
                                        parameter,
223
                                        operator.getTmForumOperator().operator()));
×
224
                }
225
                String value = parameterParts[1];
1✔
226
                if (value.contains(TMFORUM_OR_VALUE)) {
1✔
227
                        value = String.format("(%s)", value.replace(TMFORUM_OR_VALUE, NGSI_LD_OR));
1✔
228
                }
229

230
                return new QueryPart(
1✔
231
                                parameterParts[0],
232
                                operator.getNgsiLdOperator(),
1✔
233
                                value);
234
        }
235

236
        private static QueryPart getQueryFromEquals(String parameter) {
237

238
                // equals could also contain a textual operator, f.e. key.gt=value -> key>value
239
                Optional<Operator> containedOperator = getOperator(parameter);
1✔
240
                if (containedOperator.isEmpty()) {
1✔
241
                        // its a plain equals
242
                        return paramsToQueryPart(parameter, Operator.EQUALS);
1✔
243
                }
244

245
                QueryPart uncleanedQueryPart = paramsToQueryPart(parameter, Operator.EQUALS);
1✔
246
                String uncleanedAttribute = uncleanedQueryPart.attribute();
1✔
247
                String cleanAttribute = uncleanedAttribute.substring(0,
1✔
248
                                uncleanedAttribute.length() - containedOperator.get().getTmForumOperator().textRepresentation()
1✔
249
                                                .length());
1✔
250
                return new QueryPart(cleanAttribute, containedOperator.get().getNgsiLdOperator(), uncleanedQueryPart.value());
1✔
251

252
        }
253

254
        private static QueryPart parseParameter(String parameter) {
255

256
                Operator operator = getOperatorFromParam(parameter);
1✔
257
                return switch (operator) {
1✔
258
                        case GREATER_THAN -> paramsToQueryPart(parameter, GREATER_THAN);
1✔
259
                        case GREATER_THAN_EQUALS -> paramsToQueryPart(parameter, GREATER_THAN_EQUALS);
1✔
260
                        case LESS_THAN_EQUALS -> paramsToQueryPart(parameter, LESS_THAN_EQUALS);
1✔
261
                        case LESS_THAN -> paramsToQueryPart(parameter, LESS_THAN);
1✔
262
                        case REGEX -> paramsToQueryPart(parameter, REGEX);
×
263
                        case EQUALS -> getQueryFromEquals(parameter);
1✔
264
                };
265

266
        }
267

268
        private static Operator getOperatorFromParam(String parameter) {
269
                if (parameter.contains(GREATER_THAN_EQUALS.getTmForumOperator().operator())) {
1✔
270
                        return GREATER_THAN_EQUALS;
1✔
271
                }
272
                if (parameter.contains(Operator.LESS_THAN_EQUALS.getTmForumOperator().operator())) {
1✔
273
                        return Operator.LESS_THAN_EQUALS;
1✔
274
                }
275
                if (parameter.contains(Operator.REGEX.getTmForumOperator().operator())) {
1✔
276
                        return Operator.REGEX;
×
277
                }
278
                if (parameter.contains(GREATER_THAN.getTmForumOperator().operator())) {
1✔
279
                        return GREATER_THAN;
1✔
280
                }
281
                if (parameter.contains(LESS_THAN.getTmForumOperator().operator())) {
1✔
282
                        return LESS_THAN;
1✔
283
                }
284
                return Operator.EQUALS;
1✔
285
        }
286

287
        private static Optional<Operator> getOperator(String partToParse) {
288
                String[] parts = partToParse.split(Operator.EQUALS.getTmForumOperator().operator());
1✔
289
                return Arrays.stream(Operator.values())
1✔
290
                                .filter(operator -> {
1✔
291
                                        TMForumOperator tmForumOperator = operator.getTmForumOperator();
1✔
292
                                        if (parts[0].endsWith(tmForumOperator.textRepresentation())) {
1✔
293
                                                return true;
1✔
294
                                        }
295
                                        return false;
1✔
296
                                })
297
                                .findAny();
1✔
298
        }
299
}
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