2 * Copyright (c) 2021 PANTHEON.tech, s.r.o. and others. All rights reserved.
4 * This program and the accompanying materials are made available under the
5 * terms of the Eclipse Public License v1.0 which accompanies this distribution,
6 * and is available at http://www.eclipse.org/legal/epl-v10.html
8 package org.opendaylight.restconf.nb.rfc8040.databind.jaxrs;
10 import static java.util.Objects.requireNonNull;
11 import static org.opendaylight.restconf.nb.rfc8040.utils.parser.ParserFieldsParameter.parseFieldsParameter;
12 import static org.opendaylight.restconf.nb.rfc8040.utils.parser.ParserFieldsParameter.parseFieldsPaths;
14 import com.google.common.annotations.Beta;
15 import com.google.common.annotations.VisibleForTesting;
16 import java.text.ParseException;
17 import java.util.Arrays;
18 import java.util.List;
19 import java.util.Map.Entry;
21 import java.util.function.Function;
22 import java.util.stream.Collectors;
23 import javax.ws.rs.core.MultivaluedMap;
24 import javax.ws.rs.core.UriInfo;
25 import org.eclipse.jdt.annotation.NonNull;
26 import org.eclipse.jdt.annotation.Nullable;
27 import org.opendaylight.restconf.common.context.InstanceIdentifierContext;
28 import org.opendaylight.restconf.common.errors.RestconfDocumentedException;
29 import org.opendaylight.restconf.common.errors.RestconfError;
30 import org.opendaylight.restconf.nb.rfc8040.ContentParameter;
31 import org.opendaylight.restconf.nb.rfc8040.DepthParameter;
32 import org.opendaylight.restconf.nb.rfc8040.FieldsParameter;
33 import org.opendaylight.restconf.nb.rfc8040.FilterParameter;
34 import org.opendaylight.restconf.nb.rfc8040.InsertParameter;
35 import org.opendaylight.restconf.nb.rfc8040.NotificationQueryParams;
36 import org.opendaylight.restconf.nb.rfc8040.PointParameter;
37 import org.opendaylight.restconf.nb.rfc8040.ReadDataParams;
38 import org.opendaylight.restconf.nb.rfc8040.StartTimeParameter;
39 import org.opendaylight.restconf.nb.rfc8040.StopTimeParameter;
40 import org.opendaylight.restconf.nb.rfc8040.WithDefaultsParameter;
41 import org.opendaylight.restconf.nb.rfc8040.WriteDataParams;
42 import org.opendaylight.restconf.nb.rfc8040.legacy.QueryParameters;
43 import org.opendaylight.yangtools.yang.common.ErrorTag;
44 import org.opendaylight.yangtools.yang.common.ErrorType;
47 public final class QueryParams {
48 private static final Set<String> ALLOWED_PARAMETERS = Set.of(ContentParameter.uriName(), DepthParameter.uriName(),
49 FieldsParameter.uriName(), WithDefaultsParameter.uriName());
50 private static final List<String> POSSIBLE_CONTENT = Arrays.stream(ContentParameter.values())
51 .map(ContentParameter::uriValue)
52 .collect(Collectors.toUnmodifiableList());
53 private static final List<String> POSSIBLE_WITH_DEFAULTS = Arrays.stream(WithDefaultsParameter.values())
54 .map(WithDefaultsParameter::uriValue)
55 .collect(Collectors.toUnmodifiableList());
57 private QueryParams() {
61 public static @NonNull NotificationQueryParams newNotificationQueryParams(final UriInfo uriInfo) {
62 StartTimeParameter startTime = null;
63 StopTimeParameter stopTime = null;
64 FilterParameter filter = null;
65 boolean skipNotificationData = false;
67 for (final Entry<String, List<String>> entry : uriInfo.getQueryParameters().entrySet()) {
68 final String paramName = entry.getKey();
69 final List<String> paramValues = entry.getValue();
72 if (paramName.equals(StartTimeParameter.uriName())) {
73 startTime = optionalParam(StartTimeParameter::forUriValue, paramName, paramValues);
75 } else if (paramName.equals(StopTimeParameter.uriName())) {
76 stopTime = optionalParam(StopTimeParameter::forUriValue, paramName, paramValues);
78 } else if (paramName.equals(FilterParameter.uriName())) {
79 filter = optionalParam(FilterParameter::forUriValue, paramName, paramValues);
80 } else if (paramName.equals("odl-skip-notification-data")) {
81 // FIXME: this should be properly encapsulated in SkipNotificatioDataParameter
82 skipNotificationData = Boolean.parseBoolean(optionalParam(paramName, paramValues));
84 throw new RestconfDocumentedException("Bad parameter used with notifications: " + paramName);
86 } catch (IllegalArgumentException e) {
87 throw new RestconfDocumentedException("Invalid " + paramName + " value: " + e.getMessage(), e);
92 return NotificationQueryParams.of(startTime, stopTime, filter, skipNotificationData);
93 } catch (IllegalArgumentException e) {
94 throw new RestconfDocumentedException("Invalid query parameters: " + e.getMessage(), e);
98 public static QueryParameters newQueryParameters(final ReadDataParams params,
99 final InstanceIdentifierContext<?> identifier) {
100 final var fields = params.fields();
101 if (fields == null) {
102 return QueryParameters.of(params);
105 return identifier.getMountPoint() != null
106 ? QueryParameters.ofFieldPaths(params, parseFieldsPaths(identifier, fields.uriValue()))
107 : QueryParameters.ofFields(params, parseFieldsParameter(identifier, fields.uriValue()));
111 * Parse parameters from URI request and check their types and values.
113 * @param uriInfo URI info
114 * @return {@link ReadDataParams}
116 public static @NonNull ReadDataParams newReadDataParams(final UriInfo uriInfo) {
117 // check only allowed parameters
118 final MultivaluedMap<String, String> queryParams = uriInfo.getQueryParameters();
119 checkParametersTypes(queryParams.keySet(), ALLOWED_PARAMETERS);
121 // check and set content
122 final String contentStr = getSingleParameter(queryParams, ContentParameter.uriName());
123 final ContentParameter content = contentStr == null ? ContentParameter.ALL
124 : RestconfDocumentedException.throwIfNull(ContentParameter.forUriValue(contentStr),
125 ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE,
126 "Invalid content parameter: %s, allowed values are %s", contentStr, POSSIBLE_CONTENT);
128 // check and set depth
129 final DepthParameter depth;
130 final String depthStr = getSingleParameter(queryParams, DepthParameter.uriName());
131 if (depthStr != null) {
133 depth = DepthParameter.forUriValue(depthStr);
134 } catch (IllegalArgumentException e) {
135 throw new RestconfDocumentedException(e, new RestconfError(ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE,
136 "Invalid depth parameter: " + depthStr, null,
137 "The depth parameter must be an integer between 1 and 65535 or \"unbounded\""));
143 // check and set fields
144 final FieldsParameter fields;
145 final String fieldsStr = getSingleParameter(queryParams, FieldsParameter.uriName());
146 if (fieldsStr != null) {
148 fields = FieldsParameter.parse(fieldsStr);
149 } catch (ParseException e) {
150 throw new RestconfDocumentedException(e, new RestconfError(ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE,
151 "Invalid filds parameter: " + fieldsStr));
157 // check and set withDefaults parameter
158 final WithDefaultsParameter withDefaults;
159 final boolean tagged;
161 final String withDefaultsStr = getSingleParameter(queryParams, WithDefaultsParameter.uriName());
162 if (withDefaultsStr != null) {
163 final WithDefaultsParameter val = WithDefaultsParameter.forUriValue(withDefaultsStr);
165 throw new RestconfDocumentedException(new RestconfError(ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE,
166 "Invalid with-defaults parameter: " + withDefaultsStr, null,
167 "The with-defaults parameter must be a string in " + POSSIBLE_WITH_DEFAULTS));
175 case REPORT_ALL_TAGGED:
188 // FIXME: recognize pretty-print here
189 return ReadDataParams.of(content, depth, fields, withDefaults, tagged, false);
192 public static @NonNull WriteDataParams newWriteDataParams(final UriInfo uriInfo) {
193 InsertParameter insert = null;
194 PointParameter point = null;
196 for (final Entry<String, List<String>> entry : uriInfo.getQueryParameters().entrySet()) {
197 final String uriName = entry.getKey();
198 final List<String> paramValues = entry.getValue();
199 if (uriName.equals(InsertParameter.uriName())) {
200 final String str = optionalParam(uriName, paramValues);
202 insert = InsertParameter.forUriValue(str);
203 if (insert == null) {
204 throw new RestconfDocumentedException("Unrecognized insert parameter value '" + str + "'",
205 ErrorType.PROTOCOL, ErrorTag.BAD_ELEMENT);
208 } else if (PointParameter.uriName().equals(uriName)) {
209 final String str = optionalParam(uriName, paramValues);
211 point = PointParameter.forUriValue(str);
214 throw new RestconfDocumentedException("Bad parameter for post: " + uriName,
215 ErrorType.PROTOCOL, ErrorTag.BAD_ELEMENT);
220 return WriteDataParams.of(insert, point);
221 } catch (IllegalArgumentException e) {
222 throw new RestconfDocumentedException("Invalid query parameters: " + e.getMessage(), e);
227 * Check if URI does not contain not allowed parameters for specified operation.
229 * @param usedParameters parameters used in URI request
230 * @param allowedParameters allowed parameters for operation
233 static void checkParametersTypes(final Set<String> usedParameters, final Set<String> allowedParameters) {
234 if (!allowedParameters.containsAll(usedParameters)) {
235 final Set<String> notAllowedParameters = usedParameters.stream()
236 .filter(param -> !allowedParameters.contains(param))
237 .collect(Collectors.toSet());
238 throw new RestconfDocumentedException("Not allowed parameters for read operation: " + notAllowedParameters,
239 ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE);
244 static @Nullable String getSingleParameter(final MultivaluedMap<String, String> params, final String name) {
245 final var values = params.get(name);
246 return values == null ? null : optionalParam(name, values);
249 private static @Nullable String optionalParam(final String name, final List<String> values) {
250 switch (values.size()) {
254 return requireNonNull(values.get(0));
256 throw new RestconfDocumentedException("Parameter " + name + " can appear at most once in request URI",
257 ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE);
261 private static <T> @Nullable T optionalParam(final Function<String, @NonNull T> factory, final String name,
262 final List<String> values) {
263 final String str = optionalParam(name, values);
264 return str == null ? null : factory.apply(str);