a9aa44c2c9f8c414af69564d850127e62be406ea
[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.restconf.utils.patch.Draft11JsonToPATCHBodyReader;
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.JsonParserStream;
46 import org.opendaylight.yangtools.yang.data.impl.schema.ImmutableNormalizedNodeStreamWriter;
47 import org.opendaylight.yangtools.yang.data.impl.schema.NormalizedNodeResult;
48 import org.opendaylight.yangtools.yang.data.impl.schema.ResultAlreadySetException;
49 import org.opendaylight.yangtools.yang.model.api.SchemaNode;
50 import org.opendaylight.yangtools.yang.model.util.SchemaContextUtil;
51 import org.slf4j.Logger;
52 import org.slf4j.LoggerFactory;
53
54 /**
55  * @deprecated This class will be replaced by {@link Draft11JsonToPATCHBodyReader}
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, 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                 readValueObject(value, in);
277                 if (in.peek() != JsonToken.END_ARRAY) {
278                     value.append(",");
279                 }
280             }
281
282             in.endArray();
283             value.append("]");
284         } else {
285             readValueObject(value, in);
286         }
287
288         in.endObject();
289         value.append("}");
290     }
291
292     /**
293      * Parse one value object of data and saves it to buffer
294      * @param value Buffer to read value object
295      * @param in JsonReader reader
296      * @throws IOException
297      */
298     private void readValueObject(@Nonnull final StringBuffer value, @Nonnull final JsonReader in) throws IOException {
299         in.beginObject();
300         value.append("{");
301
302         while (in.hasNext()) {
303             value.append("\"" + in.nextName() + "\"");
304             value.append(":");
305
306             if (in.peek() == JsonToken.STRING) {
307                 value.append("\"" + in.nextString() + "\"");
308             } else {
309                 if (in.peek() == JsonToken.BEGIN_ARRAY) {
310                     in.beginArray();
311                     value.append("[");
312
313                     while (in.hasNext()) {
314                         readValueObject(value, in);
315                         if (in.peek() != JsonToken.END_ARRAY) {
316                             value.append(",");
317                         }
318                     }
319
320                     in.endArray();
321                     value.append("]");
322                 } else {
323                     readValueObject(value, in);
324                 }
325             }
326
327             if (in.peek() != JsonToken.END_OBJECT) {
328                 value.append(",");
329             }
330         }
331
332         in.endObject();
333         value.append("}");
334     }
335
336     /**
337      * Read patch edit data defined in value node to NormalizedNode
338      * @param in reader JsonReader reader
339      * @return NormalizedNode representing data
340      */
341     private NormalizedNode readEditData(@Nonnull final JsonReader in, @Nonnull SchemaNode targetSchemaNode,
342                                         @Nonnull InstanceIdentifierContext path) {
343         final NormalizedNodeResult resultHolder = new NormalizedNodeResult();
344         final NormalizedNodeStreamWriter writer = ImmutableNormalizedNodeStreamWriter.from(resultHolder);
345         JsonParserStream.create(writer, path.getSchemaContext(), targetSchemaNode).parse(in);
346
347         return resultHolder.getResult();
348     }
349
350     /**
351      * Prepare PATCHEntity from PatchEdit instance when it satisfies conditions, otherwise throws exception
352      * @param edit Instance of PatchEdit
353      * @return PATCHEntity
354      */
355     private PATCHEntity prepareEditOperation(@Nonnull final PatchEdit edit) {
356         if (edit.getOperation() != null && edit.getTargetSchemaNode() != null
357                 && checkDataPresence(edit.getOperation(), (edit.getData() != null))) {
358             if (isPatchOperationWithValue(edit.getOperation())) {
359                 // for lists allow to manipulate with list items through their parent
360                 final YangInstanceIdentifier targetNode;
361                 if (edit.getTarget().getLastPathArgument() instanceof NodeIdentifierWithPredicates) {
362                     targetNode = edit.getTarget().getParent();
363                 } else {
364                     targetNode = edit.getTarget();
365                 }
366
367                 return new PATCHEntity(edit.getId(), edit.getOperation(), targetNode, edit.getData());
368             } else {
369                 return new PATCHEntity(edit.getId(), edit.getOperation(), edit.getTarget());
370             }
371         }
372
373         throw new RestconfDocumentedException("Error parsing input", ErrorType.PROTOCOL, ErrorTag.MALFORMED_MESSAGE);
374     }
375
376     /**
377      * Check if data is present when operation requires it and not present when operation data is not allowed
378      * @param operation Name of operation
379      * @param hasData Data in edit are present/not present
380      * @return true if data is present when operation requires it or if there are no data when operation does not
381      * allow it, false otherwise
382      */
383     private boolean checkDataPresence(@Nonnull final String operation, final boolean hasData) {
384         if (isPatchOperationWithValue(operation)) {
385             if (hasData) {
386                 return true;
387             } else {
388                 return false;
389             }
390         } else  {
391             if (!hasData) {
392                 return true;
393             } else {
394                 return false;
395             }
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 id;
411         }
412
413         public void setId(String id) {
414             this.id = id;
415         }
416
417         public String getOperation() {
418             return operation;
419         }
420
421         public void setOperation(String operation) {
422             this.operation = operation;
423         }
424
425         public YangInstanceIdentifier getTarget() {
426             return target;
427         }
428
429         public void setTarget(YangInstanceIdentifier target) {
430             this.target = target;
431         }
432
433         public SchemaNode getTargetSchemaNode() {
434             return targetSchemaNode;
435         }
436
437         public void setTargetSchemaNode(SchemaNode targetSchemaNode) {
438             this.targetSchemaNode = targetSchemaNode;
439         }
440
441         public NormalizedNode getData() {
442             return data;
443         }
444
445         public void setData(NormalizedNode data) {
446             this.data = data;
447         }
448
449         public void clear() {
450             id = null;
451             operation = null;
452             target = null;
453             targetSchemaNode = null;
454             data = null;
455         }
456     }
457 }