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