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