d1fe9ec092b9db8e068e77b9c4ad6fe547506d17
[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 List<String> POSSIBLE_CONTENT = Arrays.stream(ContentParam.values())
48         .map(ContentParam::paramValue)
49         .collect(Collectors.toUnmodifiableList());
50     private static final List<String> POSSIBLE_WITH_DEFAULTS = Arrays.stream(WithDefaultsParam.values())
51         .map(WithDefaultsParam::paramValue)
52         .collect(Collectors.toUnmodifiableList());
53     private static final Set<String> KNOWN_PARAMS = Set.of(
54         // Read data
55         ContentParam.uriName, DepthParam.uriName, FieldsParam.uriName, WithDefaultsParam.uriName,
56         // Modify data
57         InsertParam.uriName, PointParam.uriName,
58         // Notifications
59         FilterParam.uriName, StartTimeParam.uriName, StopTimeParam.uriName ,"odl-skip-notification-data");
60
61
62     private QueryParams() {
63         // Utility class
64     }
65
66     public static @NonNull NotificationQueryParams newNotificationQueryParams(final UriInfo uriInfo) {
67         StartTimeParam startTime = null;
68         StopTimeParam stopTime = null;
69         FilterParam filter = null;
70         boolean skipNotificationData = false;
71
72         for (Entry<String, List<String>> entry : uriInfo.getQueryParameters().entrySet()) {
73             final String paramName = entry.getKey();
74             final List<String> paramValues = entry.getValue();
75
76             try {
77                 switch (paramName) {
78                     case FilterParam.uriName:
79                         filter = optionalParam(FilterParam::forUriValue, paramName, paramValues);
80                         break;
81                     case StartTimeParam.uriName:
82                         startTime = optionalParam(StartTimeParam::forUriValue, paramName, paramValues);
83                         break;
84                     case StopTimeParam.uriName:
85                         stopTime = optionalParam(StopTimeParam::forUriValue, paramName, paramValues);
86                         break;
87                     case "odl-skip-notification-data":
88                         // FIXME: this should be properly encapsulated in SkipNotificatioDataParameter
89                         skipNotificationData = Boolean.parseBoolean(optionalParam(paramName, paramValues));
90                         break;
91                     default:
92                         throw unhandledParam("notification", paramName);
93                 }
94             } catch (IllegalArgumentException e) {
95                 throw new RestconfDocumentedException("Invalid " + paramName + " value: " + e.getMessage(),
96                     ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE, e);
97             }
98         }
99
100         try {
101             return NotificationQueryParams.of(startTime, stopTime, filter, skipNotificationData);
102         } catch (IllegalArgumentException e) {
103             throw new RestconfDocumentedException("Invalid query parameters: " + e.getMessage(), e);
104         }
105     }
106
107     public static QueryParameters newQueryParameters(final ReadDataParams params,
108             final InstanceIdentifierContext<?> identifier) {
109         final var fields = params.fields();
110         if (fields == null) {
111             return QueryParameters.of(params);
112         }
113
114         return identifier.getMountPoint() != null
115             ? QueryParameters.ofFieldPaths(params, parseFieldsPaths(identifier, fields))
116                 : QueryParameters.ofFields(params, parseFieldsParameter(identifier, fields));
117     }
118
119     /**
120      * Parse parameters from URI request and check their types and values.
121      *
122      * @param uriInfo    URI info
123      * @return {@link ReadDataParams}
124      */
125     public static @NonNull ReadDataParams newReadDataParams(final UriInfo uriInfo) {
126         ContentParam content = ContentParam.ALL;
127         DepthParam depth = null;
128         FieldsParam fields = null;
129         WithDefaultsParam withDefaults = null;
130         boolean tagged = false;
131
132         for (Entry<String, List<String>> entry : uriInfo.getQueryParameters().entrySet()) {
133             final String paramName = entry.getKey();
134             final List<String> paramValues = entry.getValue();
135
136             switch (paramName) {
137                 case ContentParam.uriName:
138                     final String contentStr = optionalParam(paramName, paramValues);
139                     if (contentStr != null) {
140                         content = RestconfDocumentedException.throwIfNull(ContentParam.forUriValue(contentStr),
141                             ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE,
142                             "Invalid content parameter: %s, allowed values are %s", contentStr, POSSIBLE_CONTENT);
143                     }
144                     break;
145                 case DepthParam.uriName:
146                     final String depthStr = optionalParam(paramName, paramValues);
147                     try {
148                         depth = DepthParam.forUriValue(depthStr);
149                     } catch (IllegalArgumentException e) {
150                         throw new RestconfDocumentedException(e, new RestconfError(ErrorType.PROTOCOL,
151                             ErrorTag.INVALID_VALUE, "Invalid depth parameter: " + depthStr, null,
152                             "The depth parameter must be an integer between 1 and 65535 or \"unbounded\""));
153                     }
154                     break;
155                 case FieldsParam.uriName:
156                     final String fieldsStr = optionalParam(paramName, paramValues);
157                     if (fieldsStr != null) {
158                         try {
159                             fields = FieldsParam.parse(fieldsStr);
160                         } catch (ParseException e) {
161                             throw new RestconfDocumentedException(e, new RestconfError(ErrorType.PROTOCOL,
162                                 ErrorTag.INVALID_VALUE, "Invalid filds parameter: " + fieldsStr));
163                         }
164                     }
165                     break;
166                 case WithDefaultsParam.uriName:
167                     final String withDefaultsStr = optionalParam(paramName, paramValues);
168                     if (withDefaultsStr != null) {
169                         final WithDefaultsParam val = WithDefaultsParam.forUriValue(withDefaultsStr);
170                         if (val == null) {
171                             throw new RestconfDocumentedException(new RestconfError(ErrorType.PROTOCOL,
172                                 ErrorTag.INVALID_VALUE, "Invalid with-defaults parameter: " + withDefaultsStr, null,
173                                 "The with-defaults parameter must be a string in " + POSSIBLE_WITH_DEFAULTS));
174                         }
175
176                         switch (val) {
177                             case REPORT_ALL:
178                                 withDefaults = null;
179                                 tagged = false;
180                                 break;
181                             case REPORT_ALL_TAGGED:
182                                 withDefaults = null;
183                                 tagged = true;
184                                 break;
185                             default:
186                                 withDefaults = val;
187                                 tagged = false;
188                         }
189                     }
190                     break;
191                 default:
192                     // FIXME: recognize pretty-print here
193                     throw unhandledParam("read", paramName);
194             }
195         }
196
197         return ReadDataParams.of(content, depth, fields, withDefaults, tagged, false);
198     }
199
200     public static @NonNull WriteDataParams newWriteDataParams(final UriInfo uriInfo) {
201         InsertParam insert = null;
202         PointParam point = null;
203
204         for (final Entry<String, List<String>> entry : uriInfo.getQueryParameters().entrySet()) {
205             final String uriName = entry.getKey();
206             final List<String> paramValues = entry.getValue();
207             switch (uriName) {
208                 case InsertParam.uriName:
209                     final String instartStr = optionalParam(uriName, paramValues);
210                     if (instartStr != null) {
211                         insert = InsertParam.forUriValue(instartStr);
212                         if (insert == null) {
213                             throw new RestconfDocumentedException(
214                                 "Unrecognized insert parameter value '" + instartStr + "'", ErrorType.PROTOCOL,
215                                 ErrorTag.BAD_ELEMENT);
216                         }
217                     }
218                     break;
219                 case PointParam.uriName:
220                     final String pointStr = optionalParam(uriName, paramValues);
221                     if (pointStr != null) {
222                         point = PointParam.forUriValue(pointStr);
223                     }
224                     break;
225                 default:
226                     throw unhandledParam("write", uriName);
227             }
228         }
229
230         try {
231             return WriteDataParams.of(insert, point);
232         } catch (IllegalArgumentException e) {
233             throw new RestconfDocumentedException("Invalid query parameters: " + e.getMessage(), e);
234         }
235     }
236
237     private static RestconfDocumentedException unhandledParam(final String operation, final String name) {
238         return KNOWN_PARAMS.contains(name)
239             ? new RestconfDocumentedException("Invalid parameter in " + operation + ": " + name,
240                 ErrorType.PROTOCOL, ErrorTag.MALFORMED_MESSAGE)
241             : new RestconfDocumentedException("Unknown parameter in " + operation + ": " + name,
242                 ErrorType.PROTOCOL, ErrorTag.UNKNOWN_ATTRIBUTE);
243     }
244
245     @VisibleForTesting
246     static @Nullable String optionalParam(final String name, final List<String> values) {
247         switch (values.size()) {
248             case 0:
249                 return null;
250             case 1:
251                 return requireNonNull(values.get(0));
252             default:
253                 throw new RestconfDocumentedException("Parameter " + name + " can appear at most once in request URI",
254                     ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE);
255         }
256     }
257
258     private static <T> @Nullable T optionalParam(final Function<String, @NonNull T> factory, final String name,
259             final List<String> values) {
260         final String str = optionalParam(name, values);
261         return str == null ? null : factory.apply(str);
262     }
263 }