Bug 7987: Json HTTP PATCH: Problem parsing simple leaf value
[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         // read simple leaf value
304         if (in.peek() == JsonToken.STRING) {
305             value.append("\"" + in.nextString() + "\"");
306             return;
307         }
308
309         in.beginObject();
310         value.append("{");
311
312         while (in.hasNext()) {
313             value.append("\"" + in.nextName() + "\"");
314             value.append(":");
315
316             if (in.peek() == JsonToken.STRING) {
317                 value.append("\"" + in.nextString() + "\"");
318             } else {
319                 if (in.peek() == JsonToken.BEGIN_ARRAY) {
320                     in.beginArray();
321                     value.append("[");
322
323                     while (in.hasNext()) {
324                         if (in.peek() == JsonToken.STRING) {
325                             value.append("\"" + in.nextString() + "\"");
326                         } else {
327                             readValueObject(value, in);
328                         }
329                         if (in.peek() != JsonToken.END_ARRAY) {
330                             value.append(",");
331                         }
332                     }
333
334                     in.endArray();
335                     value.append("]");
336                 } else {
337                     readValueObject(value, in);
338                 }
339             }
340
341             if (in.peek() != JsonToken.END_OBJECT) {
342                 value.append(",");
343             }
344         }
345
346         in.endObject();
347         value.append("}");
348     }
349
350     /**
351      * Read patch edit data defined in value node to NormalizedNode
352      * @param in reader JsonReader reader
353      * @return NormalizedNode representing data
354      */
355     private static NormalizedNode readEditData(@Nonnull final JsonReader in, @Nonnull final SchemaNode targetSchemaNode,
356                                         @Nonnull final InstanceIdentifierContext path) {
357         final NormalizedNodeResult resultHolder = new NormalizedNodeResult();
358         final NormalizedNodeStreamWriter writer = ImmutableNormalizedNodeStreamWriter.from(resultHolder);
359         JsonParserStream.create(writer, path.getSchemaContext(), targetSchemaNode).parse(in);
360
361         return resultHolder.getResult();
362     }
363
364     /**
365      * Prepare PATCHEntity from PatchEdit instance when it satisfies conditions, otherwise throws exception
366      * @param edit Instance of PatchEdit
367      * @return PATCHEntity
368      */
369     private static PATCHEntity prepareEditOperation(@Nonnull final PatchEdit edit) {
370         if ((edit.getOperation() != null) && (edit.getTargetSchemaNode() != null)
371                 && checkDataPresence(edit.getOperation(), (edit.getData() != null))) {
372             if (isPatchOperationWithValue(edit.getOperation())) {
373                 // for lists allow to manipulate with list items through their parent
374                 final YangInstanceIdentifier targetNode;
375                 if (edit.getTarget().getLastPathArgument() instanceof NodeIdentifierWithPredicates) {
376                     targetNode = edit.getTarget().getParent();
377                 } else {
378                     targetNode = edit.getTarget();
379                 }
380
381                 return new PATCHEntity(edit.getId(), edit.getOperation(), targetNode, edit.getData());
382             } else {
383                 return new PATCHEntity(edit.getId(), edit.getOperation(), edit.getTarget());
384             }
385         }
386
387         throw new RestconfDocumentedException("Error parsing input", ErrorType.PROTOCOL, ErrorTag.MALFORMED_MESSAGE);
388     }
389
390     /**
391      * Check if data is present when operation requires it and not present when operation data is not allowed
392      * @param operation Name of operation
393      * @param hasData Data in edit are present/not present
394      * @return true if data is present when operation requires it or if there are no data when operation does not
395      * allow it, false otherwise
396      */
397     private static boolean checkDataPresence(@Nonnull final String operation, final boolean hasData) {
398         if (isPatchOperationWithValue(operation)) {
399             return hasData;
400         } else  {
401             return !hasData;
402         }
403     }
404
405     /**
406      * Helper class representing one patch edit
407      */
408     private static final class PatchEdit {
409         private String id;
410         private String operation;
411         private YangInstanceIdentifier target;
412         private SchemaNode targetSchemaNode;
413         private NormalizedNode data;
414
415         public String getId() {
416             return this.id;
417         }
418
419         public void setId(final String id) {
420             this.id = id;
421         }
422
423         public String getOperation() {
424             return this.operation;
425         }
426
427         public void setOperation(final String operation) {
428             this.operation = operation;
429         }
430
431         public YangInstanceIdentifier getTarget() {
432             return this.target;
433         }
434
435         public void setTarget(final YangInstanceIdentifier target) {
436             this.target = target;
437         }
438
439         public SchemaNode getTargetSchemaNode() {
440             return this.targetSchemaNode;
441         }
442
443         public void setTargetSchemaNode(final SchemaNode targetSchemaNode) {
444             this.targetSchemaNode = targetSchemaNode;
445         }
446
447         public NormalizedNode getData() {
448             return this.data;
449         }
450
451         public void setData(final NormalizedNode data) {
452             this.data = data;
453         }
454
455         public void clear() {
456             this.id = null;
457             this.operation = null;
458             this.target = null;
459             this.targetSchemaNode = null;
460             this.data = null;
461         }
462     }
463 }