Split out restconf-api
[netconf.git] / protocol / restconf-api / src / main / java / org / opendaylight / restconf / api / ApiPath.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.api;
9
10 import static com.google.common.base.Verify.verifyNotNull;
11 import static java.util.Objects.requireNonNull;
12
13 import com.google.common.annotations.Beta;
14 import com.google.common.base.MoreObjects;
15 import com.google.common.base.MoreObjects.ToStringHelper;
16 import com.google.common.collect.ImmutableList;
17 import java.text.ParseException;
18 import java.util.Objects;
19 import org.eclipse.jdt.annotation.NonNullByDefault;
20 import org.eclipse.jdt.annotation.Nullable;
21 import org.opendaylight.yangtools.concepts.Immutable;
22 import org.opendaylight.yangtools.yang.common.UnresolvedQName;
23 import org.opendaylight.yangtools.yang.common.UnresolvedQName.Unqualified;
24
25 /**
26  * Intermediate representation of a parsed {@code api-path} string as defined in
27  * <a href="https://datatracker.ietf.org/doc/html/rfc8040#section-3.5.3.1">RFC section 3.5.3.1</a>. It models the
28  * path as a series of {@link Step}s.
29  */
30 @Beta
31 @NonNullByDefault
32 public final class ApiPath implements Immutable {
33     /**
34      * A single step in an {@link ApiPath}.
35      */
36     public abstract static class Step implements Immutable {
37         private final @Nullable String module;
38         private final Unqualified identifier;
39
40         Step(final @Nullable String module, final String identifier) {
41             this.identifier = verifyNotNull(UnresolvedQName.tryLocalName(identifier),
42                 "Unexpected invalid identifier %s", identifier);
43             this.module = module;
44         }
45
46         public Unqualified identifier() {
47             return identifier;
48         }
49
50         public @Nullable String module() {
51             return module;
52         }
53
54         @Override
55         public abstract int hashCode();
56
57         @Override
58         public abstract boolean equals(@Nullable Object obj);
59
60         final boolean equals(final Step other) {
61             return Objects.equals(module, other.module) && identifier.equals(other.identifier);
62         }
63
64         @Override
65         public final String toString() {
66             return addToStringAttributes(MoreObjects.toStringHelper(this).omitNullValues()).toString();
67         }
68
69         ToStringHelper addToStringAttributes(final ToStringHelper helper) {
70             return helper.add("module", module).add("identifier", identifier);
71         }
72     }
73
74     /**
75      * An {@code api-identifier} step in a {@link ApiPath}.
76      */
77     public static final class ApiIdentifier extends Step {
78         public ApiIdentifier(final @Nullable String module, final String identifier) {
79             super(module, identifier);
80         }
81
82         @Override
83         public int hashCode() {
84             return Objects.hash(module(), identifier());
85         }
86
87         @Override
88         public boolean equals(final @Nullable Object obj) {
89             return this == obj || obj instanceof ApiIdentifier other && equals(other);
90         }
91     }
92
93     /**
94      * A {@code list-instance} step in a {@link ApiPath}.
95      */
96     public static final class ListInstance extends Step {
97         private final ImmutableList<String> keyValues;
98
99         ListInstance(final @Nullable String module, final String identifier, final ImmutableList<String> keyValues) {
100             super(module, identifier);
101             this.keyValues = requireNonNull(keyValues);
102         }
103
104         public ImmutableList<String> keyValues() {
105             return keyValues;
106         }
107
108         @Override
109         public int hashCode() {
110             return Objects.hash(module(), identifier(), keyValues);
111         }
112
113         @Override
114         public boolean equals(final @Nullable Object obj) {
115             return this == obj || obj instanceof ListInstance other && equals(other)
116                 && keyValues.equals(other.keyValues);
117         }
118
119         @Override
120         ToStringHelper addToStringAttributes(final ToStringHelper helper) {
121             return super.addToStringAttributes(helper).add("keyValues", keyValues);
122         }
123     }
124
125     private static final ApiPath EMPTY = new ApiPath(ImmutableList.of());
126
127     private final ImmutableList<Step> steps;
128
129     private ApiPath(final ImmutableList<Step> steps) {
130         this.steps = requireNonNull(steps);
131     }
132
133     /**
134      * Return an empty ApiPath.
135      *
136      * @return An empty ApiPath.
137      */
138     public static ApiPath empty() {
139         return EMPTY;
140     }
141
142     /**
143      * Parse an {@link ApiPath} from a raw Request URI fragment or another source. The string is expected to contain
144      * percent-encoded bytes. Any sequence of such bytes is interpreted as a {@code UTF-8}-encoded string. Invalid
145      * sequences are rejected.
146      *
147      * @param str Request URI part
148      * @return An {@link ApiPath}
149      * @throws NullPointerException if {@code str} is {@code null}
150      * @throws ParseException if the string cannot be parsed
151      */
152     public static ApiPath parse(final String str) throws ParseException {
153         return str.isEmpty() ? EMPTY : parseString(ApiPathParser.newStrict(), str);
154     }
155
156     /**
157      * Parse an {@link ApiPath} from a raw Request URI fragment. The string is expected to contain percent-encoded
158      * bytes. Any sequence of such bytes is interpreted as a {@code UTF-8}-encoded string. Invalid sequences are
159      * rejected, but consecutive slashes may be tolerated, depending on runtime configuration.
160      *
161      * @param str Request URI part
162      * @return An {@link ApiPath}
163      * @throws NullPointerException if {@code str} is {@code null}
164      * @throws ParseException if the string cannot be parsed
165      */
166     public static ApiPath parseUrl(final String str) throws ParseException {
167         return str.isEmpty() ? EMPTY : parseString(ApiPathParser.newUrl(), str);
168     }
169
170     public ImmutableList<Step> steps() {
171         return steps;
172     }
173
174     public int indexOf(final String module, final String identifier) {
175         final var m = requireNonNull(module);
176         final var id = requireNonNull(identifier);
177         for (int i = 0, size = steps.size(); i < size; ++i) {
178             final var step = steps.get(i);
179             if (m.equals(step.module) && id.equals(step.identifier.getLocalName())) {
180                 return i;
181             }
182         }
183         return -1;
184     }
185
186     public ApiPath subPath(final int fromIndex) {
187         return subPath(fromIndex, steps.size());
188     }
189
190     public ApiPath subPath(final int fromIndex, final int toIndex) {
191         final var subList = steps.subList(fromIndex, toIndex);
192         if (subList == steps) {
193             return this;
194         } else if (subList.isEmpty()) {
195             return EMPTY;
196         } else {
197             return new ApiPath(subList);
198         }
199     }
200
201     @Override
202     public int hashCode() {
203         return steps.hashCode();
204     }
205
206     @Override
207     public boolean equals(final @Nullable Object obj) {
208         return obj == this || obj instanceof ApiPath other && steps.equals(other.steps());
209     }
210
211     private static ApiPath parseString(final ApiPathParser parser, final String str) throws ParseException {
212         final var steps = parser.parseSteps(str);
213         return steps.isEmpty() ? EMPTY : new ApiPath(steps);
214     }
215 }