Refactor pretty printing
[netconf.git] / restconf / restconf-nb / src / main / java / org / opendaylight / restconf / nb / rfc8040 / Insert.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;
9
10 import static java.util.Objects.requireNonNull;
11 import static org.opendaylight.restconf.server.api.EventStreamGetParams.mandatoryParam;
12
13 import com.google.common.annotations.Beta;
14 import com.google.common.base.MoreObjects;
15 import java.text.ParseException;
16 import java.util.Map;
17 import org.eclipse.jdt.annotation.NonNull;
18 import org.eclipse.jdt.annotation.NonNullByDefault;
19 import org.eclipse.jdt.annotation.Nullable;
20 import org.opendaylight.restconf.api.ApiPath;
21 import org.opendaylight.restconf.api.query.InsertParam;
22 import org.opendaylight.restconf.api.query.PointParam;
23 import org.opendaylight.restconf.server.api.DatabindContext;
24 import org.opendaylight.restconf.server.spi.ApiPathNormalizer;
25 import org.opendaylight.yangtools.concepts.Immutable;
26 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.PathArgument;
27
28 /**
29  * Parser and holder of query parameters from uriInfo for data and datastore modification operations.
30  */
31 // FIXME: Java 21: model this as a sealed interface and two records BeforeOrAfter and FirstOrLast, which further expose
32 //                 a boolean to differentiate which of the cases we are dealing with. This will allow users to use
33 //                 switch expression with record decomposition to safely dispatch execution. Only BeforeOrAfter will
34 //                 have a @NonNull PointParam then and there will not be an insert field. We can also ditch toString(),
35 //                 as the records will do the right thing.
36 public final class Insert implements Immutable {
37     @Beta
38     @NonNullByDefault
39     @FunctionalInterface
40     public interface PointNormalizer {
41
42         PathArgument normalizePoint(ApiPath value);
43     }
44
45     private final @NonNull InsertParam insert;
46     private final @Nullable PathArgument pointArg;
47
48     private Insert(final InsertParam insert, final PathArgument pointArg) {
49         this.insert = requireNonNull(insert);
50         this.pointArg = pointArg;
51     }
52
53     /**
54      * Return an {@link Insert} parameter for specified query parameters.
55      *
56      * @param queryParameters Parameters and their values
57      * @return An {@link Insert}, or {@code null} if no insert information is present
58      * @throws NullPointerException if any argument is {@code null}
59      * @throws IllegalArgumentException if the parameters are invalid
60      */
61     public static @Nullable Insert ofQueryParameters(final DatabindContext databind,
62             final Map<String, String> queryParameters) {
63         InsertParam insert = null;
64         PointParam point = null;
65
66         for (var entry : queryParameters.entrySet()) {
67             final var paramName = entry.getKey();
68             final var paramValue = entry.getValue();
69
70             switch (paramName) {
71                 case InsertParam.uriName:
72                     insert = mandatoryParam(InsertParam::forUriValue, paramName, paramValue);
73                     break;
74                 case PointParam.uriName:
75                     point = mandatoryParam(PointParam::forUriValue, paramName, paramValue);
76                     break;
77                 default:
78                     throw new IllegalArgumentException("Invalid parameter: " + paramName);
79             }
80         }
81
82         return Insert.forParams(insert, point, new ApiPathNormalizer(databind));
83     }
84
85     public static @Nullable Insert forParams(final @Nullable InsertParam insert, final @Nullable PointParam point,
86             final PointNormalizer pointParser) {
87         if (insert == null) {
88             if (point != null) {
89                 throw invalidPointIAE();
90             }
91             return null;
92         }
93
94         return switch (insert) {
95             case BEFORE, AFTER -> {
96                 // https://www.rfc-editor.org/rfc/rfc8040#section-4.8.5:
97                 //        If the values "before" or "after" are used, then a "point" query
98                 //        parameter for the "insert" query parameter MUST also be present, or a
99                 //        "400 Bad Request" status-line is returned.
100                 if (point == null) {
101                     throw new IllegalArgumentException(
102                         "Insert parameter " + insert.paramValue() + " cannot be used without a Point parameter.");
103                 }
104                 yield new Insert(insert, parsePoint(pointParser, point.value()));
105             }
106             case FIRST, LAST -> {
107                 // https://www.rfc-editor.org/rfc/rfc8040#section-4.8.6:
108                 // [when "point" parameter is present and]
109                 //        If the "insert" query parameter is not present or has a value other
110                 //        than "before" or "after", then a "400 Bad Request" status-line is
111                 //        returned.
112                 if (point != null) {
113                     throw invalidPointIAE();
114                 }
115                 yield new Insert(insert, null);
116             }
117         };
118     }
119
120     private static PathArgument parsePoint(final PointNormalizer pointParser, final String value) {
121         final ApiPath pointPath;
122         try {
123             pointPath = ApiPath.parse(value);
124         } catch (ParseException e) {
125             throw new IllegalArgumentException("Malformed point parameter '" + value + "': " + e.getMessage(), e);
126         }
127         return pointParser.normalizePoint(pointPath);
128     }
129
130     private static IllegalArgumentException invalidPointIAE() {
131         return new IllegalArgumentException(
132             "Point parameter can be used only with 'after' or 'before' values of Insert parameter.");
133     }
134
135     public @NonNull InsertParam insert() {
136         return insert;
137     }
138
139     public @Nullable PathArgument pointArg() {
140         return pointArg;
141     }
142
143     @Override
144     public String toString() {
145         final var helper = MoreObjects.toStringHelper(this).add("insert", insert.paramValue());
146         final var local = pointArg;
147         if (local != null) {
148             helper.add("point", pointArg);
149         }
150         return helper.toString();
151     }
152 }