Add InputStreamNormalizer
[yangtools.git] / data / yang-data-util / src / main / java / org / opendaylight / yangtools / yang / data / util / codec / AbstractInputStreamNormalizer.java
1 /*
2  * Copyright (c) 2023 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.yangtools.yang.data.util.codec;
9
10 import static java.util.Objects.requireNonNull;
11
12 import java.io.IOException;
13 import java.io.InputStream;
14 import java.util.ArrayList;
15 import org.eclipse.jdt.annotation.NonNull;
16 import org.opendaylight.yangtools.yang.common.QName;
17 import org.opendaylight.yangtools.yang.common.UnresolvedQName.Unqualified;
18 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifier;
19 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.PathArgument;
20 import org.opendaylight.yangtools.yang.data.api.schema.ChoiceNode;
21 import org.opendaylight.yangtools.yang.data.api.schema.ContainerNode;
22 import org.opendaylight.yangtools.yang.data.api.schema.LeafSetNode;
23 import org.opendaylight.yangtools.yang.data.api.schema.MapNode;
24 import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNodeContainer;
25 import org.opendaylight.yangtools.yang.data.api.schema.UnkeyedListNode;
26 import org.opendaylight.yangtools.yang.data.api.schema.stream.InputStreamNormalizer;
27 import org.opendaylight.yangtools.yang.data.api.schema.stream.NormalizationException;
28 import org.opendaylight.yangtools.yang.data.api.schema.stream.NormalizationResult;
29 import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext;
30 import org.opendaylight.yangtools.yang.model.api.EffectiveStatementInference;
31 import org.opendaylight.yangtools.yang.model.api.stmt.ActionEffectiveStatement;
32 import org.opendaylight.yangtools.yang.model.api.stmt.DataTreeEffectiveStatement;
33 import org.opendaylight.yangtools.yang.model.api.stmt.RpcEffectiveStatement;
34 import org.opendaylight.yangtools.yang.model.util.SchemaInferenceStack;
35
36 /**
37  * An {@link AbstractCodecFactory} which additionally provides services defined in {@link InputStreamNormalizer}.
38  *
39  * <p>
40  * This class existsonly because both JSON and XML implementations of {@link InputStreamNormalizer} are naturally hosted
41  * in their respective {@link AbstractCodecFactory} implementations and therefore it is a convenient place to share
42  * common implementation bits.
43  */
44 public abstract class AbstractInputStreamNormalizer<T extends TypeAwareCodec<?, ?, ?>>
45         extends AbstractCodecFactory<T> implements InputStreamNormalizer {
46     protected AbstractInputStreamNormalizer(final @NonNull EffectiveModelContext schemaContext,
47             final @NonNull CodecCache<T> cache) {
48         super(schemaContext, cache);
49     }
50
51     @Override
52     public final NormalizationResult<ContainerNode> parseDatastore(final NodeIdentifier containerName,
53             final Unqualified moduleName, final InputStream stream) throws NormalizationException {
54         try {
55             return parseDatastore(requireNonNull(stream), requireNonNull(containerName), requireNonNull(moduleName));
56         } catch (IOException | IllegalArgumentException e) {
57             throw NormalizationException.ofCause(e);
58         }
59     }
60
61     protected abstract @NonNull NormalizationResult<ContainerNode> parseDatastore(@NonNull InputStream stream,
62         @NonNull NodeIdentifier containerName, @NonNull Unqualified moduleName)
63             throws IOException, NormalizationException;
64
65     @Override
66     public final NormalizationResult<?> parseData(final EffectiveStatementInference inference, final InputStream stream)
67             throws NormalizationException {
68         final var stack = checkInferenceNotEmpty(inference);
69         final var stmt = stack.currentStatement();
70         if (!(stmt instanceof DataTreeEffectiveStatement<?> dataStmt)) {
71             throw new IllegalArgumentException("Invalid inference statement " + stmt);
72         }
73
74         final NormalizationResult<?> data;
75         try {
76             data = parseData(stack, requireNonNull(stream));
77         } catch (IOException | IllegalArgumentException e) {
78             throw NormalizationException.ofCause(e);
79         }
80         return checkNodeName(data, dataStmt.argument());
81     }
82
83     protected abstract @NonNull NormalizationResult<?> parseData(@NonNull SchemaInferenceStack stack,
84         @NonNull InputStream stream) throws IOException, NormalizationException;
85
86     @Override
87     public final PrefixAndResult parseChildData(final EffectiveStatementInference inference, final InputStream stream)
88             throws NormalizationException {
89         checkInference(inference);
90
91         final NormalizationResult<?> normalized;
92         try {
93             normalized = parseChildData(requireNonNull(stream), inference);
94         } catch (IOException | IllegalArgumentException e) {
95             throw NormalizationException.ofCause(e);
96         }
97
98         final var prefix = new ArrayList<@NonNull PathArgument>();
99         var data = normalized.data();
100         var metadata = normalized.metadata();
101         var mountPoints = normalized.mountPoints();
102
103         // Deal with the semantic differences of what "child" means in NormalizedNode versus in YANG data tree
104         // structure.
105
106         // NormalizedNode structure has 'choice' statements visible and addressable, whereas YANG data tree makes
107         // them completely transparent.
108         //
109         // Therefore we need to peel any ChoiceNode from the result and shift them to the prefix. Since each choice was
110         // created implicitly to contain the element mentioned in the stream.
111         while (data instanceof ChoiceNode choice) {
112             prefix.add(choice.name());
113             data = choice.body().iterator().next();
114         }
115
116         // NormalizedNode structure has 'list' and 'leaf-list' statements visible and addressable, whereas YANG data
117         // tree addressing can only point to individual instances. RFC8040 section 4.4.1 states:
118         //
119         //        The message-body is expected to contain the
120         //        content of a child resource to create within the parent (target
121         //        resource).  The message-body MUST contain exactly one instance of the
122         //        expected data resource.  The data model for the child tree is the
123         //        subtree, as defined by YANG for the child resource.
124         //
125         // Therefore we need to peel any UnkeyedListNode, MapNode and LeafSetNodes from the top-level and shift them
126         // to the prefix. Note that from the parser perspective, each such node can legally contain zero, one or more
127         // entries, but this method is restricted to allowing only a single entry.
128         if (data instanceof MapNode || data instanceof LeafSetNode || data instanceof UnkeyedListNode) {
129             final var dataName = data.name();
130             final var body = ((NormalizedNodeContainer<?>) data).body();
131             final var size = body.size();
132             if (body.size() != 1) {
133                 throw NormalizationException.ofMessage(
134                     "Exactly one instance of " + dataName.getNodeType() + " is required, " + size + " supplied");
135             }
136
137
138             prefix.add(dataName);
139             data = body.iterator().next();
140             if (metadata != null) {
141                 metadata = metadata.getChildren().get(dataName);
142             }
143             if (mountPoints != null) {
144                 mountPoints = mountPoints.getChildren().get(dataName);
145             }
146         }
147
148         return new PrefixAndResult(prefix, new NormalizationResult<>(data, metadata, mountPoints));
149     }
150
151     protected abstract @NonNull NormalizationResult<?> parseChildData(@NonNull InputStream stream,
152         @NonNull EffectiveStatementInference inference) throws IOException, NormalizationException;
153
154     @Override
155     public final NormalizationResult<ContainerNode> parseInput(final EffectiveStatementInference inference,
156             final InputStream stream) throws NormalizationException {
157         final var stack = checkInferenceNotEmpty(inference);
158         final var stmt = stack.currentStatement();
159         final QName expected;
160         if (stmt instanceof RpcEffectiveStatement rpc) {
161             expected = rpc.input().argument();
162         } else if (stmt instanceof ActionEffectiveStatement action) {
163             expected = action.input().argument();
164         } else {
165             throw new IllegalArgumentException("Invalid inference statement " + stmt);
166         }
167         return parseInputOutput(stream, stack, expected);
168     }
169
170     @Override
171     public final NormalizationResult<ContainerNode> parseOutput(final EffectiveStatementInference inference,
172             final InputStream stream) throws NormalizationException {
173         final var stack = checkInferenceNotEmpty(inference);
174         final var stmt = stack.currentStatement();
175         final QName expected;
176         if (stmt instanceof RpcEffectiveStatement rpc) {
177             expected = rpc.output().argument();
178         } else if (stmt instanceof ActionEffectiveStatement action) {
179             expected = action.output().argument();
180         } else {
181             throw new IllegalArgumentException("Invalid inference statement " + stmt);
182         }
183         return parseInputOutput(stream, stack, expected);
184     }
185
186     private @NonNull NormalizationResult<ContainerNode> parseInputOutput(final @NonNull InputStream stream,
187             final @NonNull SchemaInferenceStack stack, final @NonNull QName expected) throws NormalizationException {
188         final NormalizationResult<?> data;
189         try {
190             data = parseInputOutput(stack, expected, requireNonNull(stream));
191         } catch (IOException | IllegalArgumentException e) {
192             throw NormalizationException.ofCause(e);
193         }
194         return checkNodeContainer(data);
195     }
196
197     protected abstract @NonNull NormalizationResult<?> parseInputOutput(@NonNull SchemaInferenceStack stack,
198         @NonNull QName expected, @NonNull InputStream stream) throws IOException, NormalizationException;
199
200     private void checkInference(final EffectiveStatementInference inference) {
201         final var modelContext = inference.getEffectiveModelContext();
202         final var local = getEffectiveModelContext();
203         if (!local.equals(modelContext)) {
204             throw new IllegalArgumentException("Mismatched inference, expecting model context " + local);
205         }
206     }
207
208     private @NonNull SchemaInferenceStack checkInferenceNotEmpty(final EffectiveStatementInference inference) {
209         checkInference(inference);
210         final var stack = SchemaInferenceStack.ofInference(inference);
211         if (stack.isEmpty()) {
212             throw new IllegalArgumentException("Inference must not be empty");
213         }
214         return stack;
215     }
216
217     @SuppressWarnings("unchecked")
218     protected static final @NonNull NormalizationResult<ContainerNode> checkNodeContainer(
219             final NormalizationResult<?> result) throws NormalizationException {
220         final var data = result.data();
221         if (data instanceof ContainerNode) {
222             return (NormalizationResult<ContainerNode>) result;
223         }
224         throw NormalizationException.ofMessage("Unexpected payload type " + data.contract());
225     }
226
227     protected static final @NonNull NormalizationResult<?> checkNodeName(final NormalizationResult<?> result,
228             final QName expected) throws NormalizationException {
229         final var qname = result.data().name().getNodeType();
230         if (qname.equals(expected)) {
231             return result;
232         }
233         throw NormalizationException.ofMessage(
234             "Payload name " + qname + " is different from identifier name " + expected);
235     }
236 }