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