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