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