2 * Copyright (c) 2015 Cisco Systems, Inc. and others. All rights reserved.
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
9 package org.opendaylight.netconf.sal.rest.impl;
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.util.ArrayList;
21 import java.util.List;
22 import javax.annotation.Nonnull;
23 import javax.ws.rs.Consumes;
24 import javax.ws.rs.WebApplicationException;
25 import javax.ws.rs.core.MediaType;
26 import javax.ws.rs.core.MultivaluedMap;
27 import javax.ws.rs.ext.MessageBodyReader;
28 import javax.ws.rs.ext.Provider;
29 import org.opendaylight.netconf.sal.rest.api.Draft02;
30 import org.opendaylight.netconf.sal.rest.api.RestconfService;
31 import org.opendaylight.netconf.sal.restconf.impl.ControllerContext;
32 import org.opendaylight.netconf.sal.restconf.impl.InstanceIdentifierContext;
33 import org.opendaylight.netconf.sal.restconf.impl.PATCHContext;
34 import org.opendaylight.netconf.sal.restconf.impl.PATCHEditOperation;
35 import org.opendaylight.netconf.sal.restconf.impl.PATCHEntity;
36 import org.opendaylight.netconf.sal.restconf.impl.RestconfDocumentedException;
37 import org.opendaylight.netconf.sal.restconf.impl.RestconfError.ErrorTag;
38 import org.opendaylight.netconf.sal.restconf.impl.RestconfError.ErrorType;
39 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
40 import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode;
41 import org.opendaylight.yangtools.yang.data.api.schema.stream.NormalizedNodeStreamWriter;
42 import org.opendaylight.yangtools.yang.data.codec.gson.JsonParserStream;
43 import org.opendaylight.yangtools.yang.data.impl.schema.ImmutableNormalizedNodeStreamWriter;
44 import org.opendaylight.yangtools.yang.data.impl.schema.NormalizedNodeResult;
45 import org.opendaylight.yangtools.yang.data.impl.schema.ResultAlreadySetException;
46 import org.opendaylight.yangtools.yang.model.api.SchemaNode;
47 import org.opendaylight.yangtools.yang.model.util.SchemaContextUtil;
48 import org.slf4j.Logger;
49 import org.slf4j.LoggerFactory;
52 @Consumes({Draft02.MediaTypes.PATCH + RestconfService.JSON})
53 public class JsonToPATCHBodyReader extends AbstractIdentifierAwareJaxRsProvider implements MessageBodyReader<PATCHContext> {
55 private final static Logger LOG = LoggerFactory.getLogger(JsonToPATCHBodyReader.class);
56 private String patchId;
59 public boolean isReadable(Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType) {
64 public PATCHContext readFrom(Class<PATCHContext> type, Type genericType, Annotation[] annotations, MediaType mediaType, MultivaluedMap<String, String> httpHeaders, InputStream entityStream) throws IOException, WebApplicationException {
66 return readFrom(getInstanceIdentifierContext(), entityStream);
67 } catch (final Exception e) {
68 throw propagateExceptionAs(e);
72 private static RuntimeException propagateExceptionAs(Exception e) throws RestconfDocumentedException {
73 if(e instanceof RestconfDocumentedException) {
74 throw (RestconfDocumentedException)e;
77 if(e instanceof ResultAlreadySetException) {
78 LOG.debug("Error parsing json input:", e);
80 throw new RestconfDocumentedException("Error parsing json input: Failed to create new parse result data. ");
83 throw new RestconfDocumentedException("Error parsing json input: " + e.getMessage(), ErrorType.PROTOCOL,
84 ErrorTag.MALFORMED_MESSAGE, e);
87 public PATCHContext readFrom(final String uriPath, final InputStream entityStream) throws
88 RestconfDocumentedException {
90 return readFrom(ControllerContext.getInstance().toInstanceIdentifier(uriPath), entityStream);
91 } catch (final Exception e) {
92 propagateExceptionAs(e);
97 private PATCHContext readFrom(final InstanceIdentifierContext<?> path, final InputStream entityStream) throws IOException {
98 if (entityStream.available() < 1) {
99 return new PATCHContext(path, null, null);
102 final JsonReader jsonReader = new JsonReader(new InputStreamReader(entityStream));
103 final List<PATCHEntity> resultList = read(jsonReader, path);
106 return new PATCHContext(path, resultList, patchId);
109 private List<PATCHEntity> read(final JsonReader in, InstanceIdentifierContext path) throws IOException {
110 List<PATCHEntity> resultCollection = new ArrayList<>();
111 StringModuleInstanceIdentifierCodec codec = new StringModuleInstanceIdentifierCodec(path.getSchemaContext());
112 JsonToPATCHBodyReader.PatchEdit edit = new JsonToPATCHBodyReader.PatchEdit();
114 while (in.hasNext()) {
121 Boolean.toString(in.nextBoolean());
135 parseByName(in.nextName(), edit, in, path, codec, resultCollection);
149 return ImmutableList.copyOf(resultCollection);
153 * Switch value of parsed JsonToken.NAME and read edit definition or patch id
154 * @param name value of token
155 * @param edit PatchEdit instance
156 * @param in JsonReader reader
157 * @param path InstanceIdentifierContext context
158 * @param codec StringModuleInstanceIdentifierCodec codec
159 * @param resultCollection collection of parsed edits
160 * @throws IOException
162 private void parseByName(@Nonnull final String name, @Nonnull final PatchEdit edit,
163 @Nonnull final JsonReader in, @Nonnull final InstanceIdentifierContext path,
164 @Nonnull final StringModuleInstanceIdentifierCodec codec,
165 @Nonnull final List<PATCHEntity> resultCollection) throws IOException {
168 if (in.peek() == JsonToken.BEGIN_ARRAY) {
171 while (in.hasNext()) {
172 readEditDefinition(edit, in, path, codec);
173 resultCollection.add(prepareEditOperation(edit));
179 readEditDefinition(edit, in, path, codec);
180 resultCollection.add(prepareEditOperation(edit));
186 this.patchId = in.nextString();
194 * Read one patch edit object from Json input
195 * @param edit PatchEdit instance to be filled with read data
196 * @param in JsonReader reader
197 * @param path InstanceIdentifierContext path context
198 * @param codec StringModuleInstanceIdentifierCodec codec
199 * @throws IOException
201 private void readEditDefinition(@Nonnull final PatchEdit edit, @Nonnull final JsonReader in,
202 @Nonnull final InstanceIdentifierContext path,
203 @Nonnull final StringModuleInstanceIdentifierCodec codec) throws IOException {
204 StringBuffer value = new StringBuffer();
207 while (in.hasNext()) {
208 final String editDefinition = in.nextName();
209 switch (editDefinition) {
211 edit.setId(in.nextString());
214 edit.setOperation(in.nextString());
217 edit.setTarget(codec.deserialize(codec.serialize(path.getInstanceIdentifier()) + in.nextString()));
218 edit.setTargetSchemaNode(SchemaContextUtil.findDataSchemaNode(path.getSchemaContext(),
219 codec.getDataContextTree().getChild(edit.getTarget()).getDataSchemaNode().getPath()
223 // save data defined in value node for next (later) processing, because target needs to be read
224 // always first and there is no ordering in Json input
225 readValueNode(value, in);
234 // read saved data to normalized node when target schema is already known
235 edit.setData(readEditData(new JsonReader(new StringReader(value.toString())), edit.getTargetSchemaNode(), path));
239 * Parse data defined in value node and saves it to buffer
240 * @param value Buffer to read value node
241 * @param in JsonReader reader
242 * @throws IOException
244 private void readValueNode(@Nonnull final StringBuffer value, @Nonnull final JsonReader in) throws IOException {
248 value.append("\"" + in.nextName() + "\"" + ":");
250 if (in.peek() == JsonToken.BEGIN_ARRAY) {
254 while (in.hasNext()) {
255 readValueObject(value, in);
256 if (in.peek() != JsonToken.END_ARRAY) {
264 readValueObject(value, in);
272 * Parse one value object of data and saves it to buffer
273 * @param value Buffer to read value object
274 * @param in JsonReader reader
275 * @throws IOException
277 private void readValueObject(@Nonnull final StringBuffer value, @Nonnull final JsonReader in) throws IOException {
281 while (in.hasNext()) {
282 value.append("\"" + in.nextName() + "\"");
284 value.append("\"" + in.nextString() + "\"");
285 if (in.peek() != JsonToken.END_OBJECT) {
295 * Read patch edit data defined in value node to NormalizedNode
296 * @param in reader JsonReader reader
297 * @return NormalizedNode representing data
299 private NormalizedNode readEditData(@Nonnull final JsonReader in, @Nonnull SchemaNode targetSchemaNode,
300 @Nonnull InstanceIdentifierContext path) {
301 final NormalizedNodeResult resultHolder = new NormalizedNodeResult();
302 final NormalizedNodeStreamWriter writer = ImmutableNormalizedNodeStreamWriter.from(resultHolder);
303 JsonParserStream.create(writer, path.getSchemaContext(), targetSchemaNode).parse(in);
305 return resultHolder.getResult();
309 * Prepare PATCHEntity from PatchEdit instance when it satisfies conditions, otherwise throws exception
310 * @param edit Instance of PatchEdit
311 * @return PATCHEntity
313 private PATCHEntity prepareEditOperation(@Nonnull final PatchEdit edit) {
314 if (edit.getOperation() != null && edit.getTargetSchemaNode() != null
315 && checkDataPresence(edit.getOperation(), (edit.getData() != null))) {
316 if (isPatchOperationWithValue(edit.getOperation())) {
317 return new PATCHEntity(edit.getId(), edit.getOperation(), edit.getTarget().getParent(), edit.getData());
319 return new PATCHEntity(edit.getId(), edit.getOperation(), edit.getTarget());
323 throw new RestconfDocumentedException("Error parsing input", ErrorType.PROTOCOL, ErrorTag.MALFORMED_MESSAGE);
327 * Check if data is present when operation requires it and not present when operation data is not allowed
328 * @param operation Name of operation
329 * @param hasData Data in edit are present/not present
330 * @return true if data is present when operation requires it or if there are no data when operation does not
331 * allow it, false otherwise
333 private boolean checkDataPresence(@Nonnull final String operation, final boolean hasData) {
334 if (isPatchOperationWithValue(operation)) {
350 * Check if operation requires data to be specified
351 * @param operation Name of the operation to be checked
352 * @return true if operation requires data, false otherwise
354 private boolean isPatchOperationWithValue(@Nonnull final String operation) {
355 switch (PATCHEditOperation.valueOf(operation.toUpperCase())) {
367 * Helper class representing one patch edit
369 private static final class PatchEdit {
371 private String operation;
372 private YangInstanceIdentifier target;
373 private SchemaNode targetSchemaNode;
374 private NormalizedNode data;
376 public String getId() {
380 public void setId(String id) {
384 public String getOperation() {
388 public void setOperation(String operation) {
389 this.operation = operation;
392 public YangInstanceIdentifier getTarget() {
396 public void setTarget(YangInstanceIdentifier target) {
397 this.target = target;
400 public SchemaNode getTargetSchemaNode() {
401 return targetSchemaNode;
404 public void setTargetSchemaNode(SchemaNode targetSchemaNode) {
405 this.targetSchemaNode = targetSchemaNode;
408 public NormalizedNode getData() {
412 public void setData(NormalizedNode data) {
416 public void clear() {
420 targetSchemaNode = null;