Remove javax.annotation nullness annotations
[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 StringBuffer value = new StringBuffer();
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().getChild(edit.getTarget()).getDataSchemaNode().getPath()
255                                         .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 StringBuffer value, final @NonNull JsonReader in) throws IOException {
283         in.beginObject();
284         value.append("{");
285
286         value.append("\"" + in.nextName() + "\"" + ":");
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("\"" + in.nextString() + "\"");
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 StringBuffer value, final @NonNull JsonReader in) throws IOException {
320         // read simple leaf value
321         if (in.peek() == JsonToken.STRING) {
322             value.append("\"" + in.nextString() + "\"");
323             return;
324         }
325
326         in.beginObject();
327         value.append("{");
328
329         while (in.hasNext()) {
330             value.append("\"" + in.nextName() + "\"");
331             value.append(":");
332
333             if (in.peek() == JsonToken.STRING) {
334                 value.append("\"" + in.nextString() + "\"");
335             } else {
336                 if (in.peek() == JsonToken.BEGIN_ARRAY) {
337                     in.beginArray();
338                     value.append("[");
339
340                     while (in.hasNext()) {
341                         if (in.peek() == JsonToken.STRING) {
342                             value.append("\"" + in.nextString() + "\"");
343                         } else {
344                             readValueObject(value, in);
345                         }
346                         if (in.peek() != JsonToken.END_ARRAY) {
347                             value.append(",");
348                         }
349                     }
350
351                     in.endArray();
352                     value.append("]");
353                 } else {
354                     readValueObject(value, in);
355                 }
356             }
357
358             if (in.peek() != JsonToken.END_OBJECT) {
359                 value.append(",");
360             }
361         }
362
363         in.endObject();
364         value.append("}");
365     }
366
367     /**
368      * Read patch edit data defined in value node to NormalizedNode.
369      * @param in reader JsonReader reader
370      * @return NormalizedNode representing data
371      */
372     private static NormalizedNode<?, ?> readEditData(final @NonNull JsonReader in,
373             final @NonNull SchemaNode targetSchemaNode, final @NonNull InstanceIdentifierContext<?> path) {
374         final NormalizedNodeResult resultHolder = new NormalizedNodeResult();
375         final NormalizedNodeStreamWriter writer = ImmutableNormalizedNodeStreamWriter.from(resultHolder);
376         JsonParserStream.create(writer,
377             JSONCodecFactorySupplier.DRAFT_LHOTKA_NETMOD_YANG_JSON_02.getShared(path.getSchemaContext()),
378             targetSchemaNode).parse(in);
379
380         return resultHolder.getResult();
381     }
382
383     /**
384      * Prepare PatchEntity from PatchEdit instance when it satisfies conditions, otherwise throws exception.
385      * @param edit Instance of PatchEdit
386      * @return PatchEntity Patch entity
387      */
388     private static PatchEntity prepareEditOperation(final @NonNull PatchEdit edit) {
389         if (edit.getOperation() != null && edit.getTargetSchemaNode() != null
390                 && checkDataPresence(edit.getOperation(), edit.getData() != null)) {
391             if (edit.getOperation().isWithValue()) {
392                 // for lists allow to manipulate with list items through their parent
393                 final YangInstanceIdentifier targetNode;
394                 if (edit.getTarget().getLastPathArgument() instanceof NodeIdentifierWithPredicates) {
395                     targetNode = edit.getTarget().getParent();
396                 } else {
397                     targetNode = edit.getTarget();
398                 }
399
400                 return new PatchEntity(edit.getId(), edit.getOperation(), targetNode, edit.getData());
401             }
402
403             return new PatchEntity(edit.getId(), edit.getOperation(), edit.getTarget());
404         }
405
406         throw new RestconfDocumentedException("Error parsing input", ErrorType.PROTOCOL, ErrorTag.MALFORMED_MESSAGE);
407     }
408
409     /**
410      * Check if data is present when operation requires it and not present when operation data is not allowed.
411      * @param operation Name of operation
412      * @param hasData Data in edit are present/not present
413      * @return true if data is present when operation requires it or if there are no data when operation does not
414      *     allow it, false otherwise
415      */
416     private static boolean checkDataPresence(final @NonNull PatchEditOperation operation, final boolean hasData) {
417         return operation.isWithValue() == hasData;
418     }
419
420     /**
421      * Helper class representing one patch edit.
422      */
423     private static final class PatchEdit {
424         private String id;
425         private PatchEditOperation operation;
426         private YangInstanceIdentifier target;
427         private SchemaNode targetSchemaNode;
428         private NormalizedNode<?, ?> data;
429
430         public String getId() {
431             return this.id;
432         }
433
434         public void setId(final String id) {
435             this.id = id;
436         }
437
438         public PatchEditOperation getOperation() {
439             return this.operation;
440         }
441
442         public void setOperation(final PatchEditOperation operation) {
443             this.operation = operation;
444         }
445
446         public YangInstanceIdentifier getTarget() {
447             return this.target;
448         }
449
450         public void setTarget(final YangInstanceIdentifier target) {
451             this.target = target;
452         }
453
454         public SchemaNode getTargetSchemaNode() {
455             return this.targetSchemaNode;
456         }
457
458         public void setTargetSchemaNode(final SchemaNode targetSchemaNode) {
459             this.targetSchemaNode = targetSchemaNode;
460         }
461
462         public NormalizedNode<?, ?> getData() {
463             return this.data;
464         }
465
466         public void setData(final NormalizedNode<?, ?> data) {
467             this.data = data;
468         }
469
470         public void clear() {
471             this.id = null;
472             this.operation = null;
473             this.target = null;
474             this.targetSchemaNode = null;
475             this.data = null;
476         }
477     }
478 }