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 java.io.IOException;
15 import org.eclipse.jdt.annotation.NonNull;
16 import org.opendaylight.restconf.api.ApiPath;
17 import org.opendaylight.restconf.api.ApiPath.ApiIdentifier;
18 import org.opendaylight.restconf.api.ApiPath.ListInstance;
19 import org.opendaylight.restconf.api.ApiPath.Step;
20 import org.opendaylight.restconf.common.errors.RestconfDocumentedException;
21 import org.opendaylight.restconf.server.api.DatabindContext;
22 import org.opendaylight.yangtools.yang.common.ErrorTag;
23 import org.opendaylight.yangtools.yang.common.ErrorType;
24 import org.opendaylight.yangtools.yang.common.QName;
25 import org.opendaylight.yangtools.yang.common.QNameModule;
26 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
27 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifier;
28 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifierWithPredicates;
29 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeWithValue;
30 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.PathArgument;
31 import org.opendaylight.yangtools.yang.data.codec.gson.JSONCodec;
32 import org.opendaylight.yangtools.yang.data.util.DataSchemaContext;
33 import org.opendaylight.yangtools.yang.data.util.DataSchemaContext.Composite;
34 import org.opendaylight.yangtools.yang.data.util.DataSchemaContext.PathMixin;
35 import org.opendaylight.yangtools.yang.model.api.LeafListSchemaNode;
36 import org.opendaylight.yangtools.yang.model.api.LeafSchemaNode;
37 import org.opendaylight.yangtools.yang.model.api.ListSchemaNode;
38 import org.opendaylight.yangtools.yang.model.api.TypedDataSchemaNode;
39 import org.opendaylight.yangtools.yang.model.util.SchemaInferenceStack;
42 * Utility class for turning {@link YangInstanceIdentifier}s into {@link ApiPath}s via
43 * {@link #dataToApiPath(YangInstanceIdentifier)}.
45 public final class ApiPathCanonizer {
46 private final @NonNull DatabindContext databind;
48 public ApiPathCanonizer(final DatabindContext databind) {
49 this.databind = requireNonNull(databind);
53 * Return the canonical {@link ApiPath} for specified {@link YangInstanceIdentifier}.
55 * @param path {@link YangInstanceIdentifier} to canonicalize
56 * @return An {@link ApiPath}
58 public @NonNull ApiPath dataToApiPath(final YangInstanceIdentifier path) {
59 final var it = path.getPathArguments().iterator();
61 return ApiPath.empty();
64 final var stack = SchemaInferenceStack.of(databind.modelContext());
65 final var builder = ImmutableList.<Step>builder();
66 DataSchemaContext context = databind.schemaTree().getRoot();
67 QNameModule parentModule = null;
69 final var arg = it.next();
71 // get module of the parent
72 if (!(context instanceof PathMixin)) {
73 parentModule = context.dataSchemaNode().getQName().getModule();
76 final var childContext = context instanceof Composite composite ? composite.enterChild(stack, arg) : null;
77 if (childContext == null) {
78 throw new RestconfDocumentedException(
79 "Invalid input '%s': schema for argument '%s' (after '%s') not found".formatted(path, arg,
80 ApiPath.of(builder.build())), ErrorType.APPLICATION, ErrorTag.UNKNOWN_ELEMENT);
83 context = childContext;
85 // only output PathArguments which are not inherent to YangInstanceIdentifier structure
86 if (!(childContext instanceof PathMixin)) {
87 builder.add(argToStep(arg, parentModule, stack, context));
89 } while (it.hasNext());
91 return new ApiPath(builder.build());
94 private @NonNull Step argToStep(final PathArgument arg, final QNameModule prevNamespace,
95 final SchemaInferenceStack stack, final DataSchemaContext context) {
96 // append namespace before every node which is defined in other module than its parent
97 // condition is satisfied also for the first path argument
98 final var nodeType = arg.getNodeType();
99 final var module = nodeType.getModule().equals(prevNamespace) ? null : resolvePrefix(nodeType);
100 final var identifier = nodeType.unbind();
102 // NodeIdentifier maps to an ApiIdentifier
103 if (arg instanceof NodeIdentifier) {
104 return new ApiIdentifier(module, identifier);
107 // NodeWithValue addresses a LeafSetEntryNode and maps to a ListInstance with a single value
108 final var schema = context.dataSchemaNode();
109 if (arg instanceof NodeWithValue<?> withValue) {
110 if (!(schema instanceof LeafListSchemaNode leafList)) {
111 throw new RestconfDocumentedException(
112 "Argument '%s' does not map to a leaf-list, but %s".formatted(arg, schema),
113 ErrorType.APPLICATION, ErrorTag.INVALID_VALUE);
115 return ListInstance.of(module, identifier, valueToString(stack, leafList, withValue.getValue()));
118 // The only remaining case is NodeIdentifierWrithPredicates, verify that instead of an explicit cast
119 if (!(arg instanceof NodeIdentifierWithPredicates withPredicates)) {
120 throw new VerifyException("Unhandled " + arg);
122 // A NodeIdentifierWithPredicates adresses a MapEntryNode and maps to a ListInstance with one or more values:
123 // 1) schema has to be a ListSchemaNode
124 if (!(schema instanceof ListSchemaNode list)) {
125 throw new RestconfDocumentedException(
126 "Argument '%s' does not map to a list, but %s".formatted(arg, schema),
127 ErrorType.APPLICATION, ErrorTag.INVALID_VALUE);
129 // 2) the key definition must be non-empty
130 final var keyDef = list.getKeyDefinition();
131 final var size = keyDef.size();
133 throw new RestconfDocumentedException(
134 "Argument '%s' maps a list without any keys %s".formatted(arg, schema),
135 ErrorType.APPLICATION, ErrorTag.INVALID_VALUE);
137 // 3) the number of predicates has to match the number of keys
138 if (size != withPredicates.size()) {
139 throw new RestconfDocumentedException(
140 "Argument '%s' does not match required keys %s".formatted(arg, keyDef),
141 ErrorType.APPLICATION, ErrorTag.INVALID_VALUE);
144 // ListSchemaNode implies the context is a composite, verify that instead of an unexplained cast when we look
145 // up the schema for individual keys
146 if (!(context instanceof Composite composite)) {
147 throw new VerifyException("Unexpected non-composite " + context + " with " + list);
150 final var builder = ImmutableList.<String>builderWithExpectedSize(size);
151 for (var key : keyDef) {
152 final var value = withPredicates.getValue(key);
154 throw new RestconfDocumentedException("Argument '%s' is missing predicate for %s".formatted(arg, key),
155 ErrorType.APPLICATION, ErrorTag.INVALID_VALUE);
158 final var tmpStack = stack.copy();
159 final var keyContext = composite.enterChild(tmpStack, key);
160 if (keyContext == null) {
161 throw new VerifyException("Failed to find key " + key + " in " + composite);
163 if (!(keyContext.dataSchemaNode() instanceof LeafSchemaNode leaf)) {
164 throw new VerifyException("Key " + key + " maps to non-leaf context " + keyContext);
166 builder.add(valueToString(tmpStack, leaf, value));
168 return ListInstance.of(module, identifier, builder.build());
171 private <T> @NonNull String valueToString(final SchemaInferenceStack stack, final TypedDataSchemaNode schema,
173 @SuppressWarnings("unchecked")
174 final var codec = (JSONCodec<T>) databind.jsonCodecs().codecFor(schema, stack);
175 try (var jsonWriter = new HackJsonWriter()) {
176 codec.writeValue(jsonWriter, value);
177 return jsonWriter.acquireCaptured().rawString();
178 } catch (IOException e) {
179 throw new IllegalStateException("Failed to serialize '" + value + "'", e);
184 * Create prefix of namespace from {@link QName}.
186 * @param qname {@link QName}
187 * @return {@link String}
189 private @NonNull String resolvePrefix(final QName qname) {
190 return databind.modelContext().findModuleStatement(qname.getModule()).orElseThrow().argument().getLocalName();