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