Remove unused jsonPatchBodyReader.readFrom()
[netconf.git] / restconf / restconf-nb / src / main / java / org / opendaylight / restconf / nb / rfc8040 / jersey / providers / patch / JsonPatchBodyReader.java
1 /*
2  * Copyright (c) 2016 Cisco Systems, Inc. and others.  All rights reserved.
3  *
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
7  */
8 package org.opendaylight.restconf.nb.rfc8040.jersey.providers.patch;
9
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;
13
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;
52
53 @Provider
54 @Consumes(MediaTypes.APPLICATION_YANG_PATCH_JSON)
55 public class JsonPatchBodyReader extends AbstractPatchBodyReader {
56     private static final Logger LOG = LoggerFactory.getLogger(JsonPatchBodyReader.class);
57
58     public JsonPatchBodyReader(final DatabindProvider databindProvider,
59             final DOMMountPointService mountPointService) {
60         super(databindProvider, mountPointService);
61     }
62
63     @SuppressWarnings("checkstyle:IllegalCatch")
64     @Override
65     protected PatchContext readBody(final InstanceIdentifierContext path, final InputStream entityStream)
66             throws WebApplicationException {
67         try {
68             return readFrom(path, entityStream);
69         } catch (final Exception e) {
70             throw propagateExceptionAs(e);
71         }
72     }
73
74     private PatchContext readFrom(final InstanceIdentifierContext path, final InputStream entityStream)
75             throws IOException {
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());
80         }
81     }
82
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);
87
88         if (exception instanceof ResultAlreadySetException) {
89             throw new RestconfDocumentedException("Error parsing json input: Failed to create new parse result data. ");
90         }
91
92         RestconfDocumentedException.throwIfYangError(exception);
93         throw new RestconfDocumentedException("Error parsing json input: " + exception.getMessage(), ErrorType.PROTOCOL,
94             ErrorTag.MALFORMED_MESSAGE, exception);
95     }
96
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();
102
103         while (in.hasNext()) {
104             switch (in.peek()) {
105                 case STRING:
106                 case NUMBER:
107                     in.nextString();
108                     break;
109                 case BOOLEAN:
110                     Boolean.toString(in.nextBoolean());
111                     break;
112                 case NULL:
113                     in.nextNull();
114                     break;
115                 case BEGIN_ARRAY:
116                     in.beginArray();
117                     break;
118                 case BEGIN_OBJECT:
119                     in.beginObject();
120                     break;
121                 case END_DOCUMENT:
122                     break;
123                 case NAME:
124                     parseByName(in.nextName(), edit, in, path, schemaTree, edits, patchId);
125                     break;
126                 case END_OBJECT:
127                     in.endObject();
128                     break;
129                 case END_ARRAY:
130                     in.endArray();
131                     break;
132
133                 default:
134                     break;
135             }
136         }
137
138         return edits.build();
139     }
140
141     /**
142      * Switch value of parsed JsonToken.NAME and read edit definition or patch id.
143      *
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
151      */
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 {
157         switch (name) {
158             case "edit":
159                 if (in.peek() == JsonToken.BEGIN_ARRAY) {
160                     in.beginArray();
161
162                     while (in.hasNext()) {
163                         readEditDefinition(edit, in, path, schemaTree);
164                         resultCollection.add(prepareEditOperation(edit));
165                         edit.clear();
166                     }
167
168                     in.endArray();
169                 } else {
170                     readEditDefinition(edit, in, path, schemaTree);
171                     resultCollection.add(prepareEditOperation(edit));
172                     edit.clear();
173                 }
174
175                 break;
176             case "patch-id":
177                 patchId.set(in.nextString());
178                 break;
179             default:
180                 break;
181         }
182     }
183
184     /**
185      * Read one patch edit object from Json input.
186      *
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
192      */
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;
197         in.beginObject();
198
199         while (in.hasNext()) {
200             final String editDefinition = in.nextName();
201             switch (editDefinition) {
202                 case "edit-id":
203                     edit.setId(in.nextString());
204                     break;
205                 case "operation":
206                     edit.setOperation(Operation.ofName(in.nextString()));
207                     break;
208                 case "target":
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()) {
213                         stack.exit();
214                     }
215
216                     if (!stack.isEmpty()) {
217                         final EffectiveStatement<?, ?> parentStmt = stack.currentStatement();
218                         verify(parentStmt instanceof SchemaNode, "Unexpected parent %s", parentStmt);
219                     }
220                     edit.setTargetSchemaNode(stack.toInference());
221
222                     break;
223                 case "value":
224                     checkArgument(edit.getData() == null && deferredValue == null, "Multiple value entries found");
225
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);
230                     } else {
231                         // We have a target schema node, reuse this reader without buffering the value.
232                         edit.setData(readEditData(in, edit.getTargetSchemaNode(), path));
233                     }
234                     break;
235                 default:
236                     // FIXME: this does not look right, as it can wreck our logic
237                     break;
238             }
239         }
240
241         in.endObject();
242
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(),
246                 path));
247         }
248     }
249
250     /**
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
255      */
256     private String readValueNode(final @NonNull JsonReader in) throws IOException {
257         in.beginObject();
258         final StringBuilder sb = new StringBuilder().append("{\"").append(in.nextName()).append("\":");
259
260         switch (in.peek()) {
261             case BEGIN_ARRAY:
262                 in.beginArray();
263                 sb.append('[');
264
265                 while (in.hasNext()) {
266                     if (in.peek() == JsonToken.STRING) {
267                         sb.append('"').append(in.nextString()).append('"');
268                     } else {
269                         readValueObject(sb, in);
270                     }
271                     if (in.peek() != JsonToken.END_ARRAY) {
272                         sb.append(',');
273                     }
274                 }
275
276                 in.endArray();
277                 sb.append(']');
278                 break;
279             default:
280                 readValueObject(sb, in);
281                 break;
282         }
283
284         in.endObject();
285         return sb.append('}').toString();
286     }
287
288     /**
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
293      */
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('"');
298             return;
299         }
300
301         in.beginObject();
302         sb.append('{');
303
304         while (in.hasNext()) {
305             sb.append('"').append(in.nextName()).append("\":");
306
307             switch (in.peek()) {
308                 case STRING:
309                     sb.append('"').append(in.nextString()).append('"');
310                     break;
311                 case BEGIN_ARRAY:
312                     in.beginArray();
313                     sb.append('[');
314
315                     while (in.hasNext()) {
316                         if (in.peek() == JsonToken.STRING) {
317                             sb.append('"').append(in.nextString()).append('"');
318                         } else {
319                             readValueObject(sb, in);
320                         }
321
322                         if (in.peek() != JsonToken.END_ARRAY) {
323                             sb.append(',');
324                         }
325                     }
326
327                     in.endArray();
328                     sb.append(']');
329                     break;
330                 default:
331                     readValueObject(sb, in);
332             }
333
334             if (in.peek() != JsonToken.END_OBJECT) {
335                 sb.append(',');
336             }
337         }
338
339         in.endObject();
340         sb.append('}');
341     }
342
343     /**
344      * Read patch edit data defined in value node to NormalizedNode.
345      * @param in reader JsonReader reader
346      * @return NormalizedNode representing data
347      */
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);
354
355         return resultHolder.getResult().data();
356     }
357
358     /**
359      * Prepare PatchEntity from PatchEdit instance when it satisfies conditions, otherwise throws exception.
360      * @param edit Instance of PatchEdit
361      * @return PatchEntity Patch entity
362      */
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());
368             }
369
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();
374             } else {
375                 targetNode = edit.getTarget();
376             }
377
378             return new PatchEntity(edit.getId(), edit.getOperation(), targetNode, edit.getData());
379         }
380
381         throw new RestconfDocumentedException("Error parsing input", ErrorType.PROTOCOL, ErrorTag.MALFORMED_MESSAGE);
382     }
383
384     /**
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
390      */
391     private static boolean checkDataPresence(final @NonNull Operation operation, final boolean hasData) {
392         return requiresValue(operation)  == hasData;
393     }
394
395     /**
396      * Helper class representing one patch edit.
397      */
398     private static final class PatchEdit {
399         private String id;
400         private Operation operation;
401         private YangInstanceIdentifier target;
402         private Inference targetSchemaNode;
403         private NormalizedNode data;
404
405         String getId() {
406             return id;
407         }
408
409         void setId(final String id) {
410             this.id = requireNonNull(id);
411         }
412
413         Operation getOperation() {
414             return operation;
415         }
416
417         void setOperation(final Operation operation) {
418             this.operation = requireNonNull(operation);
419         }
420
421         YangInstanceIdentifier getTarget() {
422             return target;
423         }
424
425         void setTarget(final YangInstanceIdentifier target) {
426             this.target = requireNonNull(target);
427         }
428
429         Inference getTargetSchemaNode() {
430             return targetSchemaNode;
431         }
432
433         void setTargetSchemaNode(final Inference targetSchemaNode) {
434             this.targetSchemaNode = requireNonNull(targetSchemaNode);
435         }
436
437         NormalizedNode getData() {
438             return data;
439         }
440
441         void setData(final NormalizedNode data) {
442             this.data = requireNonNull(data);
443         }
444
445         void clear() {
446             id = null;
447             operation = null;
448             target = null;
449             targetSchemaNode = null;
450             data = null;
451         }
452     }
453 }