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