X-Git-Url: https://git.opendaylight.org/gerrit/gitweb?a=blobdiff_plain;f=restconf%2Frestconf-nb-rfc8040%2Fsrc%2Fmain%2Fjava%2Forg%2Fopendaylight%2Frestconf%2Fnb%2Frfc8040%2Futils%2Fparser%2FYangInstanceIdentifierDeserializer.java;h=9f73482f41aa55cc19fa14d575e6b422a1e61283;hb=bbbd3c4e766d31d63446f43e40545cd0758dab08;hp=004a99d6166d42082f40267652ecc7570575d677;hpb=4ce087c00ded24533b40629c6ee9ced42e4a5e07;p=netconf.git diff --git a/restconf/restconf-nb-rfc8040/src/main/java/org/opendaylight/restconf/nb/rfc8040/utils/parser/YangInstanceIdentifierDeserializer.java b/restconf/restconf-nb-rfc8040/src/main/java/org/opendaylight/restconf/nb/rfc8040/utils/parser/YangInstanceIdentifierDeserializer.java index 004a99d616..9f73482f41 100644 --- a/restconf/restconf-nb-rfc8040/src/main/java/org/opendaylight/restconf/nb/rfc8040/utils/parser/YangInstanceIdentifierDeserializer.java +++ b/restconf/restconf-nb-rfc8040/src/main/java/org/opendaylight/restconf/nb/rfc8040/utils/parser/YangInstanceIdentifierDeserializer.java @@ -7,44 +7,43 @@ */ package org.opendaylight.restconf.nb.rfc8040.utils.parser; +import static com.google.common.base.Verify.verify; +import static com.google.common.base.Verify.verifyNotNull; import static java.util.Objects.requireNonNull; -import com.google.common.base.CharMatcher; -import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; -import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import java.text.ParseException; import java.util.ArrayList; -import java.util.Iterator; import java.util.List; -import java.util.Optional; +import org.eclipse.jdt.annotation.NonNull; import org.opendaylight.restconf.common.errors.RestconfDocumentedException; -import org.opendaylight.restconf.common.util.RestUtil; -import org.opendaylight.restconf.common.util.RestconfSchemaUtil; -import org.opendaylight.restconf.nb.rfc8040.codecs.RestCodec; +import org.opendaylight.restconf.nb.rfc8040.ApiPath; +import org.opendaylight.restconf.nb.rfc8040.ApiPath.ListInstance; 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.common.QNameModule; import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier; import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifier; +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; -import org.opendaylight.yangtools.yang.data.util.DataSchemaContextNode; +import org.opendaylight.yangtools.yang.data.impl.codec.TypeDefinitionAwareCodec; import org.opendaylight.yangtools.yang.data.util.DataSchemaContextTree; -import org.opendaylight.yangtools.yang.model.api.ActionDefinition; import org.opendaylight.yangtools.yang.model.api.ActionNodeContainer; -import org.opendaylight.yangtools.yang.model.api.ContainerLike; -import org.opendaylight.yangtools.yang.model.api.DataNodeContainer; import org.opendaylight.yangtools.yang.model.api.DataSchemaNode; import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext; -import org.opendaylight.yangtools.yang.model.api.IdentitySchemaNode; import org.opendaylight.yangtools.yang.model.api.LeafListSchemaNode; import org.opendaylight.yangtools.yang.model.api.LeafSchemaNode; import org.opendaylight.yangtools.yang.model.api.ListSchemaNode; -import org.opendaylight.yangtools.yang.model.api.Module; import org.opendaylight.yangtools.yang.model.api.RpcDefinition; import org.opendaylight.yangtools.yang.model.api.SchemaContext; import org.opendaylight.yangtools.yang.model.api.SchemaNode; import org.opendaylight.yangtools.yang.model.api.TypeDefinition; +import org.opendaylight.yangtools.yang.model.api.stmt.IdentityEffectiveStatement; +import org.opendaylight.yangtools.yang.model.api.stmt.RpcEffectiveStatement; import org.opendaylight.yangtools.yang.model.api.type.IdentityrefTypeDefinition; +import org.opendaylight.yangtools.yang.model.api.type.InstanceIdentifierTypeDefinition; import org.opendaylight.yangtools.yang.model.api.type.LeafrefTypeDefinition; import org.opendaylight.yangtools.yang.model.util.SchemaInferenceStack; @@ -52,384 +51,270 @@ import org.opendaylight.yangtools.yang.model.util.SchemaInferenceStack; * Deserializer for {@link String} to {@link YangInstanceIdentifier} for restconf. */ public final class YangInstanceIdentifierDeserializer { - private static final CharMatcher IDENTIFIER_PREDICATE = - CharMatcher.noneOf(ParserConstants.RFC3986_RESERVED_CHARACTERS).precomputed(); - private static final CharMatcher PERCENT_ENCODING = CharMatcher.is('%'); - // position of the first encoded char after percent sign in percent encoded string - private static final int FIRST_ENCODED_CHAR = 1; - // position of the last encoded char after percent sign in percent encoded string - private static final int LAST_ENCODED_CHAR = 3; - // percent encoded radix for parsing integers - private static final int PERCENT_ENCODED_RADIX = 16; - - private final EffectiveModelContext schemaContext; - private final String data; - - private DataSchemaContextNode current; - private int offset; - - private YangInstanceIdentifierDeserializer(final EffectiveModelContext schemaContext, final String data) { + public static final class Result { + public final @NonNull YangInstanceIdentifier path; + public final @NonNull SchemaInferenceStack stack; + public final @NonNull SchemaNode node; + + Result(final EffectiveModelContext context) { + path = YangInstanceIdentifier.empty(); + node = requireNonNull(context); + stack = SchemaInferenceStack.of(context); + } + + Result(final EffectiveModelContext context, final QName qname) { + // Legacy behavior: RPCs do not really have a YangInstanceIdentifier, but the rest of the code expects it + path = YangInstanceIdentifier.of(qname); + stack = SchemaInferenceStack.of(context); + + final var stmt = stack.enterSchemaTree(qname); + verify(stmt instanceof RpcDefinition, "Unexpected statement %s", stmt); + node = (RpcDefinition) stmt; + } + + Result(final List steps, final SchemaInferenceStack stack, final SchemaNode node) { + path = YangInstanceIdentifier.create(steps); + this.stack = requireNonNull(stack); + this.node = requireNonNull(node); + } + } + + private final @NonNull EffectiveModelContext schemaContext; + private final @NonNull ApiPath apiPath; + + private YangInstanceIdentifierDeserializer(final EffectiveModelContext schemaContext, final ApiPath apiPath) { this.schemaContext = requireNonNull(schemaContext); - this.data = requireNonNull(data); - current = DataSchemaContextTree.from(schemaContext).getRoot(); + this.apiPath = requireNonNull(apiPath); } /** - * Method to create {@link Iterable} from {@link PathArgument} which are parsing from data by {@link SchemaContext}. + * Method to create {@link List} from {@link PathArgument} which are parsing from data by {@link SchemaContext}. * * @param schemaContext for validate of parsing path arguments * @param data path to data, in URL string form * @return {@link Iterable} of {@link PathArgument} + * @throws RestconfDocumentedException the path is not valid */ - public static List create(final EffectiveModelContext schemaContext, final String data) { - return new YangInstanceIdentifierDeserializer(schemaContext, data).parse(); - } - - private List parse() { - final List path = new ArrayList<>(); - - while (!allCharsConsumed()) { - validArg(); - final QName qname = prepareQName(); - - // this is the last identifier (input is consumed) or end of identifier (slash) - if (allCharsConsumed() || currentChar() == '/') { - prepareIdentifier(qname, path); - path.add(current == null ? NodeIdentifier.create(qname) : current.getIdentifier()); - } else if (currentChar() == '=') { - if (nextContextNode(qname, path).getDataSchemaNode() instanceof ListSchemaNode) { - prepareNodeWithPredicates(qname, path, (ListSchemaNode) current.getDataSchemaNode()); - } else { - prepareNodeWithValue(qname, path); - } - } else { - throw getParsingCharFailedException(); - } + public static Result create(final EffectiveModelContext schemaContext, final String data) { + final ApiPath path; + try { + path = ApiPath.parse(requireNonNull(data)); + } catch (ParseException e) { + throw new RestconfDocumentedException("Invalid path '" + data + "' at offset " + e.getErrorOffset(), + ErrorType.PROTOCOL, ErrorTag.MALFORMED_MESSAGE, e); } - - return ImmutableList.copyOf(path); + return create(schemaContext, path); } - private void prepareNodeWithPredicates(final QName qname, final List path, - final ListSchemaNode listSchemaNode) { - checkValid(listSchemaNode != null, ErrorTag.MALFORMED_MESSAGE, "Data schema node is null"); - - final Iterator keys = listSchemaNode.getKeyDefinition().iterator(); - final ImmutableMap.Builder values = ImmutableMap.builder(); - - // skip already expected equal sign - skipCurrentChar(); + public static Result create(final EffectiveModelContext schemaContext, final ApiPath path) { + return new YangInstanceIdentifierDeserializer(schemaContext, path).parse(); + } - // read key value separated by comma - while (keys.hasNext() && !allCharsConsumed() && currentChar() != '/') { + // FIXME: NETCONF-818: this method really needs to report an Inference and optionally a YangInstanceIdentifier + // - we need the inference for discerning the correct context + // - RPCs do not have a YangInstanceIdentifier + // - Actions always have a YangInstanceIdentifier, but it points to their parent + // - we need to discern the cases RPC invocation, Action invocation and data tree access quickly + // + // All of this really is an utter mess because we end up calling into this code from various places which, + // for example, should not allow RPCs to be valid targets + private Result parse() { + final var it = apiPath.steps().iterator(); + if (!it.hasNext()) { + return new Result(schemaContext); + } - // empty key value - if (currentChar() == ',') { - values.put(keys.next(), ""); - skipCurrentChar(); - continue; + // First step is somewhat special: + // - it has to contain a module qualifier + // - it has to consider RPCs, for which we need SchemaContext + // + // We therefore peel that first iteration here and not worry about those details in further iterations + var step = it.next(); + final var firstModule = RestconfDocumentedException.throwIfNull(step.module(), + ErrorType.PROTOCOL, ErrorTag.MALFORMED_MESSAGE, + "First member must use namespace-qualified form, '%s' does not", step.identifier()); + var namespace = resolveNamespace(firstModule); + var qname = step.identifier().bindTo(namespace); + + // We go through more modern APIs here to get this special out of the way quickly + final var optRpc = schemaContext.findModuleStatement(namespace).orElseThrow() + .findSchemaTreeNode(RpcEffectiveStatement.class, qname); + if (optRpc.isPresent()) { + // We have found an RPC match, + if (it.hasNext()) { + throw new RestconfDocumentedException("First step in the path resolves to RPC '" + qname + "' and " + + "therefore it must be the only step present", ErrorType.PROTOCOL, ErrorTag.MALFORMED_MESSAGE); } - - // check if next value is parsable - checkValid(IDENTIFIER_PREDICATE.matches(currentChar()), ErrorTag.MALFORMED_MESSAGE, - "Value that starts with character %c is not parsable.", currentChar()); - - // parse value - final QName key = keys.next(); - Optional leafSchemaNode = listSchemaNode.findDataChildByName(key); - RestconfDocumentedException.throwIf(leafSchemaNode.isEmpty(), ErrorType.PROTOCOL, ErrorTag.BAD_ELEMENT, - "Schema not found for %s", key); - - final String value = findAndParsePercentEncoded(nextIdentifierFromNextSequence(IDENTIFIER_PREDICATE)); - final Object valueByType = prepareValueByType(leafSchemaNode.get(), value); - values.put(key, valueByType); - - // skip comma - if (keys.hasNext() && !allCharsConsumed() && currentChar() == ',') { - skipCurrentChar(); + if (step instanceof ListInstance) { + throw new RestconfDocumentedException("First step in the path resolves to RPC '" + qname + "' and " + + "therefore it must not contain key values", ErrorType.PROTOCOL, ErrorTag.MALFORMED_MESSAGE); } - } - // the last key is considered to be empty - if (keys.hasNext()) { - // at this point, it must be true that current char is '/' or all chars have already been consumed - values.put(keys.next(), ""); - - // there should be no more missing keys - RestconfDocumentedException.throwIf(keys.hasNext(), ErrorType.PROTOCOL, ErrorTag.MISSING_ATTRIBUTE, - "Cannot parse input identifier '%s'. Key value is missing for QName: %s", data, qname); + return new Result(schemaContext, optRpc.orElseThrow().argument()); } - path.add(YangInstanceIdentifier.NodeIdentifierWithPredicates.of(qname, values.build())); - } - - private Object prepareValueByType(final DataSchemaNode schemaNode, final String value) { - Object decoded; - - TypeDefinition> typedef; - if (schemaNode instanceof LeafListSchemaNode) { - typedef = ((LeafListSchemaNode) schemaNode).getType(); - } else { - typedef = ((LeafSchemaNode) schemaNode).getType(); - } - final TypeDefinition baseType = RestUtil.resolveBaseTypeFrom(typedef); - if (baseType instanceof LeafrefTypeDefinition) { - typedef = SchemaInferenceStack.ofInstantiatedPath(schemaContext, schemaNode.getPath()) - .resolveLeafref((LeafrefTypeDefinition) baseType); - } - decoded = RestCodec.from(typedef, null, schemaContext).deserialize(value); - if (decoded == null && typedef instanceof IdentityrefTypeDefinition) { - decoded = toIdentityrefQName(value, schemaNode); - } - return decoded; - } - - private QName prepareQName() { - checkValidIdentifierStart(); - final String preparedPrefix = nextIdentifierFromNextSequence(ParserConstants.YANG_IDENTIFIER_PART); - final String prefix; - final String localName; + final var stack = SchemaInferenceStack.of(schemaContext); + final var path = new ArrayList(); + final SchemaNode node; - if (allCharsConsumed()) { - return getQNameOfDataSchemaNode(preparedPrefix); - } + var parentNode = DataSchemaContextTree.from(schemaContext).getRoot(); + while (true) { + final var parentSchema = parentNode.getDataSchemaNode(); + if (parentSchema instanceof ActionNodeContainer) { + final var optAction = ((ActionNodeContainer) parentSchema).findAction(qname); + if (optAction.isPresent()) { + if (it.hasNext()) { + throw new RestconfDocumentedException("Request path resolves to action '" + qname + "' and " + + "therefore it must not continue past it", ErrorType.PROTOCOL, ErrorTag.MALFORMED_MESSAGE); + } + if (step instanceof ListInstance) { + throw new RestconfDocumentedException("Request path resolves to action '" + qname + "' and " + + "therefore it must not contain key values", + ErrorType.PROTOCOL, ErrorTag.MALFORMED_MESSAGE); + } - switch (currentChar()) { - case '/': - case '=': - prefix = preparedPrefix; - return getQNameOfDataSchemaNode(prefix); - case ':': - prefix = preparedPrefix; - skipCurrentChar(); - checkValidIdentifierStart(); - localName = nextIdentifierFromNextSequence(ParserConstants.YANG_IDENTIFIER_PART); - - if (!allCharsConsumed() && currentChar() == '=') { - return getQNameOfDataSchemaNode(localName); - } else { - final Module module = moduleForPrefix(prefix); - RestconfDocumentedException.throwIf(module == null, ErrorType.PROTOCOL, ErrorTag.UNKNOWN_ELEMENT, - "Failed to lookup for module with name '%s'.", prefix); - return QName.create(module.getQNameModule(), localName); + // Legacy behavior: Action's path should not include its path, but the rest of the code expects it + path.add(new NodeIdentifier(qname)); + stack.enterSchemaTree(qname); + node = optAction.orElseThrow(); + break; } - default: - throw getParsingCharFailedException(); - } - } + } - private void prepareNodeWithValue(final QName qname, final List path) { - skipCurrentChar(); - final String value = nextIdentifierFromNextSequence(IDENTIFIER_PREDICATE); + // Resolve the child step with respect to data schema tree + final var found = RestconfDocumentedException.throwIfNull(parentNode.enterChild(stack, qname), + ErrorType.PROTOCOL, ErrorTag.DATA_MISSING, "Schema for '%s' not found", qname); - // exception if value attribute is missing - RestconfDocumentedException.throwIf(value.isEmpty(), ErrorType.PROTOCOL, ErrorTag.MISSING_ATTRIBUTE, - "Cannot parse input identifier '%s' - value is missing for QName: %s.", data, qname); - final DataSchemaNode dataSchemaNode = current.getDataSchemaNode(); - final Object valueByType = prepareValueByType(dataSchemaNode, findAndParsePercentEncoded(value)); - path.add(new YangInstanceIdentifier.NodeWithValue<>(qname, valueByType)); - } + // Now add all mixins encountered to the path + var childNode = found; + while (childNode.isMixin()) { + path.add(childNode.getIdentifier()); + childNode = verifyNotNull(childNode.enterChild(stack, qname), + "Mixin %s is missing child for %s while resolving %s", childNode, qname, found); + } - private void prepareIdentifier(final QName qname, final List path) { - final DataSchemaContextNode currentNode = nextContextNode(qname, path); - if (currentNode != null) { - checkValid(!currentNode.isKeyedEntry(), ErrorTag.MISSING_ATTRIBUTE, + final PathArgument pathArg; + if (step instanceof ListInstance) { + final var values = ((ListInstance) step).keyValues(); + final var schema = childNode.getDataSchemaNode(); + pathArg = schema instanceof ListSchemaNode + ? prepareNodeWithPredicates(stack, qname, (ListSchemaNode) schema, values) + : prepareNodeWithValue(stack, qname, schema, values); + } else { + RestconfDocumentedException.throwIf(childNode.isKeyedEntry(), + ErrorType.PROTOCOL, ErrorTag.MISSING_ATTRIBUTE, "Entry '%s' requires key or value predicate to be present.", qname); - } - } - - @SuppressFBWarnings(value = "NP_NULL_ON_SOME_PATH", - justification = "code does check for null 'current' but FB doesn't recognize it") - private DataSchemaContextNode nextContextNode(final QName qname, final List path) { - final DataSchemaContextNode initialContext = current; - final DataSchemaNode initialDataSchema = initialContext.getDataSchemaNode(); + pathArg = childNode.getIdentifier(); + } - current = initialContext.getChild(qname); + path.add(pathArg); - if (current == null) { - final Optional module = schemaContext.findModule(qname.getModule()); - if (module.isPresent()) { - for (final RpcDefinition rpcDefinition : module.get().getRpcs()) { - if (rpcDefinition.getQName().getLocalName().equals(qname.getLocalName())) { - return null; - } - } + if (!it.hasNext()) { + node = childNode.getDataSchemaNode(); + break; } - if (findActionDefinition(initialDataSchema, qname.getLocalName()).isPresent()) { - return null; + + parentNode = childNode; + step = it.next(); + final var module = step.module(); + if (module != null) { + namespace = resolveNamespace(module); } - } - checkValid(current != null, ErrorTag.MALFORMED_MESSAGE, "'%s' is not correct schema node identifier.", qname); - while (current.isMixin()) { - path.add(current.getIdentifier()); - current = current.getChild(qname); - } - return current; - } - private Module moduleForPrefix(final String prefix) { - return schemaContext.findModules(prefix).stream().findFirst().orElse(null); - } + qname = step.identifier().bindTo(namespace); + } - private boolean allCharsConsumed() { - return offset == data.length(); + return new Result(path, stack, node); } - private void checkValid(final boolean condition, final ErrorTag errorTag, final String errorMsg) { - if (!condition) { - throw createParsingException(errorTag, errorMsg); + private NodeIdentifierWithPredicates prepareNodeWithPredicates(final SchemaInferenceStack stack, final QName qname, + final @NonNull ListSchemaNode schema, final List<@NonNull String> keyValues) { + final var keyDef = schema.getKeyDefinition(); + final var keySize = keyDef.size(); + final var varSize = keyValues.size(); + if (keySize != varSize) { + throw new RestconfDocumentedException( + "Schema for " + qname + " requires " + keySize + " key values, " + varSize + " supplied", + ErrorType.PROTOCOL, keySize > varSize ? ErrorTag.MISSING_ATTRIBUTE : ErrorTag.UNKNOWN_ATTRIBUTE); } - } - private void checkValid(final boolean condition, final ErrorTag errorTag, final String fmt, final Object arg) { - if (!condition) { - throw createParsingException(errorTag, String.format(fmt, arg)); + final var values = ImmutableMap.builderWithExpectedSize(keySize); + final var tmp = stack.copy(); + for (int i = 0; i < keySize; ++i) { + final QName keyName = keyDef.get(i); + final var child = schema.getDataChildByName(keyName); + tmp.enterSchemaTree(keyName); + values.put(keyName, prepareValueByType(tmp, child, keyValues.get(i))); + tmp.exit(); } - } - - private void checkValidIdentifierStart() { - checkValid(ParserConstants.YANG_IDENTIFIER_START.matches(currentChar()), ErrorTag.MALFORMED_MESSAGE, - "Identifier must start with character from set 'a-zA-Z_'"); - } - - private RestconfDocumentedException getParsingCharFailedException() { - return createParsingException(ErrorTag.MALFORMED_MESSAGE, - "Bad char '" + currentChar() + "' on the current position."); - } - - private RestconfDocumentedException createParsingException(final ErrorTag errorTag, final String messagePart) { - return new RestconfDocumentedException(String.format( - "Could not parse Instance Identifier '%s'. Offset: '%d' : Reason: %s", data, offset, messagePart), - ErrorType.PROTOCOL, errorTag); - } - private char currentChar() { - return data.charAt(offset); + return NodeIdentifierWithPredicates.of(qname, values.build()); } - private void skipCurrentChar() { - offset++; - } + private Object prepareValueByType(final SchemaInferenceStack stack, final DataSchemaNode schemaNode, + final @NonNull String value) { - private String nextIdentifierFromNextSequence(final CharMatcher matcher) { - final int start = offset; - while (!allCharsConsumed() && matcher.matches(currentChar())) { - skipCurrentChar(); + TypeDefinition> typedef; + if (schemaNode instanceof LeafListSchemaNode) { + typedef = ((LeafListSchemaNode) schemaNode).getType(); + } else { + typedef = ((LeafSchemaNode) schemaNode).getType(); } - return data.substring(start, offset); - } - - private void validArg() { - // every identifier except of the first MUST start with slash - if (offset != 0) { - checkValid('/' == currentChar(), ErrorTag.MALFORMED_MESSAGE, "Identifier must start with '/'."); - - // skip consecutive slashes, users often assume restconf URLs behave just as HTTP does by squashing - // multiple slashes into a single one - while (!allCharsConsumed() && '/' == currentChar()) { - skipCurrentChar(); - } - - // check if slash is not also the last char in identifier - checkValid(!allCharsConsumed(), ErrorTag.MALFORMED_MESSAGE, "Identifier cannot end with '/'."); + if (typedef instanceof LeafrefTypeDefinition) { + typedef = stack.resolveLeafref((LeafrefTypeDefinition) typedef); } - } - private QName getQNameOfDataSchemaNode(final String nodeName) { - final DataSchemaNode dataSchemaNode = current.getDataSchemaNode(); - if (dataSchemaNode instanceof ContainerLike) { - return getQNameOfDataSchemaNode((ContainerLike) dataSchemaNode, nodeName); - } else if (dataSchemaNode instanceof ListSchemaNode) { - return getQNameOfDataSchemaNode((ListSchemaNode) dataSchemaNode, nodeName); + if (typedef instanceof IdentityrefTypeDefinition) { + return toIdentityrefQName(value, schemaNode); } - throw new UnsupportedOperationException("Unsupported schema node " + dataSchemaNode); - } - - private static QName getQNameOfDataSchemaNode( - final T parent, final String nodeName) { - final Optional actionDef = findActionDefinition(parent, nodeName); - final SchemaNode node; - if (actionDef.isPresent()) { - node = actionDef.get(); - } else { - node = RestconfSchemaUtil.findSchemaNodeInCollection(parent.getChildNodes(), nodeName); - } - return node.getQName(); - } + try { + if (typedef instanceof InstanceIdentifierTypeDefinition) { + return new StringModuleInstanceIdentifierCodec(schemaContext).deserialize(value); + } - private static Optional findActionDefinition(final SchemaNode dataSchemaNode, - final String nodeName) { - requireNonNull(dataSchemaNode, "DataSchema Node must not be null."); - if (dataSchemaNode instanceof ActionNodeContainer) { - return ((ActionNodeContainer) dataSchemaNode).getActions().stream() - .filter(actionDef -> actionDef.getQName().getLocalName().equals(nodeName)).findFirst(); + return verifyNotNull(TypeDefinitionAwareCodec.from(typedef), + "Unhandled type %s decoding %s", typedef, value).deserialize(value); + } catch (IllegalArgumentException e) { + throw new RestconfDocumentedException("Invalid value '" + value + "' for " + schemaNode.getQName(), + ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE, e); } - return Optional.empty(); } - private static String findAndParsePercentEncoded(final String preparedPrefix) { - if (preparedPrefix.indexOf('%') == -1) { - return preparedPrefix; - } - - // FIXME: this is extremely inefficient: we should be converting ranges of characters, not driven by - // CharMatcher, but by String.indexOf() - final StringBuilder parsedPrefix = new StringBuilder(preparedPrefix); - while (PERCENT_ENCODING.matchesAnyOf(parsedPrefix)) { - final int percentCharPosition = PERCENT_ENCODING.indexIn(parsedPrefix); - parsedPrefix.replace(percentCharPosition, percentCharPosition + LAST_ENCODED_CHAR, - String.valueOf((char) Integer.parseInt(parsedPrefix.substring( - percentCharPosition + FIRST_ENCODED_CHAR, percentCharPosition + LAST_ENCODED_CHAR), - PERCENT_ENCODED_RADIX))); - } - - return parsedPrefix.toString(); + private NodeWithValue prepareNodeWithValue(final SchemaInferenceStack stack, final QName qname, + final DataSchemaNode schema, final List keyValues) { + // TODO: qname should be always equal to schema.getQName(), right? + return new NodeWithValue<>(qname, prepareValueByType(stack, schema, + // FIXME: ahem: we probably want to do something differently here + keyValues.get(0))); } private QName toIdentityrefQName(final String value, final DataSchemaNode schemaNode) { - final String moduleName = toModuleName(value); - final String nodeName = toNodeName(value); - final Iterator modulesIterator = schemaContext.findModules(moduleName).iterator(); - if (!modulesIterator.hasNext()) { - throw new RestconfDocumentedException(String.format("Cannot decode value '%s' for identityref type " - + "in %s. Make sure reserved characters such as comma, single-quote, double-quote, colon," - + " double-quote, space, and forward slash (,'\":\" /) are percent-encoded," - + " for example ':' is '%%3A'", value, current.getIdentifier().getNodeType()), - ErrorType.PROTOCOL, ErrorTag.BAD_ELEMENT); - } - for (final IdentitySchemaNode identitySchemaNode : modulesIterator.next().getIdentities()) { - final QName qName = identitySchemaNode.getQName(); - if (qName.getLocalName().equals(nodeName)) { - return qName; - } - } - return QName.create(schemaNode.getQName().getNamespace(), schemaNode.getQName().getRevision(), nodeName); - } - - private static String toNodeName(final String str) { - final int idx = str.indexOf(':'); - if (idx == -1) { - return str; - } - - if (str.indexOf(':', idx + 1) != -1) { - return str; + final QNameModule namespace; + final String localName; + final int firstColon = value.indexOf(':'); + if (firstColon != -1) { + namespace = resolveNamespace(value.substring(0, firstColon)); + localName = value.substring(firstColon + 1); + } else { + namespace = schemaNode.getQName().getModule(); + localName = value; } - return str.substring(idx + 1); + return schemaContext.getModuleStatement(namespace) + .streamEffectiveSubstatements(IdentityEffectiveStatement.class) + .map(IdentityEffectiveStatement::argument) + .filter(qname -> localName.equals(qname.getLocalName())) + .findFirst() + .orElseThrow(() -> new RestconfDocumentedException( + "No identity found for '" + localName + "' in namespace " + namespace, + ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE)); } - private static String toModuleName(final String str) { - final int idx = str.indexOf(':'); - if (idx == -1) { - return null; - } - - if (str.indexOf(':', idx + 1) != -1) { - return null; - } - - return str.substring(0, idx); + private @NonNull QNameModule resolveNamespace(final String moduleName) { + final var modules = schemaContext.findModules(moduleName); + RestconfDocumentedException.throwIf(modules.isEmpty(), ErrorType.PROTOCOL, ErrorTag.UNKNOWN_ELEMENT, + "Failed to lookup for module with name '%s'.", moduleName); + return modules.iterator().next().getQNameModule(); } }