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