Separate out ReadDataParams
[netconf.git] / restconf / restconf-nb-rfc8040 / src / main / java / org / opendaylight / restconf / nb / rfc8040 / databind / jaxrs / QueryParams.java
1 /*
2  * Copyright (c) 2021 PANTHEON.tech, s.r.o. and others.  All rights reserved.
3  *
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
7  */
8 package org.opendaylight.restconf.nb.rfc8040.databind.jaxrs;
9
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;
13
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;
20 import java.util.Set;
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;
45
46 @Beta
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());
56
57     private QueryParams() {
58         // Utility class
59     }
60
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;
66
67         for (final Entry<String, List<String>> entry : uriInfo.getQueryParameters().entrySet()) {
68             final String paramName = entry.getKey();
69             final List<String> paramValues = entry.getValue();
70
71             try {
72                 if (paramName.equals(StartTimeParameter.uriName())) {
73                     startTime = optionalParam(StartTimeParameter::forUriValue, paramName, paramValues);
74                     break;
75                 } else if (paramName.equals(StopTimeParameter.uriName())) {
76                     stopTime = optionalParam(StopTimeParameter::forUriValue, paramName, paramValues);
77                     break;
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));
83                 } else {
84                     throw new RestconfDocumentedException("Bad parameter used with notifications: " + paramName);
85                 }
86             } catch (IllegalArgumentException e) {
87                 throw new RestconfDocumentedException("Invalid " + paramName + " value: " + e.getMessage(), e);
88             }
89         }
90
91         try {
92             return NotificationQueryParams.of(startTime, stopTime, filter, skipNotificationData);
93         } catch (IllegalArgumentException e) {
94             throw new RestconfDocumentedException("Invalid query parameters: " + e.getMessage(), e);
95         }
96     }
97
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);
103         }
104
105         return identifier.getMountPoint() != null
106             ? QueryParameters.ofFieldPaths(params, parseFieldsPaths(identifier, fields.uriValue()))
107                 : QueryParameters.ofFields(params, parseFieldsParameter(identifier, fields.uriValue()));
108     }
109
110     /**
111      * Parse parameters from URI request and check their types and values.
112      *
113      * @param uriInfo    URI info
114      * @return {@link ReadDataParams}
115      */
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);
120
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);
127
128         // check and set depth
129         final DepthParameter depth;
130         final String depthStr = getSingleParameter(queryParams, DepthParameter.uriName());
131         if (depthStr != null) {
132             try {
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\""));
138             }
139         } else {
140             depth = null;
141         }
142
143         // check and set fields
144         final FieldsParameter fields;
145         final String fieldsStr = getSingleParameter(queryParams, FieldsParameter.uriName());
146         if (fieldsStr != null) {
147             try {
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));
152             }
153         } else {
154             fields = null;
155         }
156
157         // check and set withDefaults parameter
158         final WithDefaultsParameter withDefaults;
159         final boolean tagged;
160
161         final String withDefaultsStr = getSingleParameter(queryParams, WithDefaultsParameter.uriName());
162         if (withDefaultsStr != null) {
163             final WithDefaultsParameter val = WithDefaultsParameter.forUriValue(withDefaultsStr);
164             if (val == null) {
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));
168             }
169
170             switch (val) {
171                 case REPORT_ALL:
172                     withDefaults = null;
173                     tagged = false;
174                     break;
175                 case REPORT_ALL_TAGGED:
176                     withDefaults = null;
177                     tagged = true;
178                     break;
179                 default:
180                     withDefaults = val;
181                     tagged = false;
182             }
183         } else {
184             withDefaults = null;
185             tagged = false;
186         }
187
188         // FIXME: recognize pretty-print here
189         return ReadDataParams.of(content, depth, fields, withDefaults, tagged, false);
190     }
191
192     public static @NonNull WriteDataParams newWriteDataParams(final UriInfo uriInfo) {
193         InsertParameter insert = null;
194         PointParameter point = null;
195
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);
201                 if (str != null) {
202                     insert = InsertParameter.forUriValue(str);
203                     if (insert == null) {
204                         throw new RestconfDocumentedException("Unrecognized insert parameter value '" + str + "'",
205                             ErrorType.PROTOCOL, ErrorTag.BAD_ELEMENT);
206                     }
207                 }
208             } else if (PointParameter.uriName().equals(uriName)) {
209                 final String str = optionalParam(uriName, paramValues);
210                 if (str != null) {
211                     point = PointParameter.forUriValue(str);
212                 }
213             } else {
214                 throw new RestconfDocumentedException("Bad parameter for post: " + uriName,
215                     ErrorType.PROTOCOL, ErrorTag.BAD_ELEMENT);
216             }
217         }
218
219         try {
220             return WriteDataParams.of(insert, point);
221         } catch (IllegalArgumentException e) {
222             throw new RestconfDocumentedException("Invalid query parameters: " + e.getMessage(), e);
223         }
224     }
225
226     /**
227      * Check if URI does not contain not allowed parameters for specified operation.
228      *
229      * @param usedParameters parameters used in URI request
230      * @param allowedParameters allowed parameters for operation
231      */
232     @VisibleForTesting
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);
240         }
241     }
242
243     @VisibleForTesting
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);
247     }
248
249     private static @Nullable String optionalParam(final String name, final List<String> values) {
250         switch (values.size()) {
251             case 0:
252                 return null;
253             case 1:
254                 return requireNonNull(values.get(0));
255             default:
256                 throw new RestconfDocumentedException("Parameter " + name + " can appear at most once in request URI",
257                     ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE);
258         }
259     }
260
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);
265     }
266 }