4f836d009690fcd7ba703ba9b9d7886f93146385
[netconf.git] / restconf / restconf-nb / 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
12 import com.google.common.annotations.Beta;
13 import com.google.common.annotations.VisibleForTesting;
14 import java.util.List;
15 import java.util.Map.Entry;
16 import java.util.Set;
17 import java.util.function.Function;
18 import javax.ws.rs.core.UriInfo;
19 import org.eclipse.jdt.annotation.NonNull;
20 import org.eclipse.jdt.annotation.Nullable;
21 import org.opendaylight.restconf.api.query.ChangedLeafNodesOnlyParam;
22 import org.opendaylight.restconf.api.query.ContentParam;
23 import org.opendaylight.restconf.api.query.DepthParam;
24 import org.opendaylight.restconf.api.query.FieldsParam;
25 import org.opendaylight.restconf.api.query.FilterParam;
26 import org.opendaylight.restconf.api.query.InsertParam;
27 import org.opendaylight.restconf.api.query.LeafNodesOnlyParam;
28 import org.opendaylight.restconf.api.query.PointParam;
29 import org.opendaylight.restconf.api.query.PrettyPrintParam;
30 import org.opendaylight.restconf.api.query.SkipNotificationDataParam;
31 import org.opendaylight.restconf.api.query.StartTimeParam;
32 import org.opendaylight.restconf.api.query.StopTimeParam;
33 import org.opendaylight.restconf.api.query.WithDefaultsParam;
34 import org.opendaylight.restconf.common.context.InstanceIdentifierContext;
35 import org.opendaylight.restconf.common.errors.RestconfDocumentedException;
36 import org.opendaylight.restconf.common.errors.RestconfError;
37 import org.opendaylight.restconf.nb.rfc8040.NotificationQueryParams;
38 import org.opendaylight.restconf.nb.rfc8040.ReadDataParams;
39 import org.opendaylight.restconf.nb.rfc8040.WriteDataParams;
40 import org.opendaylight.restconf.nb.rfc8040.legacy.QueryParameters;
41 import org.opendaylight.restconf.nb.rfc8040.utils.parser.NetconfFieldsTranslator;
42 import org.opendaylight.restconf.nb.rfc8040.utils.parser.WriterFieldsTranslator;
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> KNOWN_PARAMS = Set.of(
49         // Read data
50         ContentParam.uriName, DepthParam.uriName, FieldsParam.uriName, WithDefaultsParam.uriName,
51         PrettyPrintParam.uriName,
52         // Modify data
53         InsertParam.uriName, PointParam.uriName,
54         // Notifications
55         FilterParam.uriName, StartTimeParam.uriName, StopTimeParam.uriName,
56         LeafNodesOnlyParam.uriName, SkipNotificationDataParam.uriName, ChangedLeafNodesOnlyParam.uriName);
57
58     private QueryParams() {
59         // Utility class
60     }
61
62     public static @NonNull NotificationQueryParams newNotificationQueryParams(final UriInfo uriInfo) {
63         StartTimeParam startTime = null;
64         StopTimeParam stopTime = null;
65         FilterParam filter = null;
66         LeafNodesOnlyParam leafNodesOnly = null;
67         SkipNotificationDataParam skipNotificationData = null;
68         ChangedLeafNodesOnlyParam changedLeafNodesOnly = null;
69
70         for (Entry<String, List<String>> entry : uriInfo.getQueryParameters().entrySet()) {
71             final String paramName = entry.getKey();
72             final List<String> paramValues = entry.getValue();
73
74             try {
75                 switch (paramName) {
76                     case FilterParam.uriName:
77                         filter = optionalParam(FilterParam::forUriValue, paramName, paramValues);
78                         break;
79                     case StartTimeParam.uriName:
80                         startTime = optionalParam(StartTimeParam::forUriValue, paramName, paramValues);
81                         break;
82                     case StopTimeParam.uriName:
83                         stopTime = optionalParam(StopTimeParam::forUriValue, paramName, paramValues);
84                         break;
85                     case LeafNodesOnlyParam.uriName:
86                         leafNodesOnly = optionalParam(LeafNodesOnlyParam::forUriValue, paramName, paramValues);
87                         break;
88                     case SkipNotificationDataParam.uriName:
89                         skipNotificationData = optionalParam(SkipNotificationDataParam::forUriValue, paramName,
90                             paramValues);
91                         break;
92                     case ChangedLeafNodesOnlyParam.uriName:
93                         changedLeafNodesOnly = optionalParam(ChangedLeafNodesOnlyParam::forUriValue, paramName,
94                             paramValues);
95                         break;
96                     default:
97                         throw unhandledParam("notification", paramName);
98                 }
99             } catch (IllegalArgumentException e) {
100                 throw new RestconfDocumentedException("Invalid " + paramName + " value: " + e.getMessage(),
101                     ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE, e);
102             }
103         }
104
105         try {
106             return NotificationQueryParams.of(startTime, stopTime, filter, leafNodesOnly, skipNotificationData,
107                     changedLeafNodesOnly);
108         } catch (IllegalArgumentException e) {
109             throw new RestconfDocumentedException("Invalid query parameters: " + e.getMessage(), e);
110         }
111     }
112
113     public static QueryParameters newQueryParameters(final ReadDataParams params,
114             final InstanceIdentifierContext identifier) {
115         final var fields = params.fields();
116         if (fields == null) {
117             return QueryParameters.of(params);
118         }
119
120         return identifier.getMountPoint() != null
121             ? QueryParameters.ofFieldPaths(params, NetconfFieldsTranslator.translate(identifier, fields))
122                 : QueryParameters.ofFields(params, WriterFieldsTranslator.translate(identifier, fields));
123     }
124
125     /**
126      * Parse parameters from URI request and check their types and values.
127      *
128      * @param uriInfo    URI info
129      * @return {@link ReadDataParams}
130      */
131     public static @NonNull ReadDataParams newReadDataParams(final UriInfo uriInfo) {
132         ContentParam content = ContentParam.ALL;
133         DepthParam depth = null;
134         FieldsParam fields = null;
135         WithDefaultsParam withDefaults = null;
136         PrettyPrintParam prettyPrint = null;
137         boolean tagged = false;
138
139         for (Entry<String, List<String>> entry : uriInfo.getQueryParameters().entrySet()) {
140             final String paramName = entry.getKey();
141             final List<String> paramValues = entry.getValue();
142
143             try {
144                 switch (paramName) {
145                     case ContentParam.uriName:
146                         content = optionalParam(ContentParam::forUriValue, paramName, paramValues);
147                         break;
148                     case DepthParam.uriName:
149                         final String depthStr = optionalParam(paramName, paramValues);
150                         try {
151                             depth = DepthParam.forUriValue(depthStr);
152                         } catch (IllegalArgumentException e) {
153                             throw new RestconfDocumentedException(e, new RestconfError(ErrorType.PROTOCOL,
154                                 ErrorTag.INVALID_VALUE, "Invalid depth parameter: " + depthStr, null,
155                                 "The depth parameter must be an integer between 1 and 65535 or \"unbounded\""));
156                         }
157                         break;
158                     case FieldsParam.uriName:
159                         fields = optionalParam(FieldsParam::forUriValue, paramName, paramValues);
160                         break;
161                     case WithDefaultsParam.uriName:
162                         final var defaultsVal = optionalParam(WithDefaultsParam::forUriValue, paramName, paramValues);
163                         if (defaultsVal != null) {
164                             tagged = switch (defaultsVal) {
165                                 case REPORT_ALL -> {
166                                     withDefaults = null;
167                                     yield false;
168                                 }
169                                 case REPORT_ALL_TAGGED -> {
170                                     withDefaults = null;
171                                     yield true;
172                                 }
173                                 default -> {
174                                     withDefaults = defaultsVal;
175                                     yield false;
176                                 }
177                             };
178                         }
179                         break;
180                     case PrettyPrintParam.uriName:
181                         prettyPrint = optionalParam(PrettyPrintParam::forUriValue, paramName, paramValues);
182                         break;
183                     default:
184                         throw unhandledParam("read", paramName);
185                 }
186             } catch (IllegalArgumentException e) {
187                 throw new RestconfDocumentedException("Invalid " + paramName + " value: " + e.getMessage(),
188                     ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE, e);
189             }
190         }
191
192         return ReadDataParams.of(content, depth, fields, withDefaults, tagged, prettyPrint);
193     }
194
195     public static @NonNull WriteDataParams newWriteDataParams(final UriInfo uriInfo) {
196         InsertParam insert = null;
197         PointParam point = null;
198
199         for (final Entry<String, List<String>> entry : uriInfo.getQueryParameters().entrySet()) {
200             final String paramName = entry.getKey();
201             final List<String> paramValues = entry.getValue();
202
203             try {
204                 switch (paramName) {
205                     case InsertParam.uriName:
206                         insert = optionalParam(InsertParam::forUriValue, paramName, paramValues);
207                         break;
208                     case PointParam.uriName:
209                         point = optionalParam(PointParam::forUriValue, paramName, paramValues);
210                         break;
211                     default:
212                         throw unhandledParam("write", paramName);
213                 }
214             } catch (IllegalArgumentException e) {
215                 throw new RestconfDocumentedException("Invalid " + paramName + " value: " + e.getMessage(),
216                     ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE, e);
217             }
218         }
219
220         try {
221             return WriteDataParams.of(insert, point);
222         } catch (IllegalArgumentException e) {
223             throw new RestconfDocumentedException("Invalid query parameters: " + e.getMessage(), e);
224         }
225     }
226
227     private static RestconfDocumentedException unhandledParam(final String operation, final String name) {
228         return KNOWN_PARAMS.contains(name)
229             ? new RestconfDocumentedException("Invalid parameter in " + operation + ": " + name,
230                 ErrorType.PROTOCOL, ErrorTag.MALFORMED_MESSAGE)
231             : new RestconfDocumentedException("Unknown parameter in " + operation + ": " + name,
232                 ErrorType.PROTOCOL, ErrorTag.UNKNOWN_ATTRIBUTE);
233     }
234
235     @VisibleForTesting
236     static @Nullable String optionalParam(final String name, final List<String> values) {
237         return switch (values.size()) {
238             case 0 -> null;
239             case 1 -> requireNonNull(values.get(0));
240             default -> throw new RestconfDocumentedException(
241                 "Parameter " + name + " can appear at most once in request URI",
242                 ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE);
243         };
244     }
245
246     private static <T> @Nullable T optionalParam(final Function<String, @NonNull T> factory, final String name,
247             final List<String> values) {
248         final String str = optionalParam(name, values);
249         return str == null ? null : factory.apply(str);
250     }
251 }