2 * Copyright (c) 2021 PANTHEON.tech, s.r.o. and others. All rights reserved.
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
8 package org.opendaylight.restconf.nb.rfc8040;
10 import com.google.common.collect.ImmutableList;
11 import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
12 import java.text.ParseException;
13 import java.util.ArrayDeque;
14 import java.util.ArrayList;
15 import java.util.Deque;
16 import java.util.List;
17 import org.eclipse.jdt.annotation.NonNull;
18 import org.opendaylight.restconf.nb.rfc8040.ApiPath.ApiIdentifier;
19 import org.opendaylight.restconf.nb.rfc8040.FieldsParameter.NodeSelector;
20 import org.opendaylight.yangtools.yang.common.YangNames;
23 * Stateful parser for {@link FieldsParameter}. This is not as hard as IETF's ABNF would lead you to believe. The
24 * original definition is:
26 * fields-expr = path "(" fields-expr ")" / path ";" fields-expr / path
27 * path = api-identifier [ "/" path ]
29 * To make some sense of this, let's express the same constructs in a more powerful ANTLR4 grammar.
32 * {@code path} is a rather simple
34 * path = api-identifier ("/" api-identifier)*
36 * which is to say a {@code path} is "a sequence of one or more api-identifiers, separated by slashes". This boils in
37 * turn down to a list {@link ApiIdentifier}s, which is guaranteed to have at least one item.
40 * {@code fields-expr} can be rewritten as three distinct possibilities:
42 * fields-expr : path "(" fields-expr ")"
43 * | path ";" fields-expr
46 * which makes it clear it is a recursive structure, where the parentheses part is sub-filters and ';' serves as
47 * concatenation. So let's rewrite that by folding the common part and use optional elements and introducing proper
48 * names for constructs
50 * fields : node-selectors EOF
51 * node-selectors : node-selector (";" node-selector)*
52 * node-selector : path sub-selectors?
53 * sub-selectors : "(" node-selectors ")"
54 * path : api-identifier ("/" api-identifier)*
58 * That ANTLR4 grammar dictates the layout of {@link FieldsParameter}. It also shows the parsing is recursive on
59 * {@code node-selectors}, which is what {@link #parse(String)} and
60 * {@link NodeSelectorParser#parseSubSelectors(String, int)} deal with.
62 final class FieldsParameterParser {
63 // Lazily instantiated queue for reuse of parser when we encounter sub-selectors. We could just rely on JIT/GC
64 // dealing with allocation rate, but we should be ready to see malicious inputs. One example of that is
65 // multiple nested sub-selectors like "a(b(c(d)));e(f(g(h)));i(j(k(l)))" With this cache we end allocating only four
66 // parsers instead of ten.
67 private Deque<NodeSelectorParser> parsers;
69 @NonNull ImmutableList<NodeSelector> parseNodeSelectors(final String str) throws ParseException {
70 final var nodeSelectors = ImmutableList.<NodeSelector>builder();
73 final var parser = new NodeSelectorParser();
75 final int next = parser.fillFrom(str, idx);
76 nodeSelectors.add(parser.collectAndReset());
78 if (next == str.length()) {
79 // We have reached the end, we are done
80 return nodeSelectors.build();
83 final char ch = str.charAt(next);
85 throw new ParseException("Expecting ';', not '" + ch + "'", next);
91 @SuppressFBWarnings(value = "UPM_UNCALLED_PRIVATE_METHOD",
92 justification = "https://github.com/spotbugs/spotbugs/issues/811")
93 private @NonNull NodeSelectorParser getParser() {
94 final var local = parsers;
96 final var existing = local.poll();
97 if (existing != null) {
101 return new NodeSelectorParser();
104 @SuppressFBWarnings(value = "UPM_UNCALLED_PRIVATE_METHOD",
105 justification = "https://github.com/spotbugs/spotbugs/issues/811")
106 private void putParser(final NodeSelectorParser parser) {
109 // Let's be conservative with memory allocation
110 parsers = local = new ArrayDeque<>(2);
115 @SuppressFBWarnings(value = "UPM_UNCALLED_PRIVATE_METHOD",
116 justification = "https://github.com/spotbugs/spotbugs/issues/811")
117 private static void expectIdentifierStart(final String str, final int offset) throws ParseException {
118 final char ch = charAt(str, offset);
119 if (!YangNames.IDENTIFIER_START.matches(ch)) {
120 throw new ParseException("Expecting [a-ZA-Z_], not '" + ch + "'", offset);
124 @SuppressFBWarnings(value = "UPM_UNCALLED_PRIVATE_METHOD",
125 justification = "https://github.com/spotbugs/spotbugs/issues/811")
126 private static char charAt(final String str, final int offset) throws ParseException {
127 if (str.length() == offset) {
128 throw new ParseException("Unexpected end of input", offset);
130 return str.charAt(offset);
133 // A note here: we could store 'str' either in this object, or FieldsParameterParser, but that makes it a bit
134 // removed via indirection. We are opting for explicit argument passing to ensure JIT sees it as a local variable
135 // along with offset.
136 private final class NodeSelectorParser {
137 private final List<ApiIdentifier> path = new ArrayList<>(4);
139 // Not that common: lazily instantiated
140 private List<NodeSelector> selectors;
142 int fillFrom(final String str, final int offset) throws ParseException {
143 return parsePathStepFirst(str, offset);
146 @NonNull NodeSelector collectAndReset() {
147 final ImmutableList<ApiIdentifier> collectedPath = ImmutableList.copyOf(path);
150 final ImmutableList<NodeSelector> collectedSelectors;
151 if (selectors != null && !selectors.isEmpty()) {
152 collectedSelectors = ImmutableList.copyOf(selectors);
155 collectedSelectors = ImmutableList.of();
158 return new NodeSelector(collectedPath, collectedSelectors);
161 // We are at the start of a step in path. We are dealing with the first part of
162 // identifier (":" identifier)?
163 // but are mindful of the big picture
164 private int parsePathStepFirst(final String str, final int offset) throws ParseException {
165 expectIdentifierStart(str, offset);
167 int idx = offset + 1;
169 if (idx == str.length()) {
170 path.add(new ApiIdentifier(null, str.substring(offset)));
174 final char ch = str.charAt(idx);
175 if (!YangNames.NOT_IDENTIFIER_PART.matches(ch)) {
180 final String first = str.substring(offset, idx);
182 // We have complete first identifier, now switch to parsing the second identifier
183 return parsePathStepSecond(first, str, idx + 1);
185 path.add(new ApiIdentifier(null, first));
190 // End of this selector, return
194 return parsePathStepFirst(str, idx + 1);
196 // Process at least one sub-selector
197 return parseSubSelectors(str, idx + 1);
199 throw new ParseException("Expecting [a-zA-Z_.-/(:;], not '" + ch + "'", idx);
204 // We are at the second identifier of a step in path, we already have the first identifier from
205 // identifier (":" identifier)?
206 // but are mindful of the big picture
207 private int parsePathStepSecond(final String module, final String str, final int offset) throws ParseException {
208 expectIdentifierStart(str, offset);
210 int idx = offset + 1;
212 if (idx == str.length()) {
213 path.add(new ApiIdentifier(module, str.substring(offset)));
217 final char ch = str.charAt(idx);
218 if (!YangNames.NOT_IDENTIFIER_PART.matches(ch)) {
222 path.add(new ApiIdentifier(module, str.substring(offset, idx)));
227 // End of this selector, return
231 return parsePathStepFirst(str, idx + 1);
233 // Process at least one sub-selector
234 return parseSubSelectors(str, idx + 1);
236 throw new ParseException("Expecting [a-zA-Z_.-/(:;], not '" + ch + "'", idx);
241 // We are dealing with sub-selectors here
242 private int parseSubSelectors(final String str, final int offset) throws ParseException {
243 var local = selectors;
245 selectors = local = new ArrayList<>(4);
249 final var parser = getParser();
251 final int next = parser.fillFrom(str, idx);
252 local.add(parser.collectAndReset());
254 final char ch = charAt(str, next);
257 // Another sub-selector
261 // End of these sub-selectors, return the parser for reuse
265 throw new ParseException("Expecting [;)], not '" + ch + "'", next);