2 * Copyright (c) 2016 Cisco Systems, Inc. and others. All rights reserved.
3 * Copyright (c) 2023 PANTHEON.tech, s.r.o.
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
9 package org.opendaylight.restconf.nb.rfc8040.databind;
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;
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;
45 public final class JsonPatchBody extends PatchBody {
46 public JsonPatchBody(final InputStream inputStream) {
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);
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();
67 while (in.hasNext()) {
74 Boolean.toString(in.nextBoolean());
88 parseByName(in.nextName(), edit, in, urlPath, schemaTree, edits, patchId);
102 return edits.build();
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 {
112 if (in.peek() == JsonToken.BEGIN_ARRAY) {
115 while (in.hasNext()) {
116 readEditDefinition(edit, in, urlPath, schemaTree);
117 resultCollection.add(prepareEditOperation(edit));
123 readEditDefinition(edit, in, urlPath, schemaTree);
124 resultCollection.add(prepareEditOperation(edit));
130 patchId.set(in.nextString());
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)
141 String deferredValue = null;
144 while (in.hasNext()) {
145 final String editDefinition = in.nextName();
146 switch (editDefinition) {
148 edit.setId(in.nextString());
151 edit.setOperation(Operation.ofName(in.nextString()));
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()) {
161 if (!stack.isEmpty()) {
162 final EffectiveStatement<?, ?> parentStmt = stack.currentStatement();
163 verify(parentStmt instanceof SchemaNode, "Unexpected parent %s", parentStmt);
165 edit.setTargetSchemaNode(stack.toInference());
169 checkArgument(edit.getData() == null && deferredValue == null, "Multiple value entries found");
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);
176 // We have a target schema node, reuse this reader without buffering the value.
177 edit.setData(readEditData(in, edit.getTargetSchemaNode(),
178 schemaTree.getEffectiveModelContext()));
182 // FIXME: this does not look right, as it can wreck our logic
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()));
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
202 private static String readValueNode(final @NonNull JsonReader in) throws IOException {
204 final StringBuilder sb = new StringBuilder().append("{\"").append(in.nextName()).append("\":");
211 while (in.hasNext()) {
212 if (in.peek() == JsonToken.STRING) {
213 sb.append('"').append(in.nextString()).append('"');
215 readValueObject(sb, in);
217 if (in.peek() != JsonToken.END_ARRAY) {
226 readValueObject(sb, in);
231 return sb.append('}').toString();
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
240 private static void readValueObject(final @NonNull StringBuilder sb, final @NonNull JsonReader in)
242 // read simple leaf value
243 if (in.peek() == JsonToken.STRING) {
244 sb.append('"').append(in.nextString()).append('"');
251 while (in.hasNext()) {
252 sb.append('"').append(in.nextName()).append("\":");
256 sb.append('"').append(in.nextString()).append('"');
262 while (in.hasNext()) {
263 if (in.peek() == JsonToken.STRING) {
264 sb.append('"').append(in.nextString()).append('"');
266 readValueObject(sb, in);
269 if (in.peek() != JsonToken.END_ARRAY) {
278 readValueObject(sb, in);
281 if (in.peek() != JsonToken.END_OBJECT) {
291 * Read patch edit data defined in value node to NormalizedNode.
292 * @param in reader JsonReader reader
293 * @return NormalizedNode representing data
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)
302 return resultHolder.getResult().data();
306 * Prepare PatchEntity from PatchEdit instance when it satisfies conditions, otherwise throws exception.
307 * @param edit Instance of PatchEdit
308 * @return PatchEntity Patch entity
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());
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();
322 targetNode = edit.getTarget();
325 return new PatchEntity(edit.getId(), edit.getOperation(), targetNode, edit.getData());
328 throw new RestconfDocumentedException("Error parsing input", ErrorType.PROTOCOL, ErrorTag.MALFORMED_MESSAGE);
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
338 private static boolean checkDataPresence(final @NonNull Operation operation, final boolean hasData) {
339 return requiresValue(operation) == hasData;
343 * Helper class representing one patch edit.
345 private static final class PatchEdit {
347 private Operation operation;
348 private YangInstanceIdentifier target;
349 private Inference targetSchemaNode;
350 private NormalizedNode data;
356 void setId(final String id) {
357 this.id = requireNonNull(id);
360 Operation getOperation() {
364 void setOperation(final Operation operation) {
365 this.operation = requireNonNull(operation);
368 YangInstanceIdentifier getTarget() {
372 void setTarget(final YangInstanceIdentifier target) {
373 this.target = requireNonNull(target);
376 Inference getTargetSchemaNode() {
377 return targetSchemaNode;
380 void setTargetSchemaNode(final Inference targetSchemaNode) {
381 this.targetSchemaNode = requireNonNull(targetSchemaNode);
384 NormalizedNode getData() {
388 void setData(final NormalizedNode data) {
389 this.data = requireNonNull(data);
396 targetSchemaNode = null;