f3c766bb20d67838d4be7298134f16b969133d1d
[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.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.ContentParam;
30 import org.opendaylight.restconf.nb.rfc8040.DepthParam;
31 import org.opendaylight.restconf.nb.rfc8040.FieldsParam;
32 import org.opendaylight.restconf.nb.rfc8040.FilterParam;
33 import org.opendaylight.restconf.nb.rfc8040.InsertParam;
34 import org.opendaylight.restconf.nb.rfc8040.NotificationQueryParams;
35 import org.opendaylight.restconf.nb.rfc8040.PointParam;
36 import org.opendaylight.restconf.nb.rfc8040.ReadDataParams;
37 import org.opendaylight.restconf.nb.rfc8040.StartTimeParam;
38 import org.opendaylight.restconf.nb.rfc8040.StopTimeParam;
39 import org.opendaylight.restconf.nb.rfc8040.WithDefaultsParam;
40 import org.opendaylight.restconf.nb.rfc8040.WriteDataParams;
41 import org.opendaylight.restconf.nb.rfc8040.legacy.QueryParameters;
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(ContentParam.uriName(), DepthParam.uriName(),
48         FieldsParam.uriName(), WithDefaultsParam.uriName());
49     private static final List<String> POSSIBLE_CONTENT = Arrays.stream(ContentParam.values())
50         .map(ContentParam::paramValue)
51         .collect(Collectors.toUnmodifiableList());
52     private static final List<String> POSSIBLE_WITH_DEFAULTS = Arrays.stream(WithDefaultsParam.values())
53         .map(WithDefaultsParam::paramValue)
54         .collect(Collectors.toUnmodifiableList());
55
56     private QueryParams() {
57         // Utility class
58     }
59
60     public static @NonNull NotificationQueryParams newNotificationQueryParams(final UriInfo uriInfo) {
61         StartTimeParam startTime = null;
62         StopTimeParam stopTime = null;
63         FilterParam filter = null;
64         boolean skipNotificationData = false;
65
66         for (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(StartTimeParam.uriName())) {
72                     startTime = optionalParam(StartTimeParam::forUriValue, paramName, paramValues);
73                     break;
74                 } else if (paramName.equals(StopTimeParam.uriName())) {
75                     stopTime = optionalParam(StopTimeParam::forUriValue, paramName, paramValues);
76                     break;
77                 } else if (paramName.equals(FilterParam.uriName())) {
78                     filter = optionalParam(FilterParam::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                         ErrorType.PROTOCOL, ErrorTag. UNKNOWN_ATTRIBUTE);
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.paramValue()))
107                 : QueryParameters.ofFields(params, parseFieldsParameter(identifier, fields.paramValue()));
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         ContentParam content = ContentParam.ALL;
118         DepthParam depth = null;
119         FieldsParam fields = null;
120         WithDefaultsParam withDefaults = null;
121         boolean tagged = false;
122
123         for (Entry<String, List<String>> entry : uriInfo.getQueryParameters().entrySet()) {
124             final String paramName = entry.getKey();
125             final List<String> paramValues = entry.getValue();
126
127             if (paramName.equals(ContentParam.uriName())) {
128                 final String str = optionalParam(paramName, paramValues);
129                 if (str != null) {
130                     content = RestconfDocumentedException.throwIfNull(ContentParam.forUriValue(str),
131                         ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE,
132                         "Invalid content parameter: %s, allowed values are %s", str, POSSIBLE_CONTENT);
133                 }
134             } else if (paramName.equals(DepthParam.uriName())) {
135                 final String str = optionalParam(paramName, paramValues);
136                 try {
137                     depth = DepthParam.forUriValue(str);
138                 } catch (IllegalArgumentException e) {
139                     throw new RestconfDocumentedException(e, new RestconfError(ErrorType.PROTOCOL,
140                         ErrorTag.INVALID_VALUE, "Invalid depth parameter: " + str, null,
141                         "The depth parameter must be an integer between 1 and 65535 or \"unbounded\""));
142                 }
143             } else if (paramName.equals(FieldsParam.uriName())) {
144                 final String str = optionalParam(paramName, paramValues);
145                 if (str != null) {
146                     try {
147                         fields = FieldsParam.parse(str);
148                     } catch (ParseException e) {
149                         throw new RestconfDocumentedException(e, new RestconfError(ErrorType.PROTOCOL,
150                             ErrorTag.INVALID_VALUE, "Invalid filds parameter: " + str));
151                     }
152                 }
153             } else if (paramName.equals(WithDefaultsParam.uriName())) {
154                 final String str = optionalParam(paramName, paramValues);
155                 if (str != null) {
156                     final WithDefaultsParam val = WithDefaultsParam.forUriValue(str);
157                     if (val == null) {
158                         throw new RestconfDocumentedException(new RestconfError(ErrorType.PROTOCOL,
159                             ErrorTag.INVALID_VALUE, "Invalid with-defaults parameter: " + str, null,
160                             "The with-defaults parameter must be a string in " + POSSIBLE_WITH_DEFAULTS));
161                     }
162
163                     switch (val) {
164                         case REPORT_ALL:
165                             withDefaults = null;
166                             tagged = false;
167                             break;
168                         case REPORT_ALL_TAGGED:
169                             withDefaults = null;
170                             tagged = true;
171                             break;
172                         default:
173                             withDefaults = val;
174                             tagged = false;
175                     }
176                 }
177             } else {
178                 // FIXME: recognize pretty-print here
179                 throw new RestconfDocumentedException("Not allowed parameter for read operation: " + paramName,
180                     ErrorType.PROTOCOL, ErrorTag.UNKNOWN_ATTRIBUTE);
181             }
182         }
183
184         return ReadDataParams.of(content, depth, fields, withDefaults, tagged, false);
185     }
186
187     public static @NonNull WriteDataParams newWriteDataParams(final UriInfo uriInfo) {
188         InsertParam insert = null;
189         PointParam point = null;
190
191         for (final Entry<String, List<String>> entry : uriInfo.getQueryParameters().entrySet()) {
192             final String uriName = entry.getKey();
193             final List<String> paramValues = entry.getValue();
194             if (uriName.equals(InsertParam.uriName())) {
195                 final String str = optionalParam(uriName, paramValues);
196                 if (str != null) {
197                     insert = InsertParam.forUriValue(str);
198                     if (insert == null) {
199                         throw new RestconfDocumentedException("Unrecognized insert parameter value '" + str + "'",
200                             ErrorType.PROTOCOL, ErrorTag.BAD_ELEMENT);
201                     }
202                 }
203             } else if (PointParam.uriName().equals(uriName)) {
204                 final String str = optionalParam(uriName, paramValues);
205                 if (str != null) {
206                     point = PointParam.forUriValue(str);
207                 }
208             } else {
209                 throw new RestconfDocumentedException("Bad parameter for post: " + uriName,
210                     ErrorType.PROTOCOL, ErrorTag.UNKNOWN_ATTRIBUTE);
211             }
212         }
213
214         try {
215             return WriteDataParams.of(insert, point);
216         } catch (IllegalArgumentException e) {
217             throw new RestconfDocumentedException("Invalid query parameters: " + e.getMessage(), e);
218         }
219     }
220
221     @VisibleForTesting
222     static @Nullable String optionalParam(final String name, final List<String> values) {
223         switch (values.size()) {
224             case 0:
225                 return null;
226             case 1:
227                 return requireNonNull(values.get(0));
228             default:
229                 throw new RestconfDocumentedException("Parameter " + name + " can appear at most once in request URI",
230                     ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE);
231         }
232     }
233
234     private static <T> @Nullable T optionalParam(final Function<String, @NonNull T> factory, final String name,
235             final List<String> values) {
236         final String str = optionalParam(name, values);
237         return str == null ? null : factory.apply(str);
238     }
239 }