Remove RestconfError.ErrorTag
[netconf.git] / restconf / restconf-nb-rfc8040 / 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.ArrayList;
24 import java.util.List;
25 import java.util.Locale;
26 import java.util.Optional;
27 import java.util.concurrent.atomic.AtomicReference;
28 import javax.ws.rs.Consumes;
29 import javax.ws.rs.WebApplicationException;
30 import javax.ws.rs.ext.Provider;
31 import org.eclipse.jdt.annotation.NonNull;
32 import org.opendaylight.mdsal.dom.api.DOMMountPointService;
33 import org.opendaylight.restconf.common.context.InstanceIdentifierContext;
34 import org.opendaylight.restconf.common.errors.RestconfDocumentedException;
35 import org.opendaylight.restconf.common.patch.PatchContext;
36 import org.opendaylight.restconf.common.patch.PatchEditOperation;
37 import org.opendaylight.restconf.common.patch.PatchEntity;
38 import org.opendaylight.restconf.nb.rfc8040.MediaTypes;
39 import org.opendaylight.restconf.nb.rfc8040.handlers.SchemaContextHandler;
40 import org.opendaylight.restconf.nb.rfc8040.utils.parser.ParserIdentifier;
41 import org.opendaylight.yangtools.yang.common.ErrorTag;
42 import org.opendaylight.yangtools.yang.common.ErrorType;
43 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
44 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifierWithPredicates;
45 import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode;
46 import org.opendaylight.yangtools.yang.data.api.schema.stream.NormalizedNodeStreamWriter;
47 import org.opendaylight.yangtools.yang.data.codec.gson.JSONCodecFactorySupplier;
48 import org.opendaylight.yangtools.yang.data.codec.gson.JsonParserStream;
49 import org.opendaylight.yangtools.yang.data.impl.schema.ImmutableNormalizedNodeStreamWriter;
50 import org.opendaylight.yangtools.yang.data.impl.schema.NormalizedNodeResult;
51 import org.opendaylight.yangtools.yang.data.impl.schema.ResultAlreadySetException;
52 import org.opendaylight.yangtools.yang.data.util.DataSchemaContextTree;
53 import org.opendaylight.yangtools.yang.model.api.SchemaNode;
54 import org.opendaylight.yangtools.yang.model.api.meta.EffectiveStatement;
55 import org.opendaylight.yangtools.yang.model.util.SchemaInferenceStack;
56 import org.slf4j.Logger;
57 import org.slf4j.LoggerFactory;
58
59 @Provider
60 @Consumes(MediaTypes.APPLICATION_YANG_PATCH_JSON)
61 public class JsonPatchBodyReader extends AbstractPatchBodyReader {
62     private static final Logger LOG = LoggerFactory.getLogger(JsonPatchBodyReader.class);
63
64     public JsonPatchBodyReader(final SchemaContextHandler schemaContextHandler,
65             final DOMMountPointService mountPointService) {
66         super(schemaContextHandler, mountPointService);
67     }
68
69     @SuppressWarnings("checkstyle:IllegalCatch")
70     @Override
71     protected PatchContext readBody(final InstanceIdentifierContext<?> path, final InputStream entityStream)
72             throws WebApplicationException {
73         try {
74             return readFrom(path, entityStream);
75         } catch (final Exception e) {
76             throw propagateExceptionAs(e);
77         }
78     }
79
80     private PatchContext readFrom(final InstanceIdentifierContext<?> path, final InputStream entityStream)
81             throws IOException {
82         final JsonReader jsonReader = new JsonReader(new InputStreamReader(entityStream, StandardCharsets.UTF_8));
83         AtomicReference<String> patchId = new AtomicReference<>();
84         final List<PatchEntity> resultList = read(jsonReader, path, patchId);
85         jsonReader.close();
86
87         return new PatchContext(path, resultList, patchId.get());
88     }
89
90     @SuppressWarnings("checkstyle:IllegalCatch")
91     public PatchContext readFrom(final String uriPath, final InputStream entityStream) throws
92             RestconfDocumentedException {
93         try {
94             return readFrom(
95                     ParserIdentifier.toInstanceIdentifier(uriPath, getSchemaContext(),
96                             Optional.ofNullable(getMountPointService())), entityStream);
97         } catch (final Exception e) {
98             propagateExceptionAs(e);
99             return null; // no-op
100         }
101     }
102
103     private static RuntimeException propagateExceptionAs(final Exception exception) throws RestconfDocumentedException {
104         Throwables.throwIfInstanceOf(exception, RestconfDocumentedException.class);
105         LOG.debug("Error parsing json input", exception);
106
107         if (exception instanceof ResultAlreadySetException) {
108             throw new RestconfDocumentedException("Error parsing json input: Failed to create new parse result data. ");
109         }
110
111         RestconfDocumentedException.throwIfYangError(exception);
112         throw new RestconfDocumentedException("Error parsing json input: " + exception.getMessage(), ErrorType.PROTOCOL,
113             ErrorTag.MALFORMED_MESSAGE, exception);
114     }
115
116     private List<PatchEntity> read(final JsonReader in, final InstanceIdentifierContext<?> path,
117             final AtomicReference<String> patchId) throws IOException {
118         final DataSchemaContextTree schemaTree = DataSchemaContextTree.from(path.getSchemaContext());
119         final List<PatchEntity> resultCollection = new ArrayList<>();
120         final JsonPatchBodyReader.PatchEdit edit = new JsonPatchBodyReader.PatchEdit();
121
122         while (in.hasNext()) {
123             switch (in.peek()) {
124                 case STRING:
125                 case NUMBER:
126                     in.nextString();
127                     break;
128                 case BOOLEAN:
129                     Boolean.toString(in.nextBoolean());
130                     break;
131                 case NULL:
132                     in.nextNull();
133                     break;
134                 case BEGIN_ARRAY:
135                     in.beginArray();
136                     break;
137                 case BEGIN_OBJECT:
138                     in.beginObject();
139                     break;
140                 case END_DOCUMENT:
141                     break;
142                 case NAME:
143                     parseByName(in.nextName(), edit, in, path, schemaTree, resultCollection, patchId);
144                     break;
145                 case END_OBJECT:
146                     in.endObject();
147                     break;
148                 case END_ARRAY:
149                     in.endArray();
150                     break;
151
152                 default:
153                     break;
154             }
155         }
156
157         return ImmutableList.copyOf(resultCollection);
158     }
159
160     /**
161      * Switch value of parsed JsonToken.NAME and read edit definition or patch id.
162      *
163      * @param name value of token
164      * @param edit PatchEdit instance
165      * @param in JsonReader reader
166      * @param path InstanceIdentifierContext context
167      * @param codec Draft11StringModuleInstanceIdentifierCodec codec
168      * @param resultCollection collection of parsed edits
169      * @throws IOException if operation fails
170      */
171     private void parseByName(final @NonNull String name, final @NonNull PatchEdit edit,
172                              final @NonNull JsonReader in, final @NonNull InstanceIdentifierContext<?> path,
173                              final @NonNull DataSchemaContextTree schemaTree,
174                              final @NonNull List<PatchEntity> resultCollection,
175                              final @NonNull AtomicReference<String> patchId) throws IOException {
176         switch (name) {
177             case "edit":
178                 if (in.peek() == JsonToken.BEGIN_ARRAY) {
179                     in.beginArray();
180
181                     while (in.hasNext()) {
182                         readEditDefinition(edit, in, path, schemaTree);
183                         resultCollection.add(prepareEditOperation(edit));
184                         edit.clear();
185                     }
186
187                     in.endArray();
188                 } else {
189                     readEditDefinition(edit, in, path, schemaTree);
190                     resultCollection.add(prepareEditOperation(edit));
191                     edit.clear();
192                 }
193
194                 break;
195             case "patch-id":
196                 patchId.set(in.nextString());
197                 break;
198             default:
199                 break;
200         }
201     }
202
203     /**
204      * Read one patch edit object from Json input.
205      *
206      * @param edit PatchEdit instance to be filled with read data
207      * @param in JsonReader reader
208      * @param path InstanceIdentifierContext path context
209      * @param codec Draft11StringModuleInstanceIdentifierCodec codec
210      * @throws IOException if operation fails
211      */
212     private void readEditDefinition(final @NonNull PatchEdit edit, final @NonNull JsonReader in,
213                                     final @NonNull InstanceIdentifierContext<?> path,
214                                     final @NonNull DataSchemaContextTree schemaTree) throws IOException {
215         String deferredValue = null;
216         in.beginObject();
217
218         while (in.hasNext()) {
219             final String editDefinition = in.nextName();
220             switch (editDefinition) {
221                 case "edit-id":
222                     edit.setId(in.nextString());
223                     break;
224                 case "operation":
225                     edit.setOperation(PatchEditOperation.valueOf(in.nextString().toUpperCase(Locale.ROOT)));
226                     break;
227                 case "target":
228                     // target can be specified completely in request URI
229                     final String target = in.nextString();
230                     if (target.equals("/")) {
231                         edit.setTarget(path.getInstanceIdentifier());
232                         edit.setTargetSchemaNode(path.getSchemaContext());
233                     } else {
234                         edit.setTarget(ParserIdentifier.parserPatchTarget(path, target));
235
236                         final EffectiveStatement<?, ?> parentStmt = SchemaInferenceStack.ofInstantiatedPath(
237                             path.getSchemaContext(),
238                             schemaTree.findChild(edit.getTarget()).orElseThrow().getDataSchemaNode()
239                                 .getPath().getParent())
240                             .currentStatement();
241                         verify(parentStmt instanceof SchemaNode, "Unexpected parent %s", parentStmt);
242                         edit.setTargetSchemaNode((SchemaNode) parentStmt);
243                     }
244
245                     break;
246                 case "value":
247                     checkArgument(edit.getData() == null && deferredValue == null, "Multiple value entries found");
248
249                     if (edit.getTargetSchemaNode() == null) {
250                         // save data defined in value node for next (later) processing, because target needs to be read
251                         // always first and there is no ordering in Json input
252                         deferredValue = readValueNode(in);
253                     } else {
254                         // We have a target schema node, reuse this reader without buffering the value.
255                         edit.setData(readEditData(in, edit.getTargetSchemaNode(), path));
256                     }
257                     break;
258                 default:
259                     // FIXME: this does not look right, as it can wreck our logic
260                     break;
261             }
262         }
263
264         in.endObject();
265
266         if (deferredValue != null) {
267             // read saved data to normalized node when target schema is already known
268             edit.setData(readEditData(new JsonReader(new StringReader(deferredValue)), edit.getTargetSchemaNode(),
269                 path));
270         }
271     }
272
273     /**
274      * Parse data defined in value node and saves it to buffer.
275      * @param sb Buffer to read value node
276      * @param in JsonReader reader
277      * @throws IOException if operation fails
278      */
279     private String readValueNode(final @NonNull JsonReader in) throws IOException {
280         in.beginObject();
281         final StringBuilder sb = new StringBuilder().append("{\"").append(in.nextName()).append("\":");
282
283         switch (in.peek()) {
284             case BEGIN_ARRAY:
285                 in.beginArray();
286                 sb.append('[');
287
288                 while (in.hasNext()) {
289                     if (in.peek() == JsonToken.STRING) {
290                         sb.append('"').append(in.nextString()).append('"');
291                     } else {
292                         readValueObject(sb, in);
293                     }
294                     if (in.peek() != JsonToken.END_ARRAY) {
295                         sb.append(',');
296                     }
297                 }
298
299                 in.endArray();
300                 sb.append(']');
301                 break;
302             default:
303                 readValueObject(sb, in);
304                 break;
305         }
306
307         in.endObject();
308         return sb.append('}').toString();
309     }
310
311     /**
312      * Parse one value object of data and saves it to buffer.
313      * @param sb Buffer to read value object
314      * @param in JsonReader reader
315      * @throws IOException if operation fails
316      */
317     private void readValueObject(final @NonNull StringBuilder sb, final @NonNull JsonReader in) throws IOException {
318         // read simple leaf value
319         if (in.peek() == JsonToken.STRING) {
320             sb.append('"').append(in.nextString()).append('"');
321             return;
322         }
323
324         in.beginObject();
325         sb.append('{');
326
327         while (in.hasNext()) {
328             sb.append('"').append(in.nextName()).append("\":");
329
330             switch (in.peek()) {
331                 case STRING:
332                     sb.append('"').append(in.nextString()).append('"');
333                     break;
334                 case BEGIN_ARRAY:
335                     in.beginArray();
336                     sb.append('[');
337
338                     while (in.hasNext()) {
339                         if (in.peek() == JsonToken.STRING) {
340                             sb.append('"').append(in.nextString()).append('"');
341                         } else {
342                             readValueObject(sb, in);
343                         }
344
345                         if (in.peek() != JsonToken.END_ARRAY) {
346                             sb.append(',');
347                         }
348                     }
349
350                     in.endArray();
351                     sb.append(']');
352                     break;
353                 default:
354                     readValueObject(sb, in);
355             }
356
357             if (in.peek() != JsonToken.END_OBJECT) {
358                 sb.append(',');
359             }
360         }
361
362         in.endObject();
363         sb.append('}');
364     }
365
366     /**
367      * Read patch edit data defined in value node to NormalizedNode.
368      * @param in reader JsonReader reader
369      * @return NormalizedNode representing data
370      */
371     private static NormalizedNode readEditData(final @NonNull JsonReader in,
372              final @NonNull SchemaNode targetSchemaNode, final @NonNull InstanceIdentifierContext<?> path) {
373         final NormalizedNodeResult resultHolder = new NormalizedNodeResult();
374         final NormalizedNodeStreamWriter writer = ImmutableNormalizedNodeStreamWriter.from(resultHolder);
375         JsonParserStream.create(writer, JSONCodecFactorySupplier.RFC7951.getShared(path.getSchemaContext()),
376             SchemaInferenceStack.ofInstantiatedPath(path.getSchemaContext(), targetSchemaNode.getPath()).toInference())
377             .parse(in);
378
379         return resultHolder.getResult();
380     }
381
382     /**
383      * Prepare PatchEntity from PatchEdit instance when it satisfies conditions, otherwise throws exception.
384      * @param edit Instance of PatchEdit
385      * @return PatchEntity Patch entity
386      */
387     private static PatchEntity prepareEditOperation(final @NonNull PatchEdit edit) {
388         if (edit.getOperation() != null && edit.getTargetSchemaNode() != null
389                 && checkDataPresence(edit.getOperation(), edit.getData() != null)) {
390             if (!edit.getOperation().isWithValue()) {
391                 return new PatchEntity(edit.getId(), edit.getOperation(), edit.getTarget());
392             }
393
394             // for lists allow to manipulate with list items through their parent
395             final YangInstanceIdentifier targetNode;
396             if (edit.getTarget().getLastPathArgument() instanceof NodeIdentifierWithPredicates) {
397                 targetNode = edit.getTarget().getParent();
398             } else {
399                 targetNode = edit.getTarget();
400             }
401
402             return new PatchEntity(edit.getId(), edit.getOperation(), targetNode, edit.getData());
403         }
404
405         throw new RestconfDocumentedException("Error parsing input", ErrorType.PROTOCOL, ErrorTag.MALFORMED_MESSAGE);
406     }
407
408     /**
409      * Check if data is present when operation requires it and not present when operation data is not allowed.
410      * @param operation Name of operation
411      * @param hasData Data in edit are present/not present
412      * @return true if data is present when operation requires it or if there are no data when operation does not
413      *     allow it, false otherwise
414      */
415     private static boolean checkDataPresence(final @NonNull PatchEditOperation operation, final boolean hasData) {
416         return operation.isWithValue() == hasData;
417     }
418
419     /**
420      * Helper class representing one patch edit.
421      */
422     private static final class PatchEdit {
423         private String id;
424         private PatchEditOperation operation;
425         private YangInstanceIdentifier target;
426         private SchemaNode targetSchemaNode;
427         private NormalizedNode data;
428
429         String getId() {
430             return id;
431         }
432
433         void setId(final String id) {
434             this.id = requireNonNull(id);
435         }
436
437         PatchEditOperation getOperation() {
438             return operation;
439         }
440
441         void setOperation(final PatchEditOperation operation) {
442             this.operation = requireNonNull(operation);
443         }
444
445         YangInstanceIdentifier getTarget() {
446             return target;
447         }
448
449         void setTarget(final YangInstanceIdentifier target) {
450             this.target = requireNonNull(target);
451         }
452
453         SchemaNode getTargetSchemaNode() {
454             return targetSchemaNode;
455         }
456
457         void setTargetSchemaNode(final SchemaNode targetSchemaNode) {
458             this.targetSchemaNode = requireNonNull(targetSchemaNode);
459         }
460
461         NormalizedNode getData() {
462             return data;
463         }
464
465         void setData(final NormalizedNode data) {
466             this.data = requireNonNull(data);
467         }
468
469         void clear() {
470             id = null;
471             operation = null;
472             target = null;
473             targetSchemaNode = null;
474             data = null;
475         }
476     }
477 }