eb840e58783a515b1ab1e5b3e9dd92bf47eaa046
[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 com.google.common.collect.ImmutableMap;
15 import java.util.List;
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.errors.RestconfDocumentedException;
36 import org.opendaylight.restconf.common.errors.RestconfError;
37 import org.opendaylight.restconf.nb.rfc8040.ReadDataParams;
38 import org.opendaylight.restconf.nb.rfc8040.legacy.InstanceIdentifierContext;
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         // ODL extensions
56         LeafNodesOnlyParam.uriName, SkipNotificationDataParam.uriName, ChangedLeafNodesOnlyParam.uriName,
57         ChildNodesOnlyParam.uriName);
58
59     private QueryParams() {
60         // Utility class
61     }
62
63     /**
64      * Normalize query parameters from an {@link UriInfo}.
65      *
66      * @param uriInfo An {@link UriInfo}
67      * @return Normalized query parameters
68      * @throws NullPointerException if {@code uriInfo} is {@code null}
69      * @throws IllegalArgumentException if there are multiple values for a parameter
70      */
71     public static @NonNull ImmutableMap<String, String> normalize(final UriInfo uriInfo) {
72         final var builder = ImmutableMap.<String, String>builder();
73         for (var entry : uriInfo.getQueryParameters().entrySet()) {
74             final var values = entry.getValue();
75             switch (values.size()) {
76                 case 0:
77                     // No-op
78                     break;
79                 case 1:
80                     builder.put(entry.getKey(), values.get(0));
81                     break;
82                 default:
83                     throw new IllegalArgumentException(
84                         "Parameter " + entry.getKey() + " can appear at most once in request URI");
85             }
86         }
87         return builder.build();
88     }
89
90     public static QueryParameters newQueryParameters(final ReadDataParams params,
91             final InstanceIdentifierContext identifier) {
92         final var fields = params.fields();
93         if (fields == null) {
94             return QueryParameters.of(params);
95         }
96
97         return identifier.getMountPoint() != null
98             ? QueryParameters.ofFieldPaths(params, NetconfFieldsTranslator.translate(identifier, fields))
99                 : QueryParameters.ofFields(params, WriterFieldsTranslator.translate(identifier, fields));
100     }
101
102     /**
103      * Parse parameters from URI request and check their types and values.
104      *
105      * @param uriInfo    URI info
106      * @return {@link ReadDataParams}
107      */
108     public static @NonNull ReadDataParams newReadDataParams(final UriInfo uriInfo) {
109         ContentParam content = ContentParam.ALL;
110         DepthParam depth = null;
111         FieldsParam fields = null;
112         WithDefaultsParam withDefaults = null;
113         PrettyPrintParam prettyPrint = null;
114
115         for (var entry : uriInfo.getQueryParameters().entrySet()) {
116             final var paramName = entry.getKey();
117             final var paramValues = entry.getValue();
118
119             try {
120                 switch (paramName) {
121                     case ContentParam.uriName:
122                         content = optionalParam(ContentParam::forUriValue, paramName, paramValues);
123                         break;
124                     case DepthParam.uriName:
125                         final String depthStr = optionalParam(paramName, paramValues);
126                         try {
127                             depth = DepthParam.forUriValue(depthStr);
128                         } catch (IllegalArgumentException e) {
129                             throw new RestconfDocumentedException(e, new RestconfError(ErrorType.PROTOCOL,
130                                 ErrorTag.INVALID_VALUE, "Invalid depth parameter: " + depthStr, null,
131                                 "The depth parameter must be an integer between 1 and 65535 or \"unbounded\""));
132                         }
133                         break;
134                     case FieldsParam.uriName:
135                         fields = optionalParam(FieldsParam::forUriValue, paramName, paramValues);
136                         break;
137                     case WithDefaultsParam.uriName:
138                         withDefaults = optionalParam(WithDefaultsParam::forUriValue, paramName, paramValues);
139                         break;
140                     case PrettyPrintParam.uriName:
141                         prettyPrint = optionalParam(PrettyPrintParam::forUriValue, paramName, paramValues);
142                         break;
143                     default:
144                         throw unhandledParam("read", paramName);
145                 }
146             } catch (IllegalArgumentException e) {
147                 throw new RestconfDocumentedException("Invalid " + paramName + " value: " + e.getMessage(),
148                     ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE, e);
149             }
150         }
151
152         return new ReadDataParams(content, depth, fields, withDefaults, prettyPrint);
153     }
154
155     private static RestconfDocumentedException unhandledParam(final String operation, final String name) {
156         return KNOWN_PARAMS.contains(name)
157             ? new RestconfDocumentedException("Invalid parameter in " + operation + ": " + name,
158                 ErrorType.PROTOCOL, ErrorTag.MALFORMED_MESSAGE)
159             : new RestconfDocumentedException("Unknown parameter in " + operation + ": " + name,
160                 ErrorType.PROTOCOL, ErrorTag.UNKNOWN_ATTRIBUTE);
161     }
162
163     @VisibleForTesting
164     static @Nullable String optionalParam(final String name, final List<String> values) {
165         return switch (values.size()) {
166             case 0 -> null;
167             case 1 -> requireNonNull(values.get(0));
168             default -> throw new RestconfDocumentedException(
169                 "Parameter " + name + " can appear at most once in request URI",
170                 ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE);
171         };
172     }
173
174     private static <T> @Nullable T optionalParam(final Function<String, @NonNull T> factory, final String name,
175             final List<String> values) {
176         final String str = optionalParam(name, values);
177         return str == null ? null : factory.apply(str);
178     }
179 }