summaryrefslogtreecommitdiffstats
path: root/netconf/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/rest/impl/JsonToPatchBodyReader.java
blob: ce5a8c4af659f233dbe56350b5aedc7cdda81691 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
/*
 * Copyright (c) 2015 Cisco Systems, Inc. and others.  All rights reserved.
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License v1.0 which accompanies this distribution,
 * and is available at http://www.eclipse.org/legal/epl-v10.html
 */
package org.opendaylight.netconf.sal.rest.impl;

import static com.google.common.base.Verify.verify;

import com.google.common.base.Throwables;
import com.google.common.collect.ImmutableList;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonToken;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.StringReader;
import java.lang.annotation.Annotation;
import java.lang.reflect.Type;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicReference;
import javax.ws.rs.Consumes;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.ext.MessageBodyReader;
import javax.ws.rs.ext.Provider;
import org.eclipse.jdt.annotation.NonNull;
import org.opendaylight.netconf.sal.rest.api.Draft02;
import org.opendaylight.netconf.sal.rest.api.RestconfService;
import org.opendaylight.netconf.sal.restconf.impl.ControllerContext;
import org.opendaylight.restconf.common.context.InstanceIdentifierContext;
import org.opendaylight.restconf.common.errors.RestconfDocumentedException;
import org.opendaylight.restconf.common.patch.PatchContext;
import org.opendaylight.restconf.common.patch.PatchEditOperation;
import org.opendaylight.restconf.common.patch.PatchEntity;
import org.opendaylight.restconf.common.util.RestUtil;
import org.opendaylight.yangtools.yang.common.ErrorTag;
import org.opendaylight.yangtools.yang.common.ErrorType;
import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifierWithPredicates;
import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode;
import org.opendaylight.yangtools.yang.data.api.schema.stream.NormalizedNodeStreamWriter;
import org.opendaylight.yangtools.yang.data.codec.gson.JSONCodecFactorySupplier;
import org.opendaylight.yangtools.yang.data.codec.gson.JsonParserStream;
import org.opendaylight.yangtools.yang.data.impl.schema.ImmutableNormalizedNodeStreamWriter;
import org.opendaylight.yangtools.yang.data.impl.schema.NormalizedNodeResult;
import org.opendaylight.yangtools.yang.data.impl.schema.ResultAlreadySetException;
import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext;
import org.opendaylight.yangtools.yang.model.api.SchemaNode;
import org.opendaylight.yangtools.yang.model.api.meta.EffectiveStatement;
import org.opendaylight.yangtools.yang.model.util.SchemaInferenceStack;
import org.opendaylight.yangtools.yang.model.util.SchemaInferenceStack.Inference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Patch reader for JSON.
 *
 * @deprecated This class will be replaced by JsonToPatchBodyReader in restconf-nb-rfc8040
 */
@Deprecated
@Provider
@Consumes({Draft02.MediaTypes.PATCH + RestconfService.JSON})
public class JsonToPatchBodyReader extends AbstractIdentifierAwareJaxRsProvider
        implements MessageBodyReader<PatchContext> {

    private static final Logger LOG = LoggerFactory.getLogger(JsonToPatchBodyReader.class);

    public JsonToPatchBodyReader(final ControllerContext controllerContext) {
        super(controllerContext);
    }

    @Override
    public boolean isReadable(final Class<?> type, final Type genericType,
                              final Annotation[] annotations, final MediaType mediaType) {
        return true;
    }

    @SuppressWarnings("checkstyle:IllegalCatch")
    @Override
    public PatchContext readFrom(final Class<PatchContext> type, final Type genericType,
                                 final Annotation[] annotations, final MediaType mediaType,
                                 final MultivaluedMap<String, String> httpHeaders, final InputStream entityStream)
            throws WebApplicationException {
        try {
            return readFrom(getInstanceIdentifierContext(), entityStream);
        } catch (final Exception e) {
            throw propagateExceptionAs(e);
        }
    }

    @SuppressWarnings("checkstyle:IllegalCatch")
    public PatchContext readFrom(final String uriPath, final InputStream entityStream) throws
            RestconfDocumentedException {
        try {
            return readFrom(getControllerContext().toInstanceIdentifier(uriPath), entityStream);
        } catch (final Exception e) {
            propagateExceptionAs(e);
            return null; // no-op
        }
    }

    private PatchContext readFrom(final InstanceIdentifierContext path, final InputStream entityStream)
            throws IOException {
        final Optional<InputStream> nonEmptyInputStreamOptional = RestUtil.isInputStreamEmpty(entityStream);
        if (nonEmptyInputStreamOptional.isEmpty()) {
            return new PatchContext(path, null, null);
        }

        final JsonReader jsonReader = new JsonReader(new InputStreamReader(nonEmptyInputStreamOptional.get(),
                StandardCharsets.UTF_8));
        AtomicReference<String> patchId = new AtomicReference<>();
        final List<PatchEntity> resultList = read(jsonReader, path, patchId);
        jsonReader.close();

        return new PatchContext(path, resultList, patchId.get());
    }

    private static RuntimeException propagateExceptionAs(final Exception exception) throws RestconfDocumentedException {
        Throwables.throwIfInstanceOf(exception, RestconfDocumentedException.class);
        LOG.debug("Error parsing json input", exception);

        if (exception instanceof ResultAlreadySetException) {
            throw new RestconfDocumentedException("Error parsing json input: Failed to create new parse result data. ");
        }

        RestconfDocumentedException.throwIfYangError(exception);
        throw new RestconfDocumentedException("Error parsing json input: " + exception.getMessage(), ErrorType.PROTOCOL,
                ErrorTag.MALFORMED_MESSAGE, exception);
    }

    private List<PatchEntity> read(final JsonReader in, final InstanceIdentifierContext path,
            final AtomicReference<String> patchId) throws IOException {
        final List<PatchEntity> resultCollection = new ArrayList<>();
        final StringModuleInstanceIdentifierCodec codec = new StringModuleInstanceIdentifierCodec(
                path.getSchemaContext());
        final JsonToPatchBodyReader.PatchEdit edit = new JsonToPatchBodyReader.PatchEdit();

        while (in.hasNext()) {
            switch (in.peek()) {
                case STRING:
                case NUMBER:
                    in.nextString();
                    break;
                case BOOLEAN:
                    Boolean.toString(in.nextBoolean());
                    break;
                case NULL:
                    in.nextNull();
                    break;
                case BEGIN_ARRAY:
                    in.beginArray();
                    break;
                case BEGIN_OBJECT:
                    in.beginObject();
                    break;
                case END_DOCUMENT:
                    break;
                case NAME:
                    parseByName(in.nextName(), edit, in, path, codec, resultCollection, patchId);
                    break;
                case END_OBJECT:
                    in.endObject();
                    break;
                case END_ARRAY:
                    in.endArray();
                    break;

                default:
                    break;
            }
        }

        return ImmutableList.copyOf(resultCollection);
    }

    /**
     * Switch value of parsed JsonToken.NAME and read edit definition or patch id.
     *
     * @param name value of token
     * @param edit PatchEdit instance
     * @param in JsonReader reader
     * @param path InstanceIdentifierContext context
     * @param codec StringModuleInstanceIdentifierCodec codec
     * @param resultCollection collection of parsed edits
     * @throws IOException if operation fails
     */
    private void parseByName(final @NonNull String name, final @NonNull PatchEdit edit,
                             final @NonNull JsonReader in, final @NonNull InstanceIdentifierContext path,
                             final @NonNull StringModuleInstanceIdentifierCodec codec,
                             final @NonNull List<PatchEntity> resultCollection,
                             final @NonNull AtomicReference<String> patchId) throws IOException {
        switch (name) {
            case "edit" :
                if (in.peek() == JsonToken.BEGIN_ARRAY) {
                    in.beginArray();

                    while (in.hasNext()) {
                        readEditDefinition(edit, in, path, codec);
                        resultCollection.add(prepareEditOperation(edit));
                        edit.clear();
                    }

                    in.endArray();
                } else {
                    readEditDefinition(edit, in, path, codec);
                    resultCollection.add(prepareEditOperation(edit));
                    edit.clear();
                }

                break;
            case "patch-id" :
                patchId.set(in.nextString());
                break;
            default:
                break;
        }
    }

    /**
     * Read one patch edit object from Json input.
     * @param edit PatchEdit instance to be filled with read data
     * @param in JsonReader reader
     * @param path InstanceIdentifierContext path context
     * @param codec StringModuleInstanceIdentifierCodec codec
     * @throws IOException if operation fails
     */
    private void readEditDefinition(final @NonNull PatchEdit edit, final @NonNull JsonReader in,
                                    final @NonNull InstanceIdentifierContext path,
                                    final @NonNull StringModuleInstanceIdentifierCodec codec) throws IOException {
        final StringBuilder value = new StringBuilder();
        in.beginObject();

        while (in.hasNext()) {
            final String editDefinition = in.nextName();
            switch (editDefinition) {
                case "edit-id" :
                    edit.setId(in.nextString());
                    break;
                case "operation" :
                    edit.setOperation(PatchEditOperation.valueOf(in.nextString().toUpperCase(Locale.ROOT)));
                    break;
                case "target" :
                    // target can be specified completely in request URI
                    final String target = in.nextString();
                    if (target.equals("/")) {
                        edit.setTarget(path.getInstanceIdentifier());
                        edit.setTargetSchemaNode(SchemaInferenceStack.of(path.getSchemaContext()).toInference());
                    } else {
                        edit.setTarget(codec.deserialize(codec.serialize(path.getInstanceIdentifier()).concat(target)));

                        final var stack = codec.getDataContextTree().enterPath(edit.getTarget()).orElseThrow().stack();
                        stack.exit();
                        final EffectiveStatement<?, ?> parentStmt = stack.currentStatement();
                        verify(parentStmt instanceof SchemaNode, "Unexpected parent %s", parentStmt);
                        edit.setTargetSchemaNode(stack.toInference());
                    }

                    break;
                case "value" :
                    // save data defined in value node for next (later) processing, because target needs to be read
                    // always first and there is no ordering in Json input
                    readValueNode(value, in);
                    break;
                default:
                    break;
            }
        }

        in.endObject();

        // read saved data to normalized node when target schema is already known
        edit.setData(readEditData(new JsonReader(
                new StringReader(value.toString())), edit.getTargetSchemaNode(), path));
    }

    /**
     * Parse data defined in value node and saves it to buffer.
     * @param value Buffer to read value node
     * @param in JsonReader reader
     * @throws IOException if operation fails
     */
    private void readValueNode(final @NonNull StringBuilder value, final @NonNull JsonReader in) throws IOException {
        in.beginObject();
        value.append('{');

        value.append('"').append(in.nextName()).append("\":");

        if (in.peek() == JsonToken.BEGIN_ARRAY) {
            in.beginArray();
            value.append('[');

            while (in.hasNext()) {
                if (in.peek() == JsonToken.STRING) {
                    value.append('"').append(in.nextString()).append('"');
                } else {
                    readValueObject(value, in);
                }
                if (in.peek() != JsonToken.END_ARRAY) {
                    value.append(',');
                }
            }

            in.endArray();
            value.append(']');
        } else {
            readValueObject(value, in);
        }

        in.endObject();
        value.append('}');
    }

    /**
     * Parse one value object of data and saves it to buffer.
     * @param value Buffer to read value object
     * @param in JsonReader reader
     * @throws IOException if operation fails
     */
    private void readValueObject(final @NonNull StringBuilder value, final @NonNull JsonReader in) throws IOException {
        // read simple leaf value
        if (in.peek() == JsonToken.STRING) {
            value.append('"').append(in.nextString()).append('"');
            return;
        }

        in.beginObject();
        value.append('{');

        while (in.hasNext()) {
            value.append('"').append(in.nextName()).append("\":");

            if (in.peek() == JsonToken.STRING) {
                value.append('"').append(in.nextString()).append('"');
            } else {
                if (in.peek() == JsonToken.BEGIN_ARRAY) {
                    in.beginArray();
                    value.append('[');

                    while (in.hasNext()) {
                        if (in.peek() == JsonToken.STRING) {
                            value.append('"').append(in.nextString()).append('"');
                        } else {
                            readValueObject(value, in);
                        }
                        if (in.peek() != JsonToken.END_ARRAY) {
                            value.append(',');
                        }
                    }

                    in.endArray();
                    value.append(']');
                } else {
                    readValueObject(value, in);
                }
            }

            if (in.peek() != JsonToken.END_OBJECT) {
                value.append(',');
            }
        }

        in.endObject();
        value.append('}');
    }

    /**
     * Read patch edit data defined in value node to NormalizedNode.
     * @param in reader JsonReader reader
     * @return NormalizedNode representing data
     */
    private static NormalizedNode readEditData(final @NonNull JsonReader in,
            final @NonNull Inference targetSchemaNode, final @NonNull InstanceIdentifierContext path) {
        final NormalizedNodeResult resultHolder = new NormalizedNodeResult();
        final NormalizedNodeStreamWriter writer = ImmutableNormalizedNodeStreamWriter.from(resultHolder);
        final EffectiveModelContext context = path.getSchemaContext();
        JsonParserStream.create(writer, JSONCodecFactorySupplier.DRAFT_LHOTKA_NETMOD_YANG_JSON_02.getShared(context),
            targetSchemaNode)
            .parse(in);

        return resultHolder.getResult();
    }

    /**
     * Prepare PatchEntity from PatchEdit instance when it satisfies conditions, otherwise throws exception.
     * @param edit Instance of PatchEdit
     * @return PatchEntity Patch entity
     */
    private static PatchEntity prepareEditOperation(final @NonNull PatchEdit edit) {
        if (edit.getOperation() != null && edit.getTargetSchemaNode() != null
                && checkDataPresence(edit.getOperation(), edit.getData() != null)) {
            if (edit.getOperation().isWithValue()) {
                // for lists allow to manipulate with list items through their parent
                final YangInstanceIdentifier targetNode;
                if (edit.getTarget().getLastPathArgument() instanceof NodeIdentifierWithPredicates) {
                    targetNode = edit.getTarget().getParent();
                } else {
                    targetNode = edit.getTarget();
                }

                return new PatchEntity(edit.getId(), edit.getOperation(), targetNode, edit.getData());
            }

            return new PatchEntity(edit.getId(), edit.getOperation(), edit.getTarget());
        }

        throw new RestconfDocumentedException("Error parsing input", ErrorType.PROTOCOL, ErrorTag.MALFORMED_MESSAGE);
    }

    /**
     * Check if data is present when operation requires it and not present when operation data is not allowed.
     * @param operation Name of operation
     * @param hasData Data in edit are present/not present
     * @return true if data is present when operation requires it or if there are no data when operation does not
     *     allow it, false otherwise
     */
    private static boolean checkDataPresence(final @NonNull PatchEditOperation operation, final boolean hasData) {
        return operation.isWithValue() == hasData;
    }

    /**
     * Helper class representing one patch edit.
     */
    private static final class PatchEdit {
        private String id;
        private PatchEditOperation operation;
        private YangInstanceIdentifier target;
        private Inference targetSchemaNode;
        private NormalizedNode data;

        public String getId() {
            return id;
        }

        public void setId(final String id) {
            this.id = id;
        }

        public PatchEditOperation getOperation() {
            return operation;
        }

        public void setOperation(final PatchEditOperation operation) {
            this.operation = operation;
        }

        public YangInstanceIdentifier getTarget() {
            return target;
        }

        public void setTarget(final YangInstanceIdentifier target) {
            this.target = target;
        }

        public Inference getTargetSchemaNode() {
            return targetSchemaNode;
        }

        public void setTargetSchemaNode(final Inference targetSchemaNode) {
            this.targetSchemaNode = targetSchemaNode;
        }

        public NormalizedNode getData() {
            return data;
        }

        public void setData(final NormalizedNode data) {
            this.data = data;
        }

        public void clear() {
            id = null;
            operation = null;
            target = null;
            targetSchemaNode = null;
            data = null;
        }
    }
}