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