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