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