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.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;
44 public final class JsonPatchBody extends PatchBody {
45 public JsonPatchBody(final InputStream inputStream) {
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);
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();
65 while (in.hasNext()) {
72 Boolean.toString(in.nextBoolean());
86 parseByName(in.nextName(), edit, in, urlPath, databind, edits, patchId);
100 return edits.build();
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 {
110 if (in.peek() == JsonToken.BEGIN_ARRAY) {
113 while (in.hasNext()) {
114 readEditDefinition(edit, in, urlPath, databind);
115 resultCollection.add(prepareEditOperation(edit));
121 readEditDefinition(edit, in, urlPath, databind);
122 resultCollection.add(prepareEditOperation(edit));
128 patchId.set(in.nextString());
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)
139 String deferredValue = null;
142 while (in.hasNext()) {
143 final String editDefinition = in.nextName();
144 switch (editDefinition) {
146 edit.setId(in.nextString());
149 edit.setOperation(Operation.ofName(in.nextString()));
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()) {
159 if (!stack.isEmpty()) {
160 final var parentStmt = stack.currentStatement();
161 verify(parentStmt instanceof SchemaNode, "Unexpected parent %s", parentStmt);
163 edit.setTargetSchemaNode(stack.toInference());
167 checkArgument(edit.getData() == null && deferredValue == null, "Multiple value entries found");
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);
174 // We have a target schema node, reuse this reader without buffering the value.
175 edit.setData(readEditData(in, edit.getTargetSchemaNode(), databind.modelContext()));
179 // FIXME: this does not look right, as it can wreck our logic
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()));
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
199 private static String readValueNode(final @NonNull JsonReader in) throws IOException {
201 final StringBuilder sb = new StringBuilder().append("{\"").append(in.nextName()).append("\":");
208 while (in.hasNext()) {
209 if (in.peek() == JsonToken.STRING) {
210 sb.append('"').append(in.nextString()).append('"');
212 readValueObject(sb, in);
214 if (in.peek() != JsonToken.END_ARRAY) {
223 readValueObject(sb, in);
228 return sb.append('}').toString();
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
237 private static void readValueObject(final @NonNull StringBuilder sb, final @NonNull JsonReader in)
239 // read simple leaf value
240 if (in.peek() == JsonToken.STRING) {
241 sb.append('"').append(in.nextString()).append('"');
248 while (in.hasNext()) {
249 sb.append('"').append(in.nextName()).append("\":");
253 sb.append('"').append(in.nextString()).append('"');
259 while (in.hasNext()) {
260 if (in.peek() == JsonToken.STRING) {
261 sb.append('"').append(in.nextString()).append('"');
263 readValueObject(sb, in);
266 if (in.peek() != JsonToken.END_ARRAY) {
275 readValueObject(sb, in);
278 if (in.peek() != JsonToken.END_OBJECT) {
288 * Read patch edit data defined in value node to NormalizedNode.
289 * @param in reader JsonReader reader
290 * @return NormalizedNode representing data
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)
299 return resultHolder.getResult().data();
303 * Prepare PatchEntity from PatchEdit instance when it satisfies conditions, otherwise throws exception.
304 * @param edit Instance of PatchEdit
305 * @return PatchEntity Patch entity
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());
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();
319 targetNode = edit.getTarget();
322 return new PatchEntity(edit.getId(), edit.getOperation(), targetNode, edit.getData());
325 throw new RestconfDocumentedException("Error parsing input", ErrorType.PROTOCOL, ErrorTag.MALFORMED_MESSAGE);
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
335 private static boolean checkDataPresence(final @NonNull Operation operation, final boolean hasData) {
336 return requiresValue(operation) == hasData;
340 * Helper class representing one patch edit.
342 private static final class PatchEdit {
344 private Operation operation;
345 private YangInstanceIdentifier target;
346 private Inference targetSchemaNode;
347 private NormalizedNode data;
353 void setId(final String id) {
354 this.id = requireNonNull(id);
357 Operation getOperation() {
361 void setOperation(final Operation operation) {
362 this.operation = requireNonNull(operation);
365 YangInstanceIdentifier getTarget() {
369 void setTarget(final YangInstanceIdentifier target) {
370 this.target = requireNonNull(target);
373 Inference getTargetSchemaNode() {
374 return targetSchemaNode;
377 void setTargetSchemaNode(final Inference targetSchemaNode) {
378 this.targetSchemaNode = requireNonNull(targetSchemaNode);
381 NormalizedNode getData() {
385 void setData(final NormalizedNode data) {
386 this.data = requireNonNull(data);
393 targetSchemaNode = null;