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