Update sal-remote-augment model to rev 2023-11-03
[netconf.git] / restconf / restconf-nb / src / main / java / org / opendaylight / restconf / nb / rfc8040 / databind / JsonPatchBody.java
1 /*
2  * Copyright (c) 2016 Cisco Systems, Inc. and others.  All rights reserved.
3  * Copyright (c) 2023 PANTHEON.tech, s.r.o.
4  *
5  * This program and the accompanying materials are made available under the
6  * terms of the Eclipse Public License v1.0 which accompanies this distribution,
7  * and is available at http://www.eclipse.org/legal/epl-v10.html
8  */
9 package org.opendaylight.restconf.nb.rfc8040.databind;
10
11 import static com.google.common.base.Preconditions.checkArgument;
12 import static com.google.common.base.Verify.verify;
13 import static java.util.Objects.requireNonNull;
14
15 import com.google.common.collect.ImmutableList;
16 import com.google.common.collect.ImmutableList.Builder;
17 import com.google.gson.stream.JsonReader;
18 import com.google.gson.stream.JsonToken;
19 import java.io.IOException;
20 import java.io.InputStream;
21 import java.io.InputStreamReader;
22 import java.io.StringReader;
23 import java.nio.charset.StandardCharsets;
24 import java.util.concurrent.atomic.AtomicReference;
25 import org.eclipse.jdt.annotation.NonNull;
26 import org.opendaylight.restconf.common.errors.RestconfDocumentedException;
27 import org.opendaylight.restconf.common.patch.PatchContext;
28 import org.opendaylight.restconf.common.patch.PatchEntity;
29 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.yang.patch.rev170222.yang.patch.yang.patch.Edit.Operation;
30 import org.opendaylight.yangtools.yang.common.ErrorTag;
31 import org.opendaylight.yangtools.yang.common.ErrorType;
32 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
33 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifierWithPredicates;
34 import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode;
35 import org.opendaylight.yangtools.yang.data.codec.gson.JSONCodecFactorySupplier;
36 import org.opendaylight.yangtools.yang.data.codec.gson.JsonParserStream;
37 import org.opendaylight.yangtools.yang.data.impl.schema.ImmutableNormalizedNodeStreamWriter;
38 import org.opendaylight.yangtools.yang.data.impl.schema.NormalizationResultHolder;
39 import org.opendaylight.yangtools.yang.data.util.DataSchemaContextTree;
40 import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext;
41 import org.opendaylight.yangtools.yang.model.api.SchemaNode;
42 import org.opendaylight.yangtools.yang.model.api.meta.EffectiveStatement;
43 import org.opendaylight.yangtools.yang.model.util.SchemaInferenceStack.Inference;
44
45 public final class JsonPatchBody extends PatchBody {
46     public JsonPatchBody(final InputStream inputStream) {
47         super(inputStream);
48     }
49
50     @Override
51     PatchContext toPatchContext(final EffectiveModelContext context, final YangInstanceIdentifier urlPath,
52             final InputStream inputStream) throws IOException {
53         try (var jsonReader = new JsonReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8))) {
54             final var patchId = new AtomicReference<String>();
55             final var resultList = read(jsonReader, context, urlPath, patchId);
56             // Note: patchId side-effect of above
57             return new PatchContext(patchId.get(), resultList);
58         }
59     }
60
61     private static ImmutableList<PatchEntity> read(final JsonReader in, final EffectiveModelContext context,
62             final YangInstanceIdentifier urlPath, final AtomicReference<String> patchId) throws IOException {
63         final var schemaTree = DataSchemaContextTree.from(context);
64         final var edits = ImmutableList.<PatchEntity>builder();
65         final var edit = new PatchEdit();
66
67         while (in.hasNext()) {
68             switch (in.peek()) {
69                 case STRING:
70                 case NUMBER:
71                     in.nextString();
72                     break;
73                 case BOOLEAN:
74                     Boolean.toString(in.nextBoolean());
75                     break;
76                 case NULL:
77                     in.nextNull();
78                     break;
79                 case BEGIN_ARRAY:
80                     in.beginArray();
81                     break;
82                 case BEGIN_OBJECT:
83                     in.beginObject();
84                     break;
85                 case END_DOCUMENT:
86                     break;
87                 case NAME:
88                     parseByName(in.nextName(), edit, in, urlPath, schemaTree, edits, patchId);
89                     break;
90                 case END_OBJECT:
91                     in.endObject();
92                     break;
93                 case END_ARRAY:
94                     in.endArray();
95                     break;
96
97                 default:
98                     break;
99             }
100         }
101
102         return edits.build();
103     }
104
105     // Switch value of parsed JsonToken.NAME and read edit definition or patch id
106     private static void parseByName(final @NonNull String name, final @NonNull PatchEdit edit,
107             final @NonNull JsonReader in, final @NonNull YangInstanceIdentifier urlPath,
108             final @NonNull DataSchemaContextTree schemaTree, final @NonNull Builder<PatchEntity> resultCollection,
109             final @NonNull AtomicReference<String> patchId) throws IOException {
110         switch (name) {
111             case "edit":
112                 if (in.peek() == JsonToken.BEGIN_ARRAY) {
113                     in.beginArray();
114
115                     while (in.hasNext()) {
116                         readEditDefinition(edit, in, urlPath, schemaTree);
117                         resultCollection.add(prepareEditOperation(edit));
118                         edit.clear();
119                     }
120
121                     in.endArray();
122                 } else {
123                     readEditDefinition(edit, in, urlPath, schemaTree);
124                     resultCollection.add(prepareEditOperation(edit));
125                     edit.clear();
126                 }
127
128                 break;
129             case "patch-id":
130                 patchId.set(in.nextString());
131                 break;
132             default:
133                 break;
134         }
135     }
136
137     // Read one patch edit object from JSON input
138     private static void readEditDefinition(final @NonNull PatchEdit edit, final @NonNull JsonReader in,
139             final @NonNull YangInstanceIdentifier urlPath, final @NonNull DataSchemaContextTree schemaTree)
140                 throws IOException {
141         String deferredValue = null;
142         in.beginObject();
143
144         while (in.hasNext()) {
145             final String editDefinition = in.nextName();
146             switch (editDefinition) {
147                 case "edit-id":
148                     edit.setId(in.nextString());
149                     break;
150                 case "operation":
151                     edit.setOperation(Operation.ofName(in.nextString()));
152                     break;
153                 case "target":
154                     // target can be specified completely in request URI
155                     edit.setTarget(parsePatchTarget(schemaTree.getEffectiveModelContext(), urlPath, in.nextString()));
156                     final var stack = schemaTree.enterPath(edit.getTarget()).orElseThrow().stack();
157                     if (!stack.isEmpty()) {
158                         stack.exit();
159                     }
160
161                     if (!stack.isEmpty()) {
162                         final EffectiveStatement<?, ?> parentStmt = stack.currentStatement();
163                         verify(parentStmt instanceof SchemaNode, "Unexpected parent %s", parentStmt);
164                     }
165                     edit.setTargetSchemaNode(stack.toInference());
166
167                     break;
168                 case "value":
169                     checkArgument(edit.getData() == null && deferredValue == null, "Multiple value entries found");
170
171                     if (edit.getTargetSchemaNode() == null) {
172                         // save data defined in value node for next (later) processing, because target needs to be read
173                         // always first and there is no ordering in Json input
174                         deferredValue = readValueNode(in);
175                     } else {
176                         // We have a target schema node, reuse this reader without buffering the value.
177                         edit.setData(readEditData(in, edit.getTargetSchemaNode(),
178                             schemaTree.getEffectiveModelContext()));
179                     }
180                     break;
181                 default:
182                     // FIXME: this does not look right, as it can wreck our logic
183                     break;
184             }
185         }
186
187         in.endObject();
188
189         if (deferredValue != null) {
190             // read saved data to normalized node when target schema is already known
191             edit.setData(readEditData(new JsonReader(new StringReader(deferredValue)), edit.getTargetSchemaNode(),
192                 schemaTree.getEffectiveModelContext()));
193         }
194     }
195
196     /**
197      * Parse data defined in value node and saves it to buffer.
198      * @param sb Buffer to read value node
199      * @param in JsonReader reader
200      * @throws IOException if operation fails
201      */
202     private static String readValueNode(final @NonNull JsonReader in) throws IOException {
203         in.beginObject();
204         final StringBuilder sb = new StringBuilder().append("{\"").append(in.nextName()).append("\":");
205
206         switch (in.peek()) {
207             case BEGIN_ARRAY:
208                 in.beginArray();
209                 sb.append('[');
210
211                 while (in.hasNext()) {
212                     if (in.peek() == JsonToken.STRING) {
213                         sb.append('"').append(in.nextString()).append('"');
214                     } else {
215                         readValueObject(sb, in);
216                     }
217                     if (in.peek() != JsonToken.END_ARRAY) {
218                         sb.append(',');
219                     }
220                 }
221
222                 in.endArray();
223                 sb.append(']');
224                 break;
225             default:
226                 readValueObject(sb, in);
227                 break;
228         }
229
230         in.endObject();
231         return sb.append('}').toString();
232     }
233
234     /**
235      * Parse one value object of data and saves it to buffer.
236      * @param sb Buffer to read value object
237      * @param in JsonReader reader
238      * @throws IOException if operation fails
239      */
240     private static void readValueObject(final @NonNull StringBuilder sb, final @NonNull JsonReader in)
241         throws IOException {
242         // read simple leaf value
243         if (in.peek() == JsonToken.STRING) {
244             sb.append('"').append(in.nextString()).append('"');
245             return;
246         }
247
248         in.beginObject();
249         sb.append('{');
250
251         while (in.hasNext()) {
252             sb.append('"').append(in.nextName()).append("\":");
253
254             switch (in.peek()) {
255                 case STRING:
256                     sb.append('"').append(in.nextString()).append('"');
257                     break;
258                 case BEGIN_ARRAY:
259                     in.beginArray();
260                     sb.append('[');
261
262                     while (in.hasNext()) {
263                         if (in.peek() == JsonToken.STRING) {
264                             sb.append('"').append(in.nextString()).append('"');
265                         } else {
266                             readValueObject(sb, in);
267                         }
268
269                         if (in.peek() != JsonToken.END_ARRAY) {
270                             sb.append(',');
271                         }
272                     }
273
274                     in.endArray();
275                     sb.append(']');
276                     break;
277                 default:
278                     readValueObject(sb, in);
279             }
280
281             if (in.peek() != JsonToken.END_OBJECT) {
282                 sb.append(',');
283             }
284         }
285
286         in.endObject();
287         sb.append('}');
288     }
289
290     /**
291      * Read patch edit data defined in value node to NormalizedNode.
292      * @param in reader JsonReader reader
293      * @return NormalizedNode representing data
294      */
295     private static NormalizedNode readEditData(final @NonNull JsonReader in, final @NonNull Inference targetSchemaNode,
296             final @NonNull EffectiveModelContext context) {
297         final var resultHolder = new NormalizationResultHolder();
298         final var writer = ImmutableNormalizedNodeStreamWriter.from(resultHolder);
299         JsonParserStream.create(writer, JSONCodecFactorySupplier.RFC7951.getShared(context), targetSchemaNode)
300             .parse(in);
301
302         return resultHolder.getResult().data();
303     }
304
305     /**
306      * Prepare PatchEntity from PatchEdit instance when it satisfies conditions, otherwise throws exception.
307      * @param edit Instance of PatchEdit
308      * @return PatchEntity Patch entity
309      */
310     private static PatchEntity prepareEditOperation(final @NonNull PatchEdit edit) {
311         if (edit.getOperation() != null && edit.getTargetSchemaNode() != null
312             && checkDataPresence(edit.getOperation(), edit.getData() != null)) {
313             if (!requiresValue(edit.getOperation())) {
314                 return new PatchEntity(edit.getId(), edit.getOperation(), edit.getTarget());
315             }
316
317             // for lists allow to manipulate with list items through their parent
318             final YangInstanceIdentifier targetNode;
319             if (edit.getTarget().getLastPathArgument() instanceof NodeIdentifierWithPredicates) {
320                 targetNode = edit.getTarget().getParent();
321             } else {
322                 targetNode = edit.getTarget();
323             }
324
325             return new PatchEntity(edit.getId(), edit.getOperation(), targetNode, edit.getData());
326         }
327
328         throw new RestconfDocumentedException("Error parsing input", ErrorType.PROTOCOL, ErrorTag.MALFORMED_MESSAGE);
329     }
330
331     /**
332      * Check if data is present when operation requires it and not present when operation data is not allowed.
333      * @param operation Name of operation
334      * @param hasData Data in edit are present/not present
335      * @return true if data is present when operation requires it or if there are no data when operation does not
336      *     allow it, false otherwise
337      */
338     private static boolean checkDataPresence(final @NonNull Operation operation, final boolean hasData) {
339         return requiresValue(operation)  == hasData;
340     }
341
342     /**
343      * Helper class representing one patch edit.
344      */
345     private static final class PatchEdit {
346         private String id;
347         private Operation operation;
348         private YangInstanceIdentifier target;
349         private Inference targetSchemaNode;
350         private NormalizedNode data;
351
352         String getId() {
353             return id;
354         }
355
356         void setId(final String id) {
357             this.id = requireNonNull(id);
358         }
359
360         Operation getOperation() {
361             return operation;
362         }
363
364         void setOperation(final Operation operation) {
365             this.operation = requireNonNull(operation);
366         }
367
368         YangInstanceIdentifier getTarget() {
369             return target;
370         }
371
372         void setTarget(final YangInstanceIdentifier target) {
373             this.target = requireNonNull(target);
374         }
375
376         Inference getTargetSchemaNode() {
377             return targetSchemaNode;
378         }
379
380         void setTargetSchemaNode(final Inference targetSchemaNode) {
381             this.targetSchemaNode = requireNonNull(targetSchemaNode);
382         }
383
384         NormalizedNode getData() {
385             return data;
386         }
387
388         void setData(final NormalizedNode data) {
389             this.data = requireNonNull(data);
390         }
391
392         void clear() {
393             id = null;
394             operation = null;
395             target = null;
396             targetSchemaNode = null;
397             data = null;
398         }
399     }
400 }