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