}
return identifier.getMountPoint() != null
- ? QueryParameters.ofFieldPaths(params, parseFieldsPaths(identifier, fields.paramValue()))
- : QueryParameters.ofFields(params, parseFieldsParameter(identifier, fields.paramValue()));
+ ? QueryParameters.ofFieldPaths(params, parseFieldsPaths(identifier, fields))
+ : QueryParameters.ofFields(params, parseFieldsParameter(identifier, fields));
}
/**
import org.eclipse.jdt.annotation.Nullable;
import org.opendaylight.restconf.common.context.InstanceIdentifierContext;
import org.opendaylight.restconf.common.errors.RestconfDocumentedException;
+import org.opendaylight.restconf.nb.rfc8040.FieldsParam;
+import org.opendaylight.restconf.nb.rfc8040.FieldsParam.NodeSelector;
import org.opendaylight.yangtools.yang.common.ErrorTag;
import org.opendaylight.yangtools.yang.common.ErrorType;
import org.opendaylight.yangtools.yang.common.QName;
import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.PathArgument;
import org.opendaylight.yangtools.yang.data.util.DataSchemaContextNode;
import org.opendaylight.yangtools.yang.model.api.DataSchemaNode;
+import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext;
import org.opendaylight.yangtools.yang.model.api.LeafListSchemaNode;
import org.opendaylight.yangtools.yang.model.api.ListSchemaNode;
-import org.opendaylight.yangtools.yang.model.api.SchemaContext;
/**
* Utilities used for parsing of fields query parameter content.
* @return {@link List} of levels; each level contains set of {@link QName}
*/
public static @NonNull List<Set<QName>> parseFieldsParameter(final @NonNull InstanceIdentifierContext<?> identifier,
- final @NonNull String input) {
+ final @NonNull FieldsParam input) {
return QNAME_PARSER.parseFields(identifier, input);
}
* of provided {@code identifier}
*/
public static @NonNull List<YangInstanceIdentifier> parseFieldsPaths(
- final @NonNull InstanceIdentifierContext<?> identifier, final @NonNull String input) {
+ final @NonNull InstanceIdentifierContext<?> identifier, final @NonNull FieldsParam input) {
final List<Set<LinkedPathElement>> levels = PATH_PARSER.parseFields(identifier, input);
final List<Map<PathArgument, LinkedPathElement>> mappedLevels = mapLevelsContentByIdentifiers(levels);
return buildPaths(mappedLevels);
* @return {@link List} of levels; each level contains {@link Set} of identifiers of type {@link T}
*/
private @NonNull List<Set<T>> parseFields(final @NonNull InstanceIdentifierContext<?> identifier,
- final @NonNull String input) {
- final List<Set<T>> parsed = new ArrayList<>();
- final SchemaContext context = identifier.getSchemaContext();
- final QNameModule startQNameModule = identifier.getSchemaNode().getQName().getModule();
+ final @NonNull FieldsParam input) {
final DataSchemaContextNode<?> startNode = DataSchemaContextNode.fromDataSchemaNode(
(DataSchemaNode) identifier.getSchemaNode());
"Start node missing in " + input, ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE);
}
- parseInput(input, startQNameModule, startNode, parsed, context);
+ final List<Set<T>> parsed = new ArrayList<>();
+ processSelectors(parsed, identifier.getSchemaContext(), identifier.getSchemaNode().getQName().getModule(),
+ startNode, input.nodeSelectors());
return parsed;
}
- /**
- * Parse input value of fields parameter and create list of sets. Each set represents one level of child nodes.
- *
- * @param input input value of fields parameter
- * @param startQNameModule starting qname module
- * @param startNode starting node
- * @param parsed list of results
- * @param context schema context
- */
- private void parseInput(final @NonNull String input, final @NonNull QNameModule startQNameModule,
- final @NonNull DataSchemaContextNode<?> startNode,
- final @NonNull List<Set<T>> parsed, final SchemaContext context) {
+ private void processSelectors(final List<Set<T>> parsed, final EffectiveModelContext context,
+ final QNameModule startNamespace, final DataSchemaContextNode<?> startNode,
+ final List<NodeSelector> selectors) {
final Set<T> startLevel = new HashSet<>();
parsed.add(startLevel);
- int currentPosition = 0;
- int startPosition = 0;
- DataSchemaContextNode<?> currentNode = startNode;
- QNameModule currentQNameModule = startQNameModule;
- Set<T> currentLevel = startLevel;
-
- while (currentPosition < input.length()) {
- final char currentChar = input.charAt(currentPosition);
-
- if (ParserConstants.YANG_IDENTIFIER_PART.matches(currentChar)) {
- currentPosition++;
- continue;
- }
-
- switch (currentChar) {
- case '/':
- // add parsed identifier to results for current level
- currentNode = addChildToResult(currentNode, input.substring(startPosition, currentPosition),
- currentQNameModule, currentLevel);
- // go one level down
- currentLevel = prepareQNameLevel(parsed, currentLevel);
+ for (var selector : selectors) {
+ var node = startNode;
+ var namespace = startNamespace;
+ var level = startLevel;
+
+
+ // Note: path is guaranteed to have at least one step
+ final var it = selector.path().iterator();
+ while (true) {
+ // FIXME: The layout of this loop is rather weird, which is due to how prepareQNameLevel() operates. We
+ // need to call it only when we know there is another identifier coming, otherwise we would end
+ // up with empty levels sneaking into the mix.
+ //
+ // Dealing with that weirdness requires understanding what the expected end results are and a
+ // larger rewrite of the algorithms involved.
+ final var step = it.next();
+ final var module = step.module();
+ if (module != null) {
+ // FIXME: this is not defensive enough, as we can fail to find the module
+ namespace = context.findModules(module).iterator().next().getQNameModule();
+ }
- currentPosition++;
- break;
- case ':':
- // new namespace and revision found
- currentQNameModule = context.findModules(
- input.substring(startPosition, currentPosition)).iterator().next().getQNameModule();
- currentPosition++;
+ // add parsed identifier to results for current level
+ node = addChildToResult(node, step.identifier().bindTo(namespace), level);
+ if (!it.hasNext()) {
break;
- case '(':
- // add current child to parsed results for current level
- final DataSchemaContextNode<?> child = addChildToResult(
- currentNode,
- input.substring(startPosition, currentPosition), currentQNameModule, currentLevel);
- // call with child node as new start node for one level down
- final int closingParenthesis = currentPosition
- + findClosingParenthesis(input.substring(currentPosition + 1));
- parseInput(
- input.substring(currentPosition + 1, closingParenthesis),
- currentQNameModule,
- child,
- parsed,
- context);
-
- // closing parenthesis must be at the end of input or separator and one more character is expected
- currentPosition = closingParenthesis + 1;
- if (currentPosition != input.length()) {
- if (currentPosition + 1 < input.length()) {
- if (input.charAt(currentPosition) == ';') {
- currentPosition++;
- } else {
- throw new RestconfDocumentedException(
- "Missing semicolon character after "
- + child.getIdentifier().getNodeType().getLocalName()
- + " child nodes",
- ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE);
- }
- } else {
- throw new RestconfDocumentedException(
- "Unexpected character '"
- + input.charAt(currentPosition)
- + "' found in fields parameter value",
- ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE);
- }
- }
+ }
- break;
- case ';':
- // complete identifier found
- addChildToResult(
- currentNode,
- input.substring(startPosition, currentPosition), currentQNameModule, currentLevel);
- currentPosition++;
-
- // next nodes can be placed on already utilized level-s
- currentNode = startNode;
- currentQNameModule = startQNameModule;
- currentLevel = startLevel;
- break;
- default:
- throw new RestconfDocumentedException(
- "Unexpected character '" + currentChar + "' found in fields parameter value",
- ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE);
+ // go one level down
+ level = prepareQNameLevel(parsed, level);
}
- startPosition = currentPosition;
- }
-
- // parse input to end
- if (startPosition < input.length()) {
- addChildToResult(currentNode, input.substring(startPosition), currentQNameModule, currentLevel);
+ final var subs = selector.subSelectors();
+ if (!subs.isEmpty()) {
+ processSelectors(parsed, context, namespace, node, subs);
+ }
}
}
return nextLevel;
}
- /**
- * Find position of matching parenthesis increased by one, but at most equals to input size.
- *
- * @param input input where to find for closing parenthesis
- * @return int position of closing parenthesis increased by one
- */
- private static int findClosingParenthesis(final @NonNull String input) {
- int position = 0;
- int count = 1;
-
- while (position < input.length()) {
- final char currentChar = input.charAt(position);
-
- if (currentChar == '(') {
- count++;
- }
-
- if (currentChar == ')') {
- count--;
- }
-
- if (count == 0) {
- break;
- }
-
- position++;
- }
-
- // closing parenthesis was not found
- if (position >= input.length()) {
- throw new RestconfDocumentedException("Missing closing parenthesis in fields parameter",
- ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE);
- }
-
- return ++position;
- }
-
/**
* Add parsed child of current node to result for current level.
*
abstract @NonNull DataSchemaContextNode<?> addChildToResult(@NonNull DataSchemaContextNode<?> currentNode,
@NonNull QName childQName, @NonNull Set<T> level);
- private @NonNull DataSchemaContextNode<?> addChildToResult(final @NonNull DataSchemaContextNode<?> currentNode,
- final @NonNull String localName, final @NonNull QNameModule namespace, final @NonNull Set<T> level) {
- return addChildToResult(currentNode, QName.create(namespace, localName), level);
- }
-
/**
* Fields parser that stores set of {@link QName}s in each level. Because of this fact, from the output
* it is is only possible to assume on what depth the selected element is placed. Identifiers of intermediary
assertInvalidFields("a:.", "Expecting [a-ZA-Z_], not '.'", 2);
assertInvalidFields("a:b+", "Expecting [a-zA-Z_.-/(:;], not '+'", 3);
assertInvalidFields("a;)", "Expecting [a-ZA-Z_], not ')'", 2);
+ assertInvalidFields("*", "Expecting [a-ZA-Z_], not '*'", 0);
}
@Test
assertInvalidFields("a;", "Unexpected end of input", 2);
assertInvalidFields("a(", "Unexpected end of input", 2);
assertInvalidFields("a(a", "Unexpected end of input", 3);
+ assertInvalidFields("library(", "Unexpected end of input", 8);
+ assertInvalidFields("library(album);", "Unexpected end of input", 15);
}
@Test
public void testUnexpectedRightParent() {
assertInvalidFields("a)", "Expecting ';', not ')'", 1);
+ assertInvalidFields("library(album)player", "Expecting ';', not 'p'", 14);
}
private static void assertInvalidFields(final String str, final String message, final int errorOffset) {
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.when;
+import java.text.ParseException;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import org.mockito.junit.MockitoJUnitRunner;
import org.opendaylight.restconf.common.context.InstanceIdentifierContext;
import org.opendaylight.restconf.common.errors.RestconfDocumentedException;
+import org.opendaylight.restconf.nb.rfc8040.FieldsParam;
import org.opendaylight.restconf.nb.rfc8040.TestRestconfUtils;
import org.opendaylight.yangtools.yang.common.ErrorTag;
import org.opendaylight.yangtools.yang.common.ErrorType;
*/
@Test
public void parseFieldsParameterSimplePathTest() {
- final String input = "library";
- final List<Set<QName>> parsedFields = ParserFieldsParameter.parseFieldsParameter(identifierJukebox, input);
+ final List<Set<QName>> parsedFields = assertFieldsParameter(identifierJukebox, "library");
assertNotNull(parsedFields);
assertEquals(1, parsedFields.size());
*/
@Test
public void parseFieldsParameterDoublePathTest() {
- final String input = "library;player";
- final List<Set<QName>> parsedFields = ParserFieldsParameter.parseFieldsParameter(identifierJukebox, input);
+ final List<Set<QName>> parsedFields = assertFieldsParameter(identifierJukebox, "library;player");
assertNotNull(parsedFields);
assertEquals(1, parsedFields.size());
*/
@Test
public void parseFieldsParameterSubPathTest() {
- final String input = "library/album/name";
- final List<Set<QName>> parsedFields = ParserFieldsParameter.parseFieldsParameter(identifierJukebox, input);
+ final List<Set<QName>> parsedFields = assertFieldsParameter(identifierJukebox, "library/album/name");
assertNotNull(parsedFields);
assertEquals(3, parsedFields.size());
*/
@Test
public void parseFieldsParameterChildrenPathTest() {
- final String input = "library(album(name))";
- final List<Set<QName>> parsedFields = ParserFieldsParameter.parseFieldsParameter(identifierJukebox, input);
+ final List<Set<QName>> parsedFields = assertFieldsParameter(identifierJukebox, "library(album(name))");
assertNotNull(parsedFields);
assertEquals(3, parsedFields.size());
*/
@Test
public void parseFieldsParameterNamespaceTest() {
- final String input = "augmented-jukebox:augmented-library";
- final List<Set<QName>> parsedFields = ParserFieldsParameter.parseFieldsParameter(identifierJukebox, input);
+ final List<Set<QName>> parsedFields = assertFieldsParameter(identifierJukebox,
+ "augmented-jukebox:augmented-library");
assertNotNull(parsedFields);
assertEquals(1, parsedFields.size());
*/
@Test
public void parseFieldsParameterWithMultipleChildrenTest1() {
- final String input = "services(type-of-service;instance/instance-name;instance/provider)";
- final List<Set<QName>> parsedFields = ParserFieldsParameter.parseFieldsParameter(identifierTestServices, input);
+ final List<Set<QName>> parsedFields = assertFieldsParameter(identifierTestServices,
+ "services(type-of-service;instance/instance-name;instance/provider)");
assertNotNull(parsedFields);
assertEquals(parsedFields.size(), 3);
*/
@Test
public void parseFieldsParameterWithMultipleChildrenTest2() {
- final String input = "services(type-of-service;instance(instance-name;provider))";
- final List<Set<QName>> parsedFields = ParserFieldsParameter.parseFieldsParameter(identifierTestServices, input);
+ final List<Set<QName>> parsedFields = assertFieldsParameter(identifierTestServices,
+ "services(type-of-service;instance(instance-name;provider))");
assertNotNull(parsedFields);
assertEquals(parsedFields.size(), 3);
*/
@Test
public void parseFieldsParameterWithMultipleChildrenTest3() {
- final String input = "services(instance/instance-name;type-of-service;next-data/next-service)";
- final List<Set<QName>> parsedFields = ParserFieldsParameter.parseFieldsParameter(identifierTestServices, input);
+ final List<Set<QName>> parsedFields = assertFieldsParameter(identifierTestServices,
+ "services(instance/instance-name;type-of-service;next-data/next-service)");
assertNotNull(parsedFields);
assertEquals(parsedFields.size(), 3);
List.of(INSTANCE_NAME_Q_NAME, NEXT_SERVICE_Q_NAME)));
}
- /**
- * Test parse fields parameter containing not expected character.
- */
- @Test
- public void parseFieldsParameterNotExpectedCharacterNegativeTest() {
- final RestconfDocumentedException ex = assertThrows(RestconfDocumentedException.class,
- () -> ParserFieldsParameter.parseFieldsParameter(identifierJukebox, "*"));
- // Bad request
- assertEquals("Error type is not correct", ErrorType.PROTOCOL, ex.getErrors().get(0).getErrorType());
- assertEquals("Error tag is not correct", ErrorTag.INVALID_VALUE, ex.getErrors().get(0).getErrorTag());
- }
-
- /**
- * Test parse fields parameter with missing closing parenthesis.
- */
- @Test
- public void parseFieldsParameterMissingParenthesisNegativeTest() {
- final String input = "library(";
-
- final RestconfDocumentedException ex = assertThrows(RestconfDocumentedException.class,
- () -> ParserFieldsParameter.parseFieldsParameter(identifierJukebox, input));
- // Bad request
- assertEquals("Error type is not correct", ErrorType.PROTOCOL, ex.getErrors().get(0).getErrorType());
- assertEquals("Error tag is not correct", ErrorTag.INVALID_VALUE, ex.getErrors().get(0).getErrorTag());
- }
-
/**
* Test parse fields parameter when not existing child node selected.
*/
@Test
- public void parseFieldsParameterMissingChildNodeNegativeTest() {
- final String input = "library(not-existing)";
-
- final RestconfDocumentedException ex = assertThrows(RestconfDocumentedException.class,
- () -> ParserFieldsParameter.parseFieldsParameter(identifierJukebox, input));
- // Bad request
- assertEquals("Error type is not correct", ErrorType.PROTOCOL, ex.getErrors().get(0).getErrorType());
- assertEquals("Error tag is not correct", ErrorTag.INVALID_VALUE, ex.getErrors().get(0).getErrorTag());
- }
-
- /**
- * Test parse fields parameter with unexpected character after parenthesis.
- */
- @Test
- public void parseFieldsParameterAfterParenthesisNegativeTest() {
- final String input = "library(album);";
+ public void parseFieldsParameterMissingChildNodeNegativeTest() throws ParseException {
+ final FieldsParam input = FieldsParam.parse("library(not-existing)");
final RestconfDocumentedException ex = assertThrows(RestconfDocumentedException.class,
() -> ParserFieldsParameter.parseFieldsParameter(identifierJukebox, input));
assertEquals("Error tag is not correct", ErrorTag.INVALID_VALUE, ex.getErrors().get(0).getErrorTag());
}
- /**
- * Test parse fields parameter with missing semicolon after parenthesis.
- */
- @Test
- public void parseFieldsParameterMissingSemicolonNegativeTest() {
- final String input = "library(album)player";
-
- final RestconfDocumentedException ex = assertThrows(RestconfDocumentedException.class,
- () -> ParserFieldsParameter.parseFieldsParameter(this.identifierJukebox, input));
- // Bad request
- assertEquals("Error type is not correct", ErrorType.PROTOCOL, ex.getErrors().get(0).getErrorType());
- assertEquals("Error tag is not correct", ErrorTag.INVALID_VALUE, ex.getErrors().get(0).getErrorTag());
- }
-
@Test
public void parseTopLevelContainerToPathTest() {
- final String input = "library";
- final List<YangInstanceIdentifier> parsedFields = ParserFieldsParameter.parseFieldsPaths(
- identifierJukebox, input);
+ final List<YangInstanceIdentifier> parsedFields = assertFieldsPaths(identifierJukebox, "library");
assertNotNull(parsedFields);
assertEquals(1, parsedFields.size());
@Test
public void parseTwoTopLevelContainersToPathsTest() {
final String input = "library;player";
- final List<YangInstanceIdentifier> parsedFields = ParserFieldsParameter.parseFieldsPaths(
- identifierJukebox, input);
+ final List<YangInstanceIdentifier> parsedFields = assertFieldsPaths(identifierJukebox, input);
assertNotNull(parsedFields);
assertEquals(2, parsedFields.size());
@Test
public void parseNestedLeafToPathTest() {
- final String input = "library/album/name";
- final List<YangInstanceIdentifier> parsedFields = ParserFieldsParameter.parseFieldsPaths(
- identifierJukebox, input);
+ final List<YangInstanceIdentifier> parsedFields = assertFieldsPaths(identifierJukebox, "library/album/name");
assertEquals(1, parsedFields.size());
final List<PathArgument> pathArguments = parsedFields.get(0).getPathArguments();
@Test
public void parseAugmentedLeafToPathTest() {
- final String input = "player/augmented-jukebox:speed";
- final List<YangInstanceIdentifier> parsedFields = ParserFieldsParameter.parseFieldsPaths(
- identifierJukebox, input);
+ final List<YangInstanceIdentifier> parsedFields = assertFieldsPaths(identifierJukebox,
+ "player/augmented-jukebox:speed");
assertEquals(1, parsedFields.size());
final List<PathArgument> pathArguments = parsedFields.get(0).getPathArguments();
@Test
public void parseMultipleFieldsOnDifferentLevelsToPathsTest() {
- final String input = "services(type-of-service;instance/instance-name;instance/provider)";
- final List<YangInstanceIdentifier> parsedFields = ParserFieldsParameter.parseFieldsPaths(
- identifierTestServices, input);
+ final List<YangInstanceIdentifier> parsedFields = assertFieldsPaths(identifierTestServices,
+ "services(type-of-service;instance/instance-name;instance/provider)");
assertEquals(3, parsedFields.size());
@Test
public void parseListFieldUnderListToPathTest() {
- final String input = "services/instance";
- final List<YangInstanceIdentifier> parsedFields = ParserFieldsParameter.parseFieldsPaths(
- identifierTestServices, input);
+ final List<YangInstanceIdentifier> parsedFields = assertFieldsPaths(identifierTestServices,
+ "services/instance");
assertEquals(1, parsedFields.size());
final List<PathArgument> pathArguments = parsedFields.get(0).getPathArguments();
@Test
public void parseLeafListFieldToPathTest() {
- final String input = "protocols";
- final List<YangInstanceIdentifier> parsedFields = ParserFieldsParameter.parseFieldsPaths(
- identifierTestServices, input);
+ final List<YangInstanceIdentifier> parsedFields = assertFieldsPaths(identifierTestServices, "protocols");
assertEquals(1, parsedFields.size());
final List<PathArgument> pathArguments = parsedFields.get(0).getPathArguments();
.filter(path -> lastPathArg.equals(path.getLastPathArgument().getNodeType()))
.findAny();
}
+
+ private static List<Set<QName>> assertFieldsParameter(final InstanceIdentifierContext<?> identifier,
+ final String input) {
+ return ParserFieldsParameter.parseFieldsParameter(identifier, assertFields(input));
+ }
+
+ private static List<YangInstanceIdentifier> assertFieldsPaths(final InstanceIdentifierContext<?> identifier,
+ final String input) {
+ return ParserFieldsParameter.parseFieldsPaths(identifier, assertFields(input));
+ }
+
+ private static FieldsParam assertFields(final String input) {
+ try {
+ return FieldsParam.parse(input);
+ } catch (ParseException e) {
+ throw new AssertionError(e);
+ }
+ }
}
\ No newline at end of file