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 java.util.Objects.requireNonNull;
13 import com.google.common.base.Throwables;
14 import com.google.common.collect.ImmutableList;
15 import com.google.gson.stream.JsonReader;
16 import com.google.gson.stream.JsonToken;
17 import java.io.IOException;
18 import java.io.InputStream;
19 import java.io.InputStreamReader;
20 import java.io.StringReader;
21 import java.nio.charset.StandardCharsets;
22 import java.util.ArrayList;
23 import java.util.List;
24 import java.util.Locale;
25 import java.util.Optional;
26 import java.util.concurrent.atomic.AtomicReference;
27 import javax.ws.rs.Consumes;
28 import javax.ws.rs.WebApplicationException;
29 import javax.ws.rs.ext.Provider;
30 import org.eclipse.jdt.annotation.NonNull;
31 import org.opendaylight.mdsal.dom.api.DOMMountPointService;
32 import org.opendaylight.restconf.common.context.InstanceIdentifierContext;
33 import org.opendaylight.restconf.common.errors.RestconfDocumentedException;
34 import org.opendaylight.restconf.common.patch.PatchContext;
35 import org.opendaylight.restconf.common.patch.PatchEditOperation;
36 import org.opendaylight.restconf.common.patch.PatchEntity;
37 import org.opendaylight.restconf.nb.rfc8040.MediaTypes;
38 import org.opendaylight.restconf.nb.rfc8040.handlers.SchemaContextHandler;
39 import org.opendaylight.restconf.nb.rfc8040.utils.parser.ParserIdentifier;
40 import org.opendaylight.yangtools.yang.common.ErrorTag;
41 import org.opendaylight.yangtools.yang.common.ErrorType;
42 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
43 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifierWithPredicates;
44 import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode;
45 import org.opendaylight.yangtools.yang.data.api.schema.stream.NormalizedNodeStreamWriter;
46 import org.opendaylight.yangtools.yang.data.codec.gson.JSONCodecFactorySupplier;
47 import org.opendaylight.yangtools.yang.data.codec.gson.JsonParserStream;
48 import org.opendaylight.yangtools.yang.data.impl.schema.ImmutableNormalizedNodeStreamWriter;
49 import org.opendaylight.yangtools.yang.data.impl.schema.NormalizedNodeResult;
50 import org.opendaylight.yangtools.yang.data.impl.schema.ResultAlreadySetException;
51 import org.opendaylight.yangtools.yang.model.util.SchemaInferenceStack;
52 import org.opendaylight.yangtools.yang.model.util.SchemaInferenceStack.Inference;
53 import org.slf4j.Logger;
54 import org.slf4j.LoggerFactory;
57 @Consumes(MediaTypes.APPLICATION_YANG_PATCH_JSON)
58 public class JsonPatchBodyReader extends AbstractPatchBodyReader {
59 private static final Logger LOG = LoggerFactory.getLogger(JsonPatchBodyReader.class);
61 public JsonPatchBodyReader(final SchemaContextHandler schemaContextHandler,
62 final DOMMountPointService mountPointService) {
63 super(schemaContextHandler, mountPointService);
66 @SuppressWarnings("checkstyle:IllegalCatch")
68 protected PatchContext readBody(final InstanceIdentifierContext<?> path, final InputStream entityStream)
69 throws WebApplicationException {
71 return readFrom(path, entityStream);
72 } catch (final Exception e) {
73 throw propagateExceptionAs(e);
77 private PatchContext readFrom(final InstanceIdentifierContext<?> path, final InputStream entityStream)
79 final JsonReader jsonReader = new JsonReader(new InputStreamReader(entityStream, StandardCharsets.UTF_8));
80 AtomicReference<String> patchId = new AtomicReference<>();
81 final List<PatchEntity> resultList = read(jsonReader, path, patchId);
84 return new PatchContext(path, resultList, patchId.get());
87 @SuppressWarnings("checkstyle:IllegalCatch")
88 public PatchContext readFrom(final String uriPath, final InputStream entityStream) throws
89 RestconfDocumentedException {
92 ParserIdentifier.toInstanceIdentifier(uriPath, getSchemaContext(),
93 Optional.ofNullable(getMountPointService())), entityStream);
94 } catch (final Exception e) {
95 propagateExceptionAs(e);
100 private static RuntimeException propagateExceptionAs(final Exception exception) throws RestconfDocumentedException {
101 Throwables.throwIfInstanceOf(exception, RestconfDocumentedException.class);
102 LOG.debug("Error parsing json input", exception);
104 if (exception instanceof ResultAlreadySetException) {
105 throw new RestconfDocumentedException("Error parsing json input: Failed to create new parse result data. ");
108 RestconfDocumentedException.throwIfYangError(exception);
109 throw new RestconfDocumentedException("Error parsing json input: " + exception.getMessage(), ErrorType.PROTOCOL,
110 ErrorTag.MALFORMED_MESSAGE, exception);
113 private List<PatchEntity> read(final JsonReader in, final InstanceIdentifierContext<?> path,
114 final AtomicReference<String> patchId) throws IOException {
115 final List<PatchEntity> resultCollection = new ArrayList<>();
116 final JsonPatchBodyReader.PatchEdit edit = new JsonPatchBodyReader.PatchEdit();
118 while (in.hasNext()) {
125 Boolean.toString(in.nextBoolean());
139 parseByName(in.nextName(), edit, in, path, resultCollection, patchId);
153 return ImmutableList.copyOf(resultCollection);
157 * Switch value of parsed JsonToken.NAME and read edit definition or patch id.
159 * @param name value of token
160 * @param edit PatchEdit instance
161 * @param in JsonReader reader
162 * @param path InstanceIdentifierContext context
163 * @param resultCollection collection of parsed edits
164 * @param patchId id of edit patch
165 * @throws IOException if operation fails
167 private void parseByName(final @NonNull String name, final @NonNull PatchEdit edit,
168 final @NonNull JsonReader in, final @NonNull InstanceIdentifierContext<?> path,
169 final @NonNull List<PatchEntity> resultCollection,
170 final @NonNull AtomicReference<String> patchId) throws IOException {
173 if (in.peek() == JsonToken.BEGIN_ARRAY) {
176 while (in.hasNext()) {
177 readEditDefinition(edit, in, path);
178 resultCollection.add(prepareEditOperation(edit));
184 readEditDefinition(edit, in, path);
185 resultCollection.add(prepareEditOperation(edit));
191 patchId.set(in.nextString());
199 * Read one patch edit object from Json input.
201 * @param edit PatchEdit instance to be filled with read data
202 * @param in JsonReader reader
203 * @param path InstanceIdentifierContext path context
204 * @throws IOException if operation fails
206 private void readEditDefinition(final @NonNull PatchEdit edit, final @NonNull JsonReader in,
207 final @NonNull InstanceIdentifierContext<?> path) throws IOException {
208 String deferredValue = null;
211 while (in.hasNext()) {
212 final String editDefinition = in.nextName();
213 switch (editDefinition) {
215 edit.setId(in.nextString());
218 edit.setOperation(PatchEditOperation.valueOf(in.nextString().toUpperCase(Locale.ROOT)));
221 // target can be specified completely in request URI
222 final String target = in.nextString();
223 final SchemaInferenceStack stack = SchemaInferenceStack.of(path.getSchemaContext());
224 if (target.equals("/")) {
225 edit.setTarget(path.getInstanceIdentifier());
226 edit.setTargetInference(stack.toInference());
228 edit.setTarget(ParserIdentifier.parserPatchTarget(path, target));
229 edit.getTarget().getPathArguments().stream()
230 .filter(arg -> !(arg instanceof YangInstanceIdentifier.NodeIdentifierWithPredicates))
231 .filter(arg -> !(arg instanceof YangInstanceIdentifier.AugmentationIdentifier))
232 .forEach(p -> stack.enterSchemaTree(p.getNodeType()));
234 edit.setTargetInference(stack.toInference());
238 checkArgument(edit.getData() == null && deferredValue == null, "Multiple value entries found");
240 if (edit.getTargetInference() == null) {
241 // save data defined in value node for next (later) processing, because target needs to be read
242 // always first and there is no ordering in Json input
243 deferredValue = readValueNode(in);
245 // We have a target schema node, reuse this reader without buffering the value.
246 edit.setData(readEditData(in, edit.getTargetInference(), path));
250 // FIXME: this does not look right, as it can wreck our logic
257 if (deferredValue != null) {
258 // read saved data to normalized node when target schema is already known
259 edit.setData(readEditData(new JsonReader(new StringReader(deferredValue)), edit.getTargetInference(),
265 * Parse data defined in value node and saves it to buffer.
266 * @param in JsonReader reader
267 * @throws IOException if operation fails
269 private String readValueNode(final @NonNull JsonReader in) throws IOException {
271 final StringBuilder sb = new StringBuilder().append("{\"").append(in.nextName()).append("\":");
278 while (in.hasNext()) {
279 if (in.peek() == JsonToken.STRING) {
280 sb.append('"').append(in.nextString()).append('"');
282 readValueObject(sb, in);
284 if (in.peek() != JsonToken.END_ARRAY) {
293 readValueObject(sb, in);
298 return sb.append('}').toString();
302 * Parse one value object of data and saves it to buffer.
303 * @param sb Buffer to read value object
304 * @param in JsonReader reader
305 * @throws IOException if operation fails
307 private void readValueObject(final @NonNull StringBuilder sb, final @NonNull JsonReader in) throws IOException {
308 // read simple leaf value
309 if (in.peek() == JsonToken.STRING) {
310 sb.append('"').append(in.nextString()).append('"');
317 while (in.hasNext()) {
318 sb.append('"').append(in.nextName()).append("\":");
322 sb.append('"').append(in.nextString()).append('"');
328 while (in.hasNext()) {
329 if (in.peek() == JsonToken.STRING) {
330 sb.append('"').append(in.nextString()).append('"');
332 readValueObject(sb, in);
335 if (in.peek() != JsonToken.END_ARRAY) {
344 readValueObject(sb, in);
347 if (in.peek() != JsonToken.END_OBJECT) {
357 * Read patch edit data defined in value node to NormalizedNode.
358 * @param in reader JsonReader reader
359 * @return NormalizedNode representing data
361 private static NormalizedNode readEditData(final @NonNull JsonReader in,
362 final @NonNull Inference inference, final @NonNull InstanceIdentifierContext<?> path) {
363 final NormalizedNodeResult resultHolder = new NormalizedNodeResult();
364 final NormalizedNodeStreamWriter writer = ImmutableNormalizedNodeStreamWriter.from(resultHolder);
365 JsonParserStream.create(writer, JSONCodecFactorySupplier.RFC7951.getShared(path.getSchemaContext()), inference)
368 return resultHolder.getResult();
372 * Prepare PatchEntity from PatchEdit instance when it satisfies conditions, otherwise throws exception.
373 * @param edit Instance of PatchEdit
374 * @return PatchEntity Patch entity
376 private static PatchEntity prepareEditOperation(final @NonNull PatchEdit edit) {
377 if (edit.getOperation() != null && edit.getTargetInference() != null
378 && checkDataPresence(edit.getOperation(), edit.getData() != null)) {
379 if (!edit.getOperation().isWithValue()) {
380 return new PatchEntity(edit.getId(), edit.getOperation(), edit.getTarget());
383 // for lists allow to manipulate with list items through their parent
384 final YangInstanceIdentifier targetNode;
385 if (edit.getTarget().getLastPathArgument() instanceof NodeIdentifierWithPredicates) {
386 targetNode = edit.getTarget().getParent();
388 targetNode = edit.getTarget();
391 return new PatchEntity(edit.getId(), edit.getOperation(), targetNode, edit.getData());
394 throw new RestconfDocumentedException("Error parsing input", ErrorType.PROTOCOL, ErrorTag.MALFORMED_MESSAGE);
398 * Check if data is present when operation requires it and not present when operation data is not allowed.
399 * @param operation Name of operation
400 * @param hasData Data in edit are present/not present
401 * @return true if data is present when operation requires it or if there are no data when operation does not
402 * allow it, false otherwise
404 private static boolean checkDataPresence(final @NonNull PatchEditOperation operation, final boolean hasData) {
405 return operation.isWithValue() == hasData;
409 * Helper class representing one patch edit.
411 private static final class PatchEdit {
413 private PatchEditOperation operation;
414 private YangInstanceIdentifier target;
415 private Inference targetInference;
416 private NormalizedNode data;
422 void setId(final String id) {
423 this.id = requireNonNull(id);
426 PatchEditOperation getOperation() {
430 void setOperation(final PatchEditOperation operation) {
431 this.operation = requireNonNull(operation);
434 YangInstanceIdentifier getTarget() {
438 void setTarget(final YangInstanceIdentifier target) {
439 this.target = requireNonNull(target);
442 Inference getTargetInference() {
443 return targetInference;
446 void setTargetInference(final Inference targetInference) {
447 this.targetInference = requireNonNull(targetInference);
450 NormalizedNode getData() {
454 void setData(final NormalizedNode data) {
455 this.data = requireNonNull(data);
462 targetInference = null;