Remove HackJsonWriter
[netconf.git] / restconf / restconf-nb / src / main / java / org / opendaylight / restconf / server / spi / ApiPathCanonizer.java
1 /*
2  * Copyright (c) 2024 PANTHEON.tech, s.r.o. and others.  All rights reserved.
3  *
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
7  */
8 package org.opendaylight.restconf.server.spi;
9
10 import static java.util.Objects.requireNonNull;
11
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;
39
40 /**
41  * Utility class for turning {@link YangInstanceIdentifier}s into {@link ApiPath}s via
42  * {@link #dataToApiPath(YangInstanceIdentifier)}.
43  */
44 public final class ApiPathCanonizer {
45     private final @NonNull DatabindContext databind;
46
47     public ApiPathCanonizer(final DatabindContext databind) {
48         this.databind = requireNonNull(databind);
49     }
50
51     /**
52      * Return the canonical {@link ApiPath} for specified {@link YangInstanceIdentifier}.
53      *
54      * @param path {@link YangInstanceIdentifier} to canonicalize
55      * @return An {@link ApiPath}
56      */
57     public @NonNull ApiPath dataToApiPath(final YangInstanceIdentifier path) {
58         final var it = path.getPathArguments().iterator();
59         if (!it.hasNext()) {
60             return ApiPath.empty();
61         }
62
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;
67         do {
68             final var arg = it.next();
69
70             // get module of the parent
71             if (!(context instanceof PathMixin)) {
72                 parentModule = context.dataSchemaNode().getQName().getModule();
73             }
74
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);
80             }
81
82             context = childContext;
83
84             // only output PathArguments which are not inherent to YangInstanceIdentifier structure
85             if (!(childContext instanceof PathMixin)) {
86                 builder.add(argToStep(arg, parentModule, stack, context));
87             }
88         } while (it.hasNext());
89
90         return new ApiPath(builder.build());
91     }
92
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();
100
101         // NodeIdentifier maps to an ApiIdentifier
102         if (arg instanceof NodeIdentifier) {
103             return new ApiIdentifier(module, identifier);
104         }
105
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);
113             }
114             return ListInstance.of(module, identifier, valueToString(stack, leafList, withValue.getValue()));
115         }
116
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);
120         }
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);
127         }
128         // 2) the key definition must be non-empty
129         final var keyDef = list.getKeyDefinition();
130         final var size = keyDef.size();
131         if (size == 0) {
132             throw new RestconfDocumentedException(
133                 "Argument '%s' maps a list without any keys %s".formatted(arg, schema),
134                 ErrorType.APPLICATION, ErrorTag.INVALID_VALUE);
135         }
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);
141         }
142
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);
147         }
148
149         final var builder = ImmutableList.<String>builderWithExpectedSize(size);
150         for (var key : keyDef) {
151             final var value = withPredicates.getValue(key);
152             if (value == null) {
153                 throw new RestconfDocumentedException("Argument '%s' is missing predicate for %s".formatted(arg, key),
154                     ErrorType.APPLICATION, ErrorTag.INVALID_VALUE);
155             }
156
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);
161             }
162             if (!(keyContext.dataSchemaNode() instanceof LeafSchemaNode leaf)) {
163                 throw new VerifyException("Key " + key + " maps to non-leaf context " + keyContext);
164             }
165             builder.add(valueToString(tmpStack, leaf, value));
166         }
167         return ListInstance.of(module, identifier, builder.build());
168     }
169
170     private <T> @NonNull String valueToString(final SchemaInferenceStack stack, final TypedDataSchemaNode schema,
171             final T value) {
172         @SuppressWarnings("unchecked")
173         final var codec = (JSONCodec<T>) databind.jsonCodecs().codecFor(schema, stack);
174         return codec.unparseValue(value).rawString();
175     }
176
177     /**
178      * Create prefix of namespace from {@link QName}.
179      *
180      * @param qname {@link QName}
181      * @return {@link String}
182      */
183     private @NonNull String resolvePrefix(final QName qname) {
184         return databind.modelContext().findModuleStatement(qname.getModule()).orElseThrow().argument().getLocalName();
185     }
186 }