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