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