2 * Copyright (c) 2016 Cisco Systems, Inc. and others. All rights reserved.
4 * This program and the accompanying materials are made available under the
5 * terms of the Eclipse Public License v1.0 which accompanies this distribution,
6 * and is available at http://www.eclipse.org/legal/epl-v10.html
8 package org.opendaylight.restconf.nb.rfc8040.jersey.providers.patch;
10 import static com.google.common.base.Preconditions.checkArgument;
11 import static com.google.common.base.Verify.verify;
12 import static java.util.Objects.requireNonNull;
14 import com.google.common.base.Throwables;
15 import com.google.common.collect.ImmutableList;
16 import com.google.gson.stream.JsonReader;
17 import com.google.gson.stream.JsonToken;
18 import java.io.IOException;
19 import java.io.InputStream;
20 import java.io.InputStreamReader;
21 import java.io.StringReader;
22 import java.nio.charset.StandardCharsets;
23 import java.util.concurrent.atomic.AtomicReference;
24 import javax.ws.rs.Consumes;
25 import javax.ws.rs.WebApplicationException;
26 import javax.ws.rs.ext.Provider;
27 import org.eclipse.jdt.annotation.NonNull;
28 import org.opendaylight.mdsal.dom.api.DOMMountPointService;
29 import org.opendaylight.restconf.common.context.InstanceIdentifierContext;
30 import org.opendaylight.restconf.common.errors.RestconfDocumentedException;
31 import org.opendaylight.restconf.common.patch.PatchContext;
32 import org.opendaylight.restconf.common.patch.PatchEntity;
33 import org.opendaylight.restconf.nb.rfc8040.MediaTypes;
34 import org.opendaylight.restconf.nb.rfc8040.databind.DatabindProvider;
35 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.yang.patch.rev170222.yang.patch.yang.patch.Edit.Operation;
36 import org.opendaylight.yangtools.yang.common.ErrorTag;
37 import org.opendaylight.yangtools.yang.common.ErrorType;
38 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
39 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifierWithPredicates;
40 import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode;
41 import org.opendaylight.yangtools.yang.data.codec.gson.JSONCodecFactorySupplier;
42 import org.opendaylight.yangtools.yang.data.codec.gson.JsonParserStream;
43 import org.opendaylight.yangtools.yang.data.impl.schema.ImmutableNormalizedNodeStreamWriter;
44 import org.opendaylight.yangtools.yang.data.impl.schema.NormalizationResultHolder;
45 import org.opendaylight.yangtools.yang.data.impl.schema.ResultAlreadySetException;
46 import org.opendaylight.yangtools.yang.data.util.DataSchemaContextTree;
47 import org.opendaylight.yangtools.yang.model.api.SchemaNode;
48 import org.opendaylight.yangtools.yang.model.api.meta.EffectiveStatement;
49 import org.opendaylight.yangtools.yang.model.util.SchemaInferenceStack.Inference;
50 import org.slf4j.Logger;
51 import org.slf4j.LoggerFactory;
54 @Consumes(MediaTypes.APPLICATION_YANG_PATCH_JSON)
55 public class JsonPatchBodyReader extends AbstractPatchBodyReader {
56 private static final Logger LOG = LoggerFactory.getLogger(JsonPatchBodyReader.class);
58 public JsonPatchBodyReader(final DatabindProvider databindProvider,
59 final DOMMountPointService mountPointService) {
60 super(databindProvider, mountPointService);
63 @SuppressWarnings("checkstyle:IllegalCatch")
65 protected PatchContext readBody(final InstanceIdentifierContext path, final InputStream entityStream)
66 throws WebApplicationException {
68 return readFrom(path, entityStream);
69 } catch (final Exception e) {
70 throw propagateExceptionAs(e);
74 private PatchContext readFrom(final InstanceIdentifierContext path, final InputStream entityStream)
76 try (var jsonReader = new JsonReader(new InputStreamReader(entityStream, StandardCharsets.UTF_8))) {
77 final var patchId = new AtomicReference<String>();
78 final var resultList = read(jsonReader, path, patchId);
79 return new PatchContext(path, resultList, patchId.get());
83 private static RestconfDocumentedException propagateExceptionAs(final Exception exception)
84 throws RestconfDocumentedException {
85 Throwables.throwIfInstanceOf(exception, RestconfDocumentedException.class);
86 LOG.debug("Error parsing json input", exception);
88 if (exception instanceof ResultAlreadySetException) {
89 throw new RestconfDocumentedException("Error parsing json input: Failed to create new parse result data. ");
92 RestconfDocumentedException.throwIfYangError(exception);
93 throw new RestconfDocumentedException("Error parsing json input: " + exception.getMessage(), ErrorType.PROTOCOL,
94 ErrorTag.MALFORMED_MESSAGE, exception);
97 private ImmutableList<PatchEntity> read(final JsonReader in, final InstanceIdentifierContext path,
98 final AtomicReference<String> patchId) throws IOException {
99 final var schemaTree = DataSchemaContextTree.from(path.getSchemaContext());
100 final var edits = ImmutableList.<PatchEntity>builder();
101 final var edit = new JsonPatchBodyReader.PatchEdit();
103 while (in.hasNext()) {
110 Boolean.toString(in.nextBoolean());
124 parseByName(in.nextName(), edit, in, path, schemaTree, edits, patchId);
138 return edits.build();
142 * Switch value of parsed JsonToken.NAME and read edit definition or patch id.
144 * @param name value of token
145 * @param edit PatchEdit instance
146 * @param in JsonReader reader
147 * @param path InstanceIdentifierContext context
148 * @param codec Draft11StringModuleInstanceIdentifierCodec codec
149 * @param resultCollection collection of parsed edits
150 * @throws IOException if operation fails
152 private void parseByName(final @NonNull String name, final @NonNull PatchEdit edit,
153 final @NonNull JsonReader in, final @NonNull InstanceIdentifierContext path,
154 final @NonNull DataSchemaContextTree schemaTree,
155 final ImmutableList.@NonNull Builder<PatchEntity> resultCollection,
156 final @NonNull AtomicReference<String> patchId) throws IOException {
159 if (in.peek() == JsonToken.BEGIN_ARRAY) {
162 while (in.hasNext()) {
163 readEditDefinition(edit, in, path, schemaTree);
164 resultCollection.add(prepareEditOperation(edit));
170 readEditDefinition(edit, in, path, schemaTree);
171 resultCollection.add(prepareEditOperation(edit));
177 patchId.set(in.nextString());
185 * Read one patch edit object from Json input.
187 * @param edit PatchEdit instance to be filled with read data
188 * @param in JsonReader reader
189 * @param path InstanceIdentifierContext path context
190 * @param codec Draft11StringModuleInstanceIdentifierCodec codec
191 * @throws IOException if operation fails
193 private void readEditDefinition(final @NonNull PatchEdit edit, final @NonNull JsonReader in,
194 final @NonNull InstanceIdentifierContext path,
195 final @NonNull DataSchemaContextTree schemaTree) throws IOException {
196 String deferredValue = null;
199 while (in.hasNext()) {
200 final String editDefinition = in.nextName();
201 switch (editDefinition) {
203 edit.setId(in.nextString());
206 edit.setOperation(Operation.ofName(in.nextString()));
209 // target can be specified completely in request URI
210 edit.setTarget(parsePatchTarget(path, in.nextString()));
211 final var stack = schemaTree.enterPath(edit.getTarget()).orElseThrow().stack();
212 if (!stack.isEmpty()) {
216 if (!stack.isEmpty()) {
217 final EffectiveStatement<?, ?> parentStmt = stack.currentStatement();
218 verify(parentStmt instanceof SchemaNode, "Unexpected parent %s", parentStmt);
220 edit.setTargetSchemaNode(stack.toInference());
224 checkArgument(edit.getData() == null && deferredValue == null, "Multiple value entries found");
226 if (edit.getTargetSchemaNode() == null) {
227 // save data defined in value node for next (later) processing, because target needs to be read
228 // always first and there is no ordering in Json input
229 deferredValue = readValueNode(in);
231 // We have a target schema node, reuse this reader without buffering the value.
232 edit.setData(readEditData(in, edit.getTargetSchemaNode(), path));
236 // FIXME: this does not look right, as it can wreck our logic
243 if (deferredValue != null) {
244 // read saved data to normalized node when target schema is already known
245 edit.setData(readEditData(new JsonReader(new StringReader(deferredValue)), edit.getTargetSchemaNode(),
251 * Parse data defined in value node and saves it to buffer.
252 * @param sb Buffer to read value node
253 * @param in JsonReader reader
254 * @throws IOException if operation fails
256 private String readValueNode(final @NonNull JsonReader in) throws IOException {
258 final StringBuilder sb = new StringBuilder().append("{\"").append(in.nextName()).append("\":");
265 while (in.hasNext()) {
266 if (in.peek() == JsonToken.STRING) {
267 sb.append('"').append(in.nextString()).append('"');
269 readValueObject(sb, in);
271 if (in.peek() != JsonToken.END_ARRAY) {
280 readValueObject(sb, in);
285 return sb.append('}').toString();
289 * Parse one value object of data and saves it to buffer.
290 * @param sb Buffer to read value object
291 * @param in JsonReader reader
292 * @throws IOException if operation fails
294 private void readValueObject(final @NonNull StringBuilder sb, final @NonNull JsonReader in) throws IOException {
295 // read simple leaf value
296 if (in.peek() == JsonToken.STRING) {
297 sb.append('"').append(in.nextString()).append('"');
304 while (in.hasNext()) {
305 sb.append('"').append(in.nextName()).append("\":");
309 sb.append('"').append(in.nextString()).append('"');
315 while (in.hasNext()) {
316 if (in.peek() == JsonToken.STRING) {
317 sb.append('"').append(in.nextString()).append('"');
319 readValueObject(sb, in);
322 if (in.peek() != JsonToken.END_ARRAY) {
331 readValueObject(sb, in);
334 if (in.peek() != JsonToken.END_OBJECT) {
344 * Read patch edit data defined in value node to NormalizedNode.
345 * @param in reader JsonReader reader
346 * @return NormalizedNode representing data
348 private static NormalizedNode readEditData(final @NonNull JsonReader in,
349 final @NonNull Inference targetSchemaNode, final @NonNull InstanceIdentifierContext path) {
350 final var resultHolder = new NormalizationResultHolder();
351 final var writer = ImmutableNormalizedNodeStreamWriter.from(resultHolder);
352 JsonParserStream.create(writer, JSONCodecFactorySupplier.RFC7951.getShared(path.getSchemaContext()),
353 targetSchemaNode).parse(in);
355 return resultHolder.getResult().data();
359 * Prepare PatchEntity from PatchEdit instance when it satisfies conditions, otherwise throws exception.
360 * @param edit Instance of PatchEdit
361 * @return PatchEntity Patch entity
363 private static PatchEntity prepareEditOperation(final @NonNull PatchEdit edit) {
364 if (edit.getOperation() != null && edit.getTargetSchemaNode() != null
365 && checkDataPresence(edit.getOperation(), edit.getData() != null)) {
366 if (!requiresValue(edit.getOperation())) {
367 return new PatchEntity(edit.getId(), edit.getOperation(), edit.getTarget());
370 // for lists allow to manipulate with list items through their parent
371 final YangInstanceIdentifier targetNode;
372 if (edit.getTarget().getLastPathArgument() instanceof NodeIdentifierWithPredicates) {
373 targetNode = edit.getTarget().getParent();
375 targetNode = edit.getTarget();
378 return new PatchEntity(edit.getId(), edit.getOperation(), targetNode, edit.getData());
381 throw new RestconfDocumentedException("Error parsing input", ErrorType.PROTOCOL, ErrorTag.MALFORMED_MESSAGE);
385 * Check if data is present when operation requires it and not present when operation data is not allowed.
386 * @param operation Name of operation
387 * @param hasData Data in edit are present/not present
388 * @return true if data is present when operation requires it or if there are no data when operation does not
389 * allow it, false otherwise
391 private static boolean checkDataPresence(final @NonNull Operation operation, final boolean hasData) {
392 return requiresValue(operation) == hasData;
396 * Helper class representing one patch edit.
398 private static final class PatchEdit {
400 private Operation operation;
401 private YangInstanceIdentifier target;
402 private Inference targetSchemaNode;
403 private NormalizedNode data;
409 void setId(final String id) {
410 this.id = requireNonNull(id);
413 Operation getOperation() {
417 void setOperation(final Operation operation) {
418 this.operation = requireNonNull(operation);
421 YangInstanceIdentifier getTarget() {
425 void setTarget(final YangInstanceIdentifier target) {
426 this.target = requireNonNull(target);
429 Inference getTargetSchemaNode() {
430 return targetSchemaNode;
433 void setTargetSchemaNode(final Inference targetSchemaNode) {
434 this.targetSchemaNode = requireNonNull(targetSchemaNode);
437 NormalizedNode getData() {
441 void setData(final NormalizedNode data) {
442 this.data = requireNonNull(data);
449 targetSchemaNode = null;