2 * Copyright (c) 2024 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.server.spi;
10 import static java.util.Objects.requireNonNull;
12 import com.google.common.base.VerifyException;
13 import com.google.common.collect.ImmutableList;
14 import org.eclipse.jdt.annotation.NonNull;
15 import org.opendaylight.restconf.api.ApiPath;
16 import org.opendaylight.restconf.api.ApiPath.ApiIdentifier;
17 import org.opendaylight.restconf.api.ApiPath.ListInstance;
18 import org.opendaylight.restconf.api.ApiPath.Step;
19 import org.opendaylight.restconf.common.errors.RestconfDocumentedException;
20 import org.opendaylight.restconf.server.api.DatabindContext;
21 import org.opendaylight.yangtools.yang.common.ErrorTag;
22 import org.opendaylight.yangtools.yang.common.ErrorType;
23 import org.opendaylight.yangtools.yang.common.QName;
24 import org.opendaylight.yangtools.yang.common.QNameModule;
25 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
26 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifier;
27 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifierWithPredicates;
28 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeWithValue;
29 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.PathArgument;
30 import org.opendaylight.yangtools.yang.data.codec.gson.JSONCodec;
31 import org.opendaylight.yangtools.yang.data.util.DataSchemaContext;
32 import org.opendaylight.yangtools.yang.data.util.DataSchemaContext.Composite;
33 import org.opendaylight.yangtools.yang.data.util.DataSchemaContext.PathMixin;
34 import org.opendaylight.yangtools.yang.model.api.LeafListSchemaNode;
35 import org.opendaylight.yangtools.yang.model.api.LeafSchemaNode;
36 import org.opendaylight.yangtools.yang.model.api.ListSchemaNode;
37 import org.opendaylight.yangtools.yang.model.api.TypedDataSchemaNode;
38 import org.opendaylight.yangtools.yang.model.util.SchemaInferenceStack;
41 * Utility class for turning {@link YangInstanceIdentifier}s into {@link ApiPath}s via
42 * {@link #dataToApiPath(YangInstanceIdentifier)}.
44 public final class ApiPathCanonizer {
45 private final @NonNull DatabindContext databind;
47 public ApiPathCanonizer(final DatabindContext databind) {
48 this.databind = requireNonNull(databind);
52 * Return the canonical {@link ApiPath} for specified {@link YangInstanceIdentifier}.
54 * @param path {@link YangInstanceIdentifier} to canonicalize
55 * @return An {@link ApiPath}
57 public @NonNull ApiPath dataToApiPath(final YangInstanceIdentifier path) {
58 final var it = path.getPathArguments().iterator();
60 return ApiPath.empty();
63 final var stack = SchemaInferenceStack.of(databind.modelContext());
64 final var builder = ImmutableList.<Step>builder();
65 DataSchemaContext context = databind.schemaTree().getRoot();
66 QNameModule parentModule = null;
68 final var arg = it.next();
70 // get module of the parent
71 if (!(context instanceof PathMixin)) {
72 parentModule = context.dataSchemaNode().getQName().getModule();
75 final var childContext = context instanceof Composite composite ? composite.enterChild(stack, arg) : null;
76 if (childContext == null) {
77 throw new RestconfDocumentedException(
78 "Invalid input '%s': schema for argument '%s' (after '%s') not found".formatted(path, arg,
79 ApiPath.of(builder.build())), ErrorType.APPLICATION, ErrorTag.UNKNOWN_ELEMENT);
82 context = childContext;
84 // only output PathArguments which are not inherent to YangInstanceIdentifier structure
85 if (!(childContext instanceof PathMixin)) {
86 builder.add(argToStep(arg, parentModule, stack, context));
88 } while (it.hasNext());
90 return new ApiPath(builder.build());
93 private @NonNull Step argToStep(final PathArgument arg, final QNameModule prevNamespace,
94 final SchemaInferenceStack stack, final DataSchemaContext context) {
95 // append namespace before every node which is defined in other module than its parent
96 // condition is satisfied also for the first path argument
97 final var nodeType = arg.getNodeType();
98 final var module = nodeType.getModule().equals(prevNamespace) ? null : resolvePrefix(nodeType);
99 final var identifier = nodeType.unbind();
101 // NodeIdentifier maps to an ApiIdentifier
102 if (arg instanceof NodeIdentifier) {
103 return new ApiIdentifier(module, identifier);
106 // NodeWithValue addresses a LeafSetEntryNode and maps to a ListInstance with a single value
107 final var schema = context.dataSchemaNode();
108 if (arg instanceof NodeWithValue<?> withValue) {
109 if (!(schema instanceof LeafListSchemaNode leafList)) {
110 throw new RestconfDocumentedException(
111 "Argument '%s' does not map to a leaf-list, but %s".formatted(arg, schema),
112 ErrorType.APPLICATION, ErrorTag.INVALID_VALUE);
114 return ListInstance.of(module, identifier, valueToString(stack, leafList, withValue.getValue()));
117 // The only remaining case is NodeIdentifierWrithPredicates, verify that instead of an explicit cast
118 if (!(arg instanceof NodeIdentifierWithPredicates withPredicates)) {
119 throw new VerifyException("Unhandled " + arg);
121 // A NodeIdentifierWithPredicates adresses a MapEntryNode and maps to a ListInstance with one or more values:
122 // 1) schema has to be a ListSchemaNode
123 if (!(schema instanceof ListSchemaNode list)) {
124 throw new RestconfDocumentedException(
125 "Argument '%s' does not map to a list, but %s".formatted(arg, schema),
126 ErrorType.APPLICATION, ErrorTag.INVALID_VALUE);
128 // 2) the key definition must be non-empty
129 final var keyDef = list.getKeyDefinition();
130 final var size = keyDef.size();
132 throw new RestconfDocumentedException(
133 "Argument '%s' maps a list without any keys %s".formatted(arg, schema),
134 ErrorType.APPLICATION, ErrorTag.INVALID_VALUE);
136 // 3) the number of predicates has to match the number of keys
137 if (size != withPredicates.size()) {
138 throw new RestconfDocumentedException(
139 "Argument '%s' does not match required keys %s".formatted(arg, keyDef),
140 ErrorType.APPLICATION, ErrorTag.INVALID_VALUE);
143 // ListSchemaNode implies the context is a composite, verify that instead of an unexplained cast when we look
144 // up the schema for individual keys
145 if (!(context instanceof Composite composite)) {
146 throw new VerifyException("Unexpected non-composite " + context + " with " + list);
149 final var builder = ImmutableList.<String>builderWithExpectedSize(size);
150 for (var key : keyDef) {
151 final var value = withPredicates.getValue(key);
153 throw new RestconfDocumentedException("Argument '%s' is missing predicate for %s".formatted(arg, key),
154 ErrorType.APPLICATION, ErrorTag.INVALID_VALUE);
157 final var tmpStack = stack.copy();
158 final var keyContext = composite.enterChild(tmpStack, key);
159 if (keyContext == null) {
160 throw new VerifyException("Failed to find key " + key + " in " + composite);
162 if (!(keyContext.dataSchemaNode() instanceof LeafSchemaNode leaf)) {
163 throw new VerifyException("Key " + key + " maps to non-leaf context " + keyContext);
165 builder.add(valueToString(tmpStack, leaf, value));
167 return ListInstance.of(module, identifier, builder.build());
170 private <T> @NonNull String valueToString(final SchemaInferenceStack stack, final TypedDataSchemaNode schema,
172 @SuppressWarnings("unchecked")
173 final var codec = (JSONCodec<T>) databind.jsonCodecs().codecFor(schema, stack);
174 return codec.unparseValue(value).rawString();
178 * Create prefix of namespace from {@link QName}.
180 * @param qname {@link QName}
181 * @return {@link String}
183 private @NonNull String resolvePrefix(final QName qname) {
184 return databind.modelContext().findModuleStatement(qname.getModule()).orElseThrow().argument().getLocalName();