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