Move ReadDataTransactionUtil.parseUriParameters()
[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.util.Arrays;
17 import java.util.List;
18 import java.util.Map.Entry;
19 import java.util.Set;
20 import java.util.function.Function;
21 import java.util.stream.Collectors;
22 import javax.ws.rs.core.MultivaluedMap;
23 import javax.ws.rs.core.UriInfo;
24 import org.eclipse.jdt.annotation.NonNull;
25 import org.eclipse.jdt.annotation.Nullable;
26 import org.opendaylight.restconf.common.context.InstanceIdentifierContext;
27 import org.opendaylight.restconf.common.errors.RestconfDocumentedException;
28 import org.opendaylight.restconf.common.errors.RestconfError;
29 import org.opendaylight.restconf.nb.rfc8040.ContentParameter;
30 import org.opendaylight.restconf.nb.rfc8040.DepthParameter;
31 import org.opendaylight.restconf.nb.rfc8040.FieldsParameter;
32 import org.opendaylight.restconf.nb.rfc8040.FilterParameter;
33 import org.opendaylight.restconf.nb.rfc8040.InsertParameter;
34 import org.opendaylight.restconf.nb.rfc8040.NotificationQueryParams;
35 import org.opendaylight.restconf.nb.rfc8040.PointParameter;
36 import org.opendaylight.restconf.nb.rfc8040.StartTimeParameter;
37 import org.opendaylight.restconf.nb.rfc8040.StopTimeParameter;
38 import org.opendaylight.restconf.nb.rfc8040.WithDefaultsParameter;
39 import org.opendaylight.restconf.nb.rfc8040.WriteDataParams;
40 import org.opendaylight.restconf.nb.rfc8040.legacy.QueryParameters;
41 import org.opendaylight.restconf.nb.rfc8040.legacy.QueryParameters.Builder;
42 import org.opendaylight.yangtools.yang.common.ErrorTag;
43 import org.opendaylight.yangtools.yang.common.ErrorType;
44
45 @Beta
46 public final class QueryParams {
47     private static final Set<String> ALLOWED_PARAMETERS = Set.of(ContentParameter.uriName(), DepthParameter.uriName(),
48         FieldsParameter.uriName(), WithDefaultsParameter.uriName());
49     private static final List<String> POSSIBLE_CONTENT = Arrays.stream(ContentParameter.values())
50         .map(ContentParameter::uriValue)
51         .collect(Collectors.toUnmodifiableList());
52     private static final List<String> POSSIBLE_WITH_DEFAULTS = Arrays.stream(WithDefaultsParameter.values())
53         .map(WithDefaultsParameter::uriValue)
54         .collect(Collectors.toUnmodifiableList());
55
56     private QueryParams() {
57         // Utility class
58     }
59
60     public static @NonNull NotificationQueryParams newNotificationQueryParams(final UriInfo uriInfo) {
61         StartTimeParameter startTime = null;
62         StopTimeParameter stopTime = null;
63         FilterParameter filter = null;
64         boolean skipNotificationData = false;
65
66         for (final Entry<String, List<String>> entry : uriInfo.getQueryParameters().entrySet()) {
67             final String paramName = entry.getKey();
68             final List<String> paramValues = entry.getValue();
69
70             try {
71                 if (paramName.equals(StartTimeParameter.uriName())) {
72                     startTime = optionalParam(StartTimeParameter::forUriValue, paramName, paramValues);
73                     break;
74                 } else if (paramName.equals(StopTimeParameter.uriName())) {
75                     stopTime = optionalParam(StopTimeParameter::forUriValue, paramName, paramValues);
76                     break;
77                 } else if (paramName.equals(FilterParameter.uriName())) {
78                     filter = optionalParam(FilterParameter::forUriValue, paramName, paramValues);
79                 } else if (paramName.equals("odl-skip-notification-data")) {
80                     // FIXME: this should be properly encapsulated in SkipNotificatioDataParameter
81                     skipNotificationData = Boolean.parseBoolean(optionalParam(paramName, paramValues));
82                 } else {
83                     throw new RestconfDocumentedException("Bad parameter used with notifications: " + paramName);
84                 }
85             } catch (IllegalArgumentException e) {
86                 throw new RestconfDocumentedException("Invalid " + paramName + " value: " + e.getMessage(), e);
87             }
88         }
89
90         try {
91             return NotificationQueryParams.of(startTime, stopTime, filter, skipNotificationData);
92         } catch (IllegalArgumentException e) {
93             throw new RestconfDocumentedException("Invalid query parameters: " + e.getMessage(), e);
94         }
95     }
96
97     /**
98      * Parse parameters from URI request and check their types and values.
99      *
100      * @param identifier {@link InstanceIdentifierContext}
101      * @param uriInfo    URI info
102      * @return {@link QueryParameters}
103      */
104     public static QueryParameters newReadDataParams(final InstanceIdentifierContext<?> identifier,
105                                                     final UriInfo uriInfo) {
106         if (uriInfo == null) {
107             return QueryParameters.empty();
108         }
109
110         // check only allowed parameters
111         final MultivaluedMap<String, String> queryParams = uriInfo.getQueryParameters();
112         checkParametersTypes(queryParams.keySet(), ALLOWED_PARAMETERS);
113
114         final Builder builder = QueryParameters.builder();
115         // check and set content
116         final String contentStr = getSingleParameter(queryParams, ContentParameter.uriName());
117         if (contentStr != null) {
118             builder.setContent(RestconfDocumentedException.throwIfNull(
119                 ContentParameter.forUriValue(contentStr), ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE,
120                 "Invalid content parameter: %s, allowed values are %s", contentStr, POSSIBLE_CONTENT));
121         }
122
123         // check and set depth
124         final String depthStr = getSingleParameter(queryParams, DepthParameter.uriName());
125         if (depthStr != null) {
126             try {
127                 builder.setDepth(DepthParameter.forUriValue(depthStr));
128             } catch (IllegalArgumentException e) {
129                 throw new RestconfDocumentedException(e, new RestconfError(ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE,
130                     "Invalid depth parameter: " + depthStr, null,
131                     "The depth parameter must be an integer between 1 and 65535 or \"unbounded\""));
132             }
133         }
134
135         // check and set fields
136         final String fieldsStr = getSingleParameter(queryParams, FieldsParameter.uriName());
137         if (fieldsStr != null) {
138             // FIXME: parse a FieldsParameter instead
139             if (identifier.getMountPoint() != null) {
140                 builder.setFieldPaths(parseFieldsPaths(identifier, fieldsStr));
141             } else {
142                 builder.setFields(parseFieldsParameter(identifier, fieldsStr));
143             }
144         }
145
146         // check and set withDefaults parameter
147         final String withDefaultsStr = getSingleParameter(queryParams, WithDefaultsParameter.uriName());
148         if (withDefaultsStr != null) {
149             final WithDefaultsParameter val = WithDefaultsParameter.forUriValue(withDefaultsStr);
150             if (val == null) {
151                 throw new RestconfDocumentedException(new RestconfError(ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE,
152                     "Invalid with-defaults parameter: " + withDefaultsStr, null,
153                     "The with-defaults parameter must be a string in " + POSSIBLE_WITH_DEFAULTS));
154             }
155
156             switch (val) {
157                 case REPORT_ALL:
158                     break;
159                 case REPORT_ALL_TAGGED:
160                     builder.setTagged(true);
161                     break;
162                 default:
163                     builder.setWithDefault(val);
164             }
165         }
166
167         return builder.build();
168     }
169
170     public static @NonNull WriteDataParams newWriteDataParams(final UriInfo uriInfo) {
171         InsertParameter insert = null;
172         PointParameter point = null;
173
174         for (final Entry<String, List<String>> entry : uriInfo.getQueryParameters().entrySet()) {
175             final String uriName = entry.getKey();
176             final List<String> paramValues = entry.getValue();
177             if (uriName.equals(InsertParameter.uriName())) {
178                 final String str = optionalParam(uriName, paramValues);
179                 if (str != null) {
180                     insert = InsertParameter.forUriValue(str);
181                     if (insert == null) {
182                         throw new RestconfDocumentedException("Unrecognized insert parameter value '" + str + "'",
183                             ErrorType.PROTOCOL, ErrorTag.BAD_ELEMENT);
184                     }
185                 }
186             } else if (PointParameter.uriName().equals(uriName)) {
187                 final String str = optionalParam(uriName, paramValues);
188                 if (str != null) {
189                     point = PointParameter.forUriValue(str);
190                 }
191             } else {
192                 throw new RestconfDocumentedException("Bad parameter for post: " + uriName,
193                     ErrorType.PROTOCOL, ErrorTag.BAD_ELEMENT);
194             }
195         }
196
197         try {
198             return WriteDataParams.of(insert, point);
199         } catch (IllegalArgumentException e) {
200             throw new RestconfDocumentedException("Invalid query parameters: " + e.getMessage(), e);
201         }
202     }
203
204     /**
205      * Check if URI does not contain not allowed parameters for specified operation.
206      *
207      * @param usedParameters parameters used in URI request
208      * @param allowedParameters allowed parameters for operation
209      */
210     @VisibleForTesting
211     static void checkParametersTypes(final Set<String> usedParameters, final Set<String> allowedParameters) {
212         if (!allowedParameters.containsAll(usedParameters)) {
213             final Set<String> notAllowedParameters = usedParameters.stream()
214                 .filter(param -> !allowedParameters.contains(param))
215                 .collect(Collectors.toSet());
216             throw new RestconfDocumentedException("Not allowed parameters for read operation: " + notAllowedParameters,
217                 ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE);
218         }
219     }
220
221     @VisibleForTesting
222     static @Nullable String getSingleParameter(final MultivaluedMap<String, String> params, final String name) {
223         final var values = params.get(name);
224         return values == null ? null : optionalParam(name, values);
225     }
226
227     private static @Nullable String optionalParam(final String name, final List<String> values) {
228         switch (values.size()) {
229             case 0:
230                 return null;
231             case 1:
232                 return requireNonNull(values.get(0));
233             default:
234                 throw new RestconfDocumentedException("Parameter " + name + " can appear at most once in request URI",
235                     ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE);
236         }
237     }
238
239     private static <T> @Nullable T optionalParam(final Function<String, @NonNull T> factory, final String name,
240             final List<String> values) {
241         final String str = optionalParam(name, values);
242         return str == null ? null : factory.apply(str);
243     }
244 }