Support leaf-nodes-only in RFC8040 northbound
[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
12 import com.google.common.annotations.Beta;
13 import com.google.common.annotations.VisibleForTesting;
14 import java.text.ParseException;
15 import java.util.Arrays;
16 import java.util.List;
17 import java.util.Map.Entry;
18 import java.util.Set;
19 import java.util.function.Function;
20 import java.util.stream.Collectors;
21 import javax.ws.rs.core.UriInfo;
22 import org.eclipse.jdt.annotation.NonNull;
23 import org.eclipse.jdt.annotation.Nullable;
24 import org.opendaylight.restconf.common.context.InstanceIdentifierContext;
25 import org.opendaylight.restconf.common.errors.RestconfDocumentedException;
26 import org.opendaylight.restconf.common.errors.RestconfError;
27 import org.opendaylight.restconf.nb.rfc8040.ContentParam;
28 import org.opendaylight.restconf.nb.rfc8040.DepthParam;
29 import org.opendaylight.restconf.nb.rfc8040.FieldsParam;
30 import org.opendaylight.restconf.nb.rfc8040.FilterParam;
31 import org.opendaylight.restconf.nb.rfc8040.InsertParam;
32 import org.opendaylight.restconf.nb.rfc8040.LeafNodesOnlyParam;
33 import org.opendaylight.restconf.nb.rfc8040.NotificationQueryParams;
34 import org.opendaylight.restconf.nb.rfc8040.PointParam;
35 import org.opendaylight.restconf.nb.rfc8040.ReadDataParams;
36 import org.opendaylight.restconf.nb.rfc8040.SkipNotificationDataParam;
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.restconf.nb.rfc8040.utils.parser.NetconfFieldsTranslator;
43 import org.opendaylight.restconf.nb.rfc8040.utils.parser.WriterFieldsTranslator;
44 import org.opendaylight.yangtools.yang.common.ErrorTag;
45 import org.opendaylight.yangtools.yang.common.ErrorType;
46
47 @Beta
48 public final class QueryParams {
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     private static final Set<String> KNOWN_PARAMS = Set.of(
56         // Read data
57         ContentParam.uriName, DepthParam.uriName, FieldsParam.uriName, WithDefaultsParam.uriName,
58         // Modify data
59         InsertParam.uriName, PointParam.uriName,
60         // Notifications
61         FilterParam.uriName, StartTimeParam.uriName, StopTimeParam.uriName ,"odl-skip-notification-data");
62
63
64     private QueryParams() {
65         // Utility class
66     }
67
68     public static @NonNull NotificationQueryParams newNotificationQueryParams(final UriInfo uriInfo) {
69         StartTimeParam startTime = null;
70         StopTimeParam stopTime = null;
71         FilterParam filter = null;
72         LeafNodesOnlyParam leafNodesOnly = null;
73         SkipNotificationDataParam skipNotificationData = null;
74
75         for (Entry<String, List<String>> entry : uriInfo.getQueryParameters().entrySet()) {
76             final String paramName = entry.getKey();
77             final List<String> paramValues = entry.getValue();
78
79             try {
80                 switch (paramName) {
81                     case FilterParam.uriName:
82                         filter = optionalParam(FilterParam::forUriValue, paramName, paramValues);
83                         break;
84                     case StartTimeParam.uriName:
85                         startTime = optionalParam(StartTimeParam::forUriValue, paramName, paramValues);
86                         break;
87                     case StopTimeParam.uriName:
88                         stopTime = optionalParam(StopTimeParam::forUriValue, paramName, paramValues);
89                         break;
90                     case LeafNodesOnlyParam.uriName:
91                         leafNodesOnly = optionalParam(LeafNodesOnlyParam::forUriValue, paramName, paramValues);
92                         break;
93                     case SkipNotificationDataParam.uriName:
94                         skipNotificationData = optionalParam(SkipNotificationDataParam::forUriValue, paramName,
95                             paramValues);
96                         break;
97                     default:
98                         throw unhandledParam("notification", paramName);
99                 }
100             } catch (IllegalArgumentException e) {
101                 throw new RestconfDocumentedException("Invalid " + paramName + " value: " + e.getMessage(),
102                     ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE, e);
103             }
104         }
105
106         try {
107             return NotificationQueryParams.of(startTime, stopTime, filter, leafNodesOnly, skipNotificationData);
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         boolean tagged = false;
137
138         for (Entry<String, List<String>> entry : uriInfo.getQueryParameters().entrySet()) {
139             final String paramName = entry.getKey();
140             final List<String> paramValues = entry.getValue();
141
142             switch (paramName) {
143                 case ContentParam.uriName:
144                     final String contentStr = optionalParam(paramName, paramValues);
145                     if (contentStr != null) {
146                         content = RestconfDocumentedException.throwIfNull(ContentParam.forUriValue(contentStr),
147                             ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE,
148                             "Invalid content parameter: %s, allowed values are %s", contentStr, POSSIBLE_CONTENT);
149                     }
150                     break;
151                 case DepthParam.uriName:
152                     final String depthStr = optionalParam(paramName, paramValues);
153                     try {
154                         depth = DepthParam.forUriValue(depthStr);
155                     } catch (IllegalArgumentException e) {
156                         throw new RestconfDocumentedException(e, new RestconfError(ErrorType.PROTOCOL,
157                             ErrorTag.INVALID_VALUE, "Invalid depth parameter: " + depthStr, null,
158                             "The depth parameter must be an integer between 1 and 65535 or \"unbounded\""));
159                     }
160                     break;
161                 case FieldsParam.uriName:
162                     final String fieldsStr = optionalParam(paramName, paramValues);
163                     if (fieldsStr != null) {
164                         try {
165                             fields = FieldsParam.parse(fieldsStr);
166                         } catch (ParseException e) {
167                             throw new RestconfDocumentedException(e, new RestconfError(ErrorType.PROTOCOL,
168                                 ErrorTag.INVALID_VALUE, "Invalid filds parameter: " + fieldsStr));
169                         }
170                     }
171                     break;
172                 case WithDefaultsParam.uriName:
173                     final String withDefaultsStr = optionalParam(paramName, paramValues);
174                     if (withDefaultsStr != null) {
175                         final WithDefaultsParam val = WithDefaultsParam.forUriValue(withDefaultsStr);
176                         if (val == null) {
177                             throw new RestconfDocumentedException(new RestconfError(ErrorType.PROTOCOL,
178                                 ErrorTag.INVALID_VALUE, "Invalid with-defaults parameter: " + withDefaultsStr, null,
179                                 "The with-defaults parameter must be a string in " + POSSIBLE_WITH_DEFAULTS));
180                         }
181
182                         switch (val) {
183                             case REPORT_ALL:
184                                 withDefaults = null;
185                                 tagged = false;
186                                 break;
187                             case REPORT_ALL_TAGGED:
188                                 withDefaults = null;
189                                 tagged = true;
190                                 break;
191                             default:
192                                 withDefaults = val;
193                                 tagged = false;
194                         }
195                     }
196                     break;
197                 default:
198                     // FIXME: recognize pretty-print here
199                     throw unhandledParam("read", paramName);
200             }
201         }
202
203         return ReadDataParams.of(content, depth, fields, withDefaults, tagged, false);
204     }
205
206     public static @NonNull WriteDataParams newWriteDataParams(final UriInfo uriInfo) {
207         InsertParam insert = null;
208         PointParam point = null;
209
210         for (final Entry<String, List<String>> entry : uriInfo.getQueryParameters().entrySet()) {
211             final String uriName = entry.getKey();
212             final List<String> paramValues = entry.getValue();
213             switch (uriName) {
214                 case InsertParam.uriName:
215                     final String instartStr = optionalParam(uriName, paramValues);
216                     if (instartStr != null) {
217                         insert = InsertParam.forUriValue(instartStr);
218                         if (insert == null) {
219                             throw new RestconfDocumentedException(
220                                 "Unrecognized insert parameter value '" + instartStr + "'", ErrorType.PROTOCOL,
221                                 ErrorTag.BAD_ELEMENT);
222                         }
223                     }
224                     break;
225                 case PointParam.uriName:
226                     final String pointStr = optionalParam(uriName, paramValues);
227                     if (pointStr != null) {
228                         point = PointParam.forUriValue(pointStr);
229                     }
230                     break;
231                 default:
232                     throw unhandledParam("write", uriName);
233             }
234         }
235
236         try {
237             return WriteDataParams.of(insert, point);
238         } catch (IllegalArgumentException e) {
239             throw new RestconfDocumentedException("Invalid query parameters: " + e.getMessage(), e);
240         }
241     }
242
243     private static RestconfDocumentedException unhandledParam(final String operation, final String name) {
244         return KNOWN_PARAMS.contains(name)
245             ? new RestconfDocumentedException("Invalid parameter in " + operation + ": " + name,
246                 ErrorType.PROTOCOL, ErrorTag.MALFORMED_MESSAGE)
247             : new RestconfDocumentedException("Unknown parameter in " + operation + ": " + name,
248                 ErrorType.PROTOCOL, ErrorTag.UNKNOWN_ATTRIBUTE);
249     }
250
251     @VisibleForTesting
252     static @Nullable String optionalParam(final String name, final List<String> values) {
253         switch (values.size()) {
254             case 0:
255                 return null;
256             case 1:
257                 return requireNonNull(values.get(0));
258             default:
259                 throw new RestconfDocumentedException("Parameter " + name + " can appear at most once in request URI",
260                     ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE);
261         }
262     }
263
264     private static <T> @Nullable T optionalParam(final Function<String, @NonNull T> factory, final String name,
265             final List<String> values) {
266         final String str = optionalParam(name, values);
267         return str == null ? null : factory.apply(str);
268     }
269 }