Bug 3866: Support for Restconf HTTP Patch
[netconf.git] / opendaylight / 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.common.QName;
40 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
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.data.util.AbstractStringInstanceIdentifierCodec;
47 import org.opendaylight.yangtools.yang.data.util.DataSchemaContextTree;
48 import org.opendaylight.yangtools.yang.model.api.DataNodeContainer;
49 import org.opendaylight.yangtools.yang.model.api.DataSchemaNode;
50 import org.opendaylight.yangtools.yang.model.api.Module;
51 import org.opendaylight.yangtools.yang.model.api.SchemaContext;
52 import org.slf4j.Logger;
53 import org.slf4j.LoggerFactory;
54
55 @Provider
56 @Consumes({Draft02.MediaTypes.PATCH + RestconfService.JSON})
57 public class JsonToPATCHBodyReader extends AbstractIdentifierAwareJaxRsProvider implements MessageBodyReader<PATCHContext> {
58
59     private final static Logger LOG = LoggerFactory.getLogger(JsonToPATCHBodyReader.class);
60     private String patchId;
61
62     @Override
63     public boolean isReadable(Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType) {
64         return true;
65     }
66
67     @Override
68     public PATCHContext readFrom(Class<PATCHContext> type, Type genericType, Annotation[] annotations, MediaType mediaType, MultivaluedMap<String, String> httpHeaders, InputStream entityStream) throws IOException, WebApplicationException {
69         try {
70             return readFrom(getInstanceIdentifierContext(), entityStream);
71         } catch (final Exception e) {
72             throw propagateExceptionAs(e);
73         }
74     }
75
76     private static RuntimeException propagateExceptionAs(Exception e) throws RestconfDocumentedException {
77         if(e instanceof RestconfDocumentedException) {
78             throw (RestconfDocumentedException)e;
79         }
80
81         if(e instanceof ResultAlreadySetException) {
82             LOG.debug("Error parsing json input:", e);
83
84             throw new RestconfDocumentedException("Error parsing json input: Failed to create new parse result data. ");
85         }
86
87         throw new RestconfDocumentedException("Error parsing json input: " + e.getMessage(), ErrorType.PROTOCOL,
88                 ErrorTag.MALFORMED_MESSAGE, e);
89     }
90
91     public PATCHContext readFrom(final String uriPath, final InputStream entityStream) throws
92             RestconfDocumentedException {
93         try {
94             return readFrom(ControllerContext.getInstance().toInstanceIdentifier(uriPath), entityStream);
95         } catch (final Exception e) {
96             propagateExceptionAs(e);
97             return null; // no-op
98         }
99     }
100
101     private PATCHContext readFrom(final InstanceIdentifierContext<?> path, final InputStream entityStream) throws IOException {
102         if (entityStream.available() < 1) {
103             return new PATCHContext(path, null, null);
104         }
105
106         final JsonReader jsonReader = new JsonReader(new InputStreamReader(entityStream));
107         final List<PATCHEntity> resultList = read(jsonReader, path);
108         jsonReader.close();
109
110         return new PATCHContext(path, resultList, patchId);
111     }
112
113     private List<PATCHEntity> read(final JsonReader in, InstanceIdentifierContext path) throws
114             IOException {
115
116         boolean inEdit = false;
117         boolean inValue = false;
118         String operation = null;
119         String target = null;
120         String editId = null;
121         List<PATCHEntity> resultCollection = new ArrayList<>();
122
123         while (in.hasNext()) {
124             switch (in.peek()) {
125                 case STRING:
126                 case NUMBER:
127                     in.nextString();
128                     break;
129                 case BOOLEAN:
130                     Boolean.toString(in.nextBoolean());
131                     break;
132                 case NULL:
133                     in.nextNull();
134                     break;
135                 case BEGIN_ARRAY:
136                     in.beginArray();
137                     break;
138                 case BEGIN_OBJECT:
139                     if (inEdit && operation != null & target != null & inValue) {
140                         //let's do the stuff - find out target node
141 //                      StringInstanceIdentifierCodec codec = new StringInstanceIdentifierCodec(path
142 //                               .getSchemaContext());
143 //                        if (path.getInstanceIdentifier().toString().equals("/")) {
144 //                        final YangInstanceIdentifier deserialized = codec.deserialize(target);
145 //                        }
146                         DataSchemaNode targetNode = ((DataNodeContainer)(path.getSchemaNode())).getDataChildByName
147                                 (target.replace("/", ""));
148                         if (targetNode == null) {
149                             LOG.debug("Target node {} not found in path {} ", target, path.getSchemaNode());
150                             throw new RestconfDocumentedException("Error parsing input", ErrorType.PROTOCOL,
151                                     ErrorTag.MALFORMED_MESSAGE);
152                         } else {
153
154                             final NormalizedNodeResult resultHolder = new NormalizedNodeResult();
155                             final NormalizedNodeStreamWriter writer = ImmutableNormalizedNodeStreamWriter.from(resultHolder);
156
157                             //keep on parsing json from place where target points
158                             final JsonParserStream jsonParser = JsonParserStream.create(writer, path.getSchemaContext(),
159                                     path.getSchemaNode());
160                             jsonParser.parse(in);
161
162                             final YangInstanceIdentifier targetII = path.getInstanceIdentifier().node(targetNode.getQName());
163                             resultCollection.add(new PATCHEntity(editId, operation, targetII, resultHolder.getResult
164                                     ()));
165                             inValue = false;
166
167                             operation = null;
168                             target = null;
169                         }
170                         in.endObject();
171                     } else {
172                         in.beginObject();
173                     }
174                     break;
175                 case END_DOCUMENT:
176                     break;
177                 case NAME:
178                     final String name = in.nextName();
179
180                     switch (name) {
181                         case "edit" : inEdit = true;
182                             break;
183                         case "operation" : operation = in.nextString();
184                             break;
185                         case "target" : target = in.nextString();
186                             break;
187                         case "value" : inValue = true;
188                             break;
189                         case "patch-id" : patchId = in.nextString();
190                             break;
191                         case "edit-id" : editId = in.nextString();
192                             break;
193                     }
194                     break;
195                 case END_OBJECT:
196                     in.endObject();
197                     break;
198             case END_ARRAY:
199                 in.endArray();
200                 break;
201
202             default:
203                 break;
204             }
205         }
206
207         return ImmutableList.copyOf(resultCollection);
208     }
209
210     private class StringInstanceIdentifierCodec extends AbstractStringInstanceIdentifierCodec {
211
212         private final DataSchemaContextTree dataContextTree;
213         private final SchemaContext context;
214
215         StringInstanceIdentifierCodec(SchemaContext context) {
216             this.context = Preconditions.checkNotNull(context);
217             this.dataContextTree = DataSchemaContextTree.from(context);
218         }
219
220         @Nonnull
221         @Override
222         protected DataSchemaContextTree getDataContextTree() {
223             return dataContextTree;
224         }
225
226         @Nullable
227         @Override
228         protected String prefixForNamespace(@Nonnull URI namespace) {
229             final Module module = context.findModuleByNamespaceAndRevision(namespace, null);
230             return module == null ? null : module.getName();
231         }
232
233         @Nullable
234         @Override
235         protected QName createQName(@Nonnull String prefix, @Nonnull String localName) {
236             throw new UnsupportedOperationException("Not implemented");
237         }
238
239     }
240 }