/* * Copyright (c) 2015 Cisco Systems, Inc. and others. All rights reserved. * * This program and the accompanying materials are made available under the * terms of the Eclipse Public License v1.0 which accompanies this distribution, * and is available at http://www.eclipse.org/legal/epl-v10.html */ package org.opendaylight.yangtools.yang.data.util; import com.google.common.base.CharMatcher; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import java.util.ArrayList; import java.util.Collection; import java.util.List; import org.opendaylight.yangtools.concepts.Builder; import org.opendaylight.yangtools.yang.common.QName; import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifierWithPredicates; import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeWithValue; import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.PathArgument; /** * Iterator which lazily parses {@link PathArgument} from string representation. * * Note that invocation of {@link #hasNext()} or {@link #next()} may result in * throwing of {@link IllegalArgumentException} if underlying string representation * is not correctly serialized or does not represent instance identifier valid * for associated schema context. */ class XpathStringParsingPathArgumentBuilder implements Builder<Collection<PathArgument>> { /** * Matcher matching WSP YANG ABNF token */ private static final CharMatcher WSP = CharMatcher.anyOf(" \t"); /** * Matcher matching IDENTIFIER first char token. */ private static final CharMatcher IDENTIFIER_FIRST_CHAR = CharMatcher.inRange('a', 'z') .or(CharMatcher.inRange('A', 'Z')).or(CharMatcher.is('_')).precomputed(); /** * Matcher matching IDENTIFIER token */ private static final CharMatcher IDENTIFIER = IDENTIFIER_FIRST_CHAR.or(CharMatcher.inRange('0', '9')) .or(CharMatcher.anyOf(".-")).precomputed(); private static final CharMatcher QUOTE = CharMatcher.anyOf("'\""); private static final char SLASH = '/'; private static final char COLON = ':'; private static final char DOT = '.'; private static final char EQUALS = '='; private static final char PRECONDITION_START = '['; private static final char PRECONDITION_END = ']'; private final AbstractStringInstanceIdentifierCodec codec; private final String data; private final List<PathArgument> product = new ArrayList<>(); private DataSchemaContextNode<?> current; private int offset; XpathStringParsingPathArgumentBuilder(final AbstractStringInstanceIdentifierCodec codec, final String data) { this.codec = Preconditions.checkNotNull(codec); this.data = Preconditions.checkNotNull(data); this.current = codec.getDataContextTree().getRoot(); this.offset = 0; } @Override public Collection<PathArgument> build() { while (!allCharactersConsumed()) { product.add(computeNextArgument()); } return ImmutableList.copyOf(product); } private PathArgument computeNextArgument() { checkValid(SLASH == currentChar(), "Identifier must start with '/'."); skipCurrentChar(); checkValid(!allCharactersConsumed(), "Identifier cannot end with '/'."); QName name = nextQName(); if (allCharactersConsumed() || SLASH == currentChar()) { return computeIdentifier(name); } checkValid(PRECONDITION_START == currentChar(), "Last element must be identifier, predicate or '/'"); return computeIdentifierWithPredicate(name); } private DataSchemaContextNode<?> nextContextNode(final QName name) { current = current.getChild(name); checkValid(current != null, "%s is not correct schema node identifier.",name); while (current.isMixin()) { product.add(current.getIdentifier()); current = current.getChild(name); } return current; } /** * Creates path argument with predicates and sets offset * to end of path argument. * * {@code * predicate = "[" *WSP (predicate-expr / pos) *WSP "]" * predicate-expr = (node-identifier / ".") *WSP "=" *WSP * ((DQUOTE string DQUOTE) / * (SQUOTE string SQUOTE)) * pos = non-negative-integer-value * } * * @param name QName of node, for which predicates are computed. * @return PathArgument representing node selection with predictes */ private PathArgument computeIdentifierWithPredicate(final QName name) { DataSchemaContextNode<?> currentNode = nextContextNode(name); checkValid(currentNode.isKeyedEntry(), "Entry %s does not allow specifying predicates.", name); ImmutableMap.Builder<QName,Object> keyValues = ImmutableMap.builder(); while (!allCharactersConsumed() && PRECONDITION_START == currentChar()) { skipCurrentChar(); skipWhitespaces(); final QName key; if (DOT == currentChar()) { key = null; skipCurrentChar(); } else { key = nextQName(); } skipWhitespaces(); checkCurrentAndSkip(EQUALS, "Precondition must contain '='"); skipWhitespaces(); final String keyValue = nextQuotedValue(); skipWhitespaces(); checkCurrentAndSkip(PRECONDITION_END, "Precondition must ends with ']'"); // Break-out from method for leaf-list case if (key == null && currentNode.isLeaf()) { checkValid(offset == data.length(), "Leaf argument must be last argument of instance identifier."); return new NodeWithValue<>(name, keyValue); } final DataSchemaContextNode<?> keyNode = currentNode.getChild(key); checkValid(keyNode != null, "%s is not correct schema node identifier.", key); final Object value = codec.deserializeKeyValue(keyNode.getDataSchemaNode(), keyValue); keyValues.put(key, value); } return new NodeIdentifierWithPredicates(name, keyValues.build()); } private PathArgument computeIdentifier(final QName name) { DataSchemaContextNode<?> currentNode = nextContextNode(name); checkValid(!currentNode.isKeyedEntry(), "Entry %s requires key or value predicate to be present", name); return currentNode.getIdentifier(); } /** * Returns following QName and sets offset to end of QName. * * @return following QName. */ private QName nextQName() { // Consume prefix or identifier final String maybePrefix = nextIdentifier(); final String prefix; final String localName; if (COLON == currentChar()) { // previous token is prefix; prefix = maybePrefix; skipCurrentChar(); localName = nextIdentifier(); } else { prefix = ""; localName = maybePrefix; } return codec.createQName(prefix, localName); } /** * Returns true if all characters from input string * were consumed. * * @return true if all characters from input string * were consumed. */ private boolean allCharactersConsumed() { return offset == data.length(); } /** * Skips current char if it equals expected otherwise fails parsing. * * @param expected Expected character * @param errorMsg Error message if {@link #currentChar()} does not match expected. */ private void checkCurrentAndSkip(final char expected, final String errorMsg) { checkValid(expected == currentChar(), errorMsg); offset++; } /** * Fails parsing if a condition is not met. * * In case of error provides pointer to failed instance identifier, * offset on which failure occurred with explanation. * * @param condition Fails parsing if {@code condition} is false * @param errorMsg Error message which will be provided to user. * @param attributes */ private void checkValid(final boolean condition, final String errorMsg, final Object... attributes) { if (!condition) { throw new IllegalArgumentException(String.format( "Could not parse Instance Identifier '%s'. Offset: %s : Reason: %s", data, offset, String.format(errorMsg, attributes))); } } /** * Returns following value of quoted literal (without quotes) and sets offset after literal. * * @return String literal */ private String nextQuotedValue() { final char quoteChar = currentChar(); checkValid(QUOTE.matches(quoteChar), "Value must be qoute escaped with ''' or '\"'."); skipCurrentChar(); final int valueStart = offset; final int endQoute = data.indexOf(quoteChar, offset); final String value = data.substring(valueStart, endQoute); offset = endQoute; skipCurrentChar(); return value; } /** * Returns character at current offset. * * @return character at current offset. */ private char currentChar() { return data.charAt(offset); } /** * Increases processing offset by 1 */ private void skipCurrentChar() { offset++; } /** * Skip whitespace characters, sets offset to first following * non-whitespace character. */ private void skipWhitespaces() { nextSequenceEnd(WSP); } /** * Returns string which matches IDENTIFIER YANG ABNF token * and sets processing offset after end of identifier. * * @return string which matches IDENTIFIER YANG ABNF token */ private String nextIdentifier() { checkValid(IDENTIFIER_FIRST_CHAR.matches(currentChar()), "Identifier must start with character from set 'a-zA-Z_'"); final int start = offset; nextSequenceEnd(IDENTIFIER); return data.substring(start, offset); } private void nextSequenceEnd(final CharMatcher matcher) { while (!allCharactersConsumed() && matcher.matches(data.charAt(offset))) { offset++; } } }