Bug 5509: HTTP Patch in Restconf doesn't support general absolute or relative target...
[netconf.git] / restconf / sal-rest-connector / 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.base.Preconditions;
12 import com.google.common.collect.ImmutableList;
13 import com.google.gson.stream.JsonReader;
14 import java.io.IOException;
15 import java.io.InputStream;
16 import java.io.InputStreamReader;
17 import java.lang.annotation.Annotation;
18 import java.lang.reflect.Type;
19 import java.net.URI;
20 import java.util.ArrayList;
21 import java.util.List;
22 import javax.annotation.Nonnull;
23 import javax.annotation.Nullable;
24 import javax.ws.rs.Consumes;
25 import javax.ws.rs.WebApplicationException;
26 import javax.ws.rs.core.MediaType;
27 import javax.ws.rs.core.MultivaluedMap;
28 import javax.ws.rs.ext.MessageBodyReader;
29 import javax.ws.rs.ext.Provider;
30 import org.opendaylight.netconf.sal.rest.api.Draft02;
31 import org.opendaylight.netconf.sal.rest.api.RestconfService;
32 import org.opendaylight.netconf.sal.restconf.impl.ControllerContext;
33 import org.opendaylight.netconf.sal.restconf.impl.InstanceIdentifierContext;
34 import org.opendaylight.netconf.sal.restconf.impl.PATCHContext;
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.stream.NormalizedNodeStreamWriter;
41 import org.opendaylight.yangtools.yang.data.codec.gson.JsonParserStream;
42 import org.opendaylight.yangtools.yang.data.impl.schema.ImmutableNormalizedNodeStreamWriter;
43 import org.opendaylight.yangtools.yang.data.impl.schema.NormalizedNodeResult;
44 import org.opendaylight.yangtools.yang.data.impl.schema.ResultAlreadySetException;
45 import org.opendaylight.yangtools.yang.data.util.AbstractModuleStringInstanceIdentifierCodec;
46 import org.opendaylight.yangtools.yang.data.util.DataSchemaContextTree;
47 import org.opendaylight.yangtools.yang.model.api.Module;
48 import org.opendaylight.yangtools.yang.model.api.SchemaContext;
49 import org.opendaylight.yangtools.yang.model.api.SchemaNode;
50 import org.opendaylight.yangtools.yang.model.util.SchemaContextUtil;
51 import org.slf4j.Logger;
52 import org.slf4j.LoggerFactory;
53
54 @Provider
55 @Consumes({Draft02.MediaTypes.PATCH + RestconfService.JSON})
56 public class JsonToPATCHBodyReader extends AbstractIdentifierAwareJaxRsProvider implements MessageBodyReader<PATCHContext> {
57
58     private final static Logger LOG = LoggerFactory.getLogger(JsonToPATCHBodyReader.class);
59     private String patchId;
60
61     @Override
62     public boolean isReadable(Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType) {
63         return true;
64     }
65
66     @Override
67     public PATCHContext readFrom(Class<PATCHContext> type, Type genericType, Annotation[] annotations, MediaType mediaType, MultivaluedMap<String, String> httpHeaders, InputStream entityStream) throws IOException, WebApplicationException {
68         try {
69             return readFrom(getInstanceIdentifierContext(), entityStream);
70         } catch (final Exception e) {
71             throw propagateExceptionAs(e);
72         }
73     }
74
75     private static RuntimeException propagateExceptionAs(Exception e) throws RestconfDocumentedException {
76         if(e instanceof RestconfDocumentedException) {
77             throw (RestconfDocumentedException)e;
78         }
79
80         if(e instanceof ResultAlreadySetException) {
81             LOG.debug("Error parsing json input:", e);
82
83             throw new RestconfDocumentedException("Error parsing json input: Failed to create new parse result data. ");
84         }
85
86         throw new RestconfDocumentedException("Error parsing json input: " + e.getMessage(), ErrorType.PROTOCOL,
87                 ErrorTag.MALFORMED_MESSAGE, e);
88     }
89
90     public PATCHContext readFrom(final String uriPath, final InputStream entityStream) throws
91             RestconfDocumentedException {
92         try {
93             return readFrom(ControllerContext.getInstance().toInstanceIdentifier(uriPath), entityStream);
94         } catch (final Exception e) {
95             propagateExceptionAs(e);
96             return null; // no-op
97         }
98     }
99
100     private PATCHContext readFrom(final InstanceIdentifierContext<?> path, final InputStream entityStream) throws IOException {
101         if (entityStream.available() < 1) {
102             return new PATCHContext(path, null, null);
103         }
104
105         final JsonReader jsonReader = new JsonReader(new InputStreamReader(entityStream));
106         final List<PATCHEntity> resultList = read(jsonReader, path);
107         jsonReader.close();
108
109         return new PATCHContext(path, resultList, patchId);
110     }
111
112     private List<PATCHEntity> read(final JsonReader in, InstanceIdentifierContext path) throws
113             IOException {
114
115         boolean inEdit = false;
116         boolean inValue = false;
117         String operation = null;
118         String target = null;
119         String editId = null;
120         List<PATCHEntity> resultCollection = new ArrayList<>();
121
122         while (in.hasNext()) {
123             switch (in.peek()) {
124                 case STRING:
125                 case NUMBER:
126                     in.nextString();
127                     break;
128                 case BOOLEAN:
129                     Boolean.toString(in.nextBoolean());
130                     break;
131                 case NULL:
132                     in.nextNull();
133                     break;
134                 case BEGIN_ARRAY:
135                     in.beginArray();
136                     break;
137                 case BEGIN_OBJECT:
138                     if (inEdit && operation != null & target != null & inValue) {
139                         StringModuleInstanceIdentifierCodec codec = new StringModuleInstanceIdentifierCodec(path
140                               .getSchemaContext());
141
142                         YangInstanceIdentifier targetII = codec.deserialize(codec.serialize(path
143                                 .getInstanceIdentifier()) + target);
144                         SchemaNode targetSchemaNode = SchemaContextUtil.findDataSchemaNode(path.getSchemaContext(),
145                                 codec.getDataContextTree().getChild(targetII).getDataSchemaNode().getPath()
146                                         .getParent());
147
148                         final NormalizedNodeResult resultHolder = new NormalizedNodeResult();
149                         final NormalizedNodeStreamWriter writer = ImmutableNormalizedNodeStreamWriter.from(resultHolder);
150
151                         //keep on parsing json from place where target points
152                         final JsonParserStream jsonParser = JsonParserStream.create(writer, path.getSchemaContext(),
153                                 targetSchemaNode);
154                         jsonParser.parse(in);
155                         resultCollection.add(new PATCHEntity(editId, operation, targetII.getParent(), resultHolder.getResult()));
156                         inValue = false;
157
158                         operation = null;
159                         target = null;
160                         in.endObject();
161                     } else {
162                         in.beginObject();
163                     }
164                     break;
165                 case END_DOCUMENT:
166                     break;
167                 case NAME:
168                     final String name = in.nextName();
169
170                     switch (name) {
171                         case "edit" : inEdit = true;
172                             break;
173                         case "operation" : operation = in.nextString();
174                             break;
175                         case "target" : target = in.nextString();
176                             break;
177                         case "value" : inValue = true;
178                             break;
179                         case "patch-id" : patchId = in.nextString();
180                             break;
181                         case "edit-id" : editId = in.nextString();
182                             break;
183                     }
184                     break;
185                 case END_OBJECT:
186                     in.endObject();
187                     break;
188             case END_ARRAY:
189                 in.endArray();
190                 break;
191
192             default:
193                 break;
194             }
195         }
196
197         return ImmutableList.copyOf(resultCollection);
198     }
199
200     private class StringModuleInstanceIdentifierCodec extends AbstractModuleStringInstanceIdentifierCodec {
201
202         private final DataSchemaContextTree dataContextTree;
203         private final SchemaContext context;
204
205         StringModuleInstanceIdentifierCodec(SchemaContext context) {
206             this.context = Preconditions.checkNotNull(context);
207             this.dataContextTree = DataSchemaContextTree.from(context);
208         }
209
210         @Override
211         protected Module moduleForPrefix(@Nonnull String prefix) {
212             return context.findModuleByName(prefix, null);
213         }
214
215         @Nonnull
216         @Override
217         protected DataSchemaContextTree getDataContextTree() {
218             return dataContextTree;
219         }
220
221         @Nullable
222         @Override
223         protected String prefixForNamespace(@Nonnull URI namespace) {
224             final Module module = context.findModuleByNamespaceAndRevision(namespace, null);
225             return module == null ? null : module.getName();
226         }
227     }
228 }