Bug 6947 / Bug 6948 - implement point and insert query params
[netconf.git] / restconf / sal-rest-connector / src / main / java / org / opendaylight / restconf / restful / utils / PutDataTransactionUtil.java
1 /*
2  * Copyright (c) 2016 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 package org.opendaylight.restconf.restful.utils;
9
10 import com.google.common.collect.Maps;
11 import com.google.common.util.concurrent.CheckedFuture;
12 import java.util.List;
13 import java.util.Map;
14 import javax.ws.rs.core.Response;
15 import org.opendaylight.controller.md.sal.common.api.data.LogicalDatastoreType;
16 import org.opendaylight.controller.md.sal.common.api.data.TransactionCommitFailedException;
17 import org.opendaylight.controller.md.sal.dom.api.DOMDataReadWriteTransaction;
18 import org.opendaylight.controller.md.sal.dom.api.DOMDataWriteTransaction;
19 import org.opendaylight.controller.md.sal.dom.api.DOMTransactionChain;
20 import org.opendaylight.netconf.md.sal.rest.common.RestconfValidationUtils;
21 import org.opendaylight.netconf.sal.restconf.impl.ControllerContext;
22 import org.opendaylight.netconf.sal.restconf.impl.InstanceIdentifierContext;
23 import org.opendaylight.netconf.sal.restconf.impl.NormalizedNodeContext;
24 import org.opendaylight.netconf.sal.restconf.impl.RestconfDocumentedException;
25 import org.opendaylight.netconf.sal.restconf.impl.RestconfError.ErrorTag;
26 import org.opendaylight.netconf.sal.restconf.impl.RestconfError.ErrorType;
27 import org.opendaylight.restconf.common.references.SchemaContextRef;
28 import org.opendaylight.restconf.restful.transaction.TransactionVarsWrapper;
29 import org.opendaylight.yangtools.yang.common.QName;
30 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
31 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifierWithPredicates;
32 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.PathArgument;
33 import org.opendaylight.yangtools.yang.data.api.schema.LeafSetEntryNode;
34 import org.opendaylight.yangtools.yang.data.api.schema.LeafSetNode;
35 import org.opendaylight.yangtools.yang.data.api.schema.MapEntryNode;
36 import org.opendaylight.yangtools.yang.data.api.schema.MapNode;
37 import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode;
38 import org.opendaylight.yangtools.yang.data.api.schema.OrderedLeafSetNode;
39 import org.opendaylight.yangtools.yang.data.api.schema.OrderedMapNode;
40 import org.opendaylight.yangtools.yang.data.impl.schema.ImmutableNodes;
41 import org.opendaylight.yangtools.yang.data.util.DataSchemaContextNode;
42 import org.opendaylight.yangtools.yang.data.util.DataSchemaContextTree;
43 import org.opendaylight.yangtools.yang.model.api.DataSchemaNode;
44 import org.opendaylight.yangtools.yang.model.api.LeafListSchemaNode;
45 import org.opendaylight.yangtools.yang.model.api.ListSchemaNode;
46 import org.opendaylight.yangtools.yang.model.api.SchemaContext;
47 import org.opendaylight.yangtools.yang.model.api.SchemaNode;
48
49 /**
50  * Util class for put data to DS
51  *
52  */
53 public final class PutDataTransactionUtil {
54
55     /**
56      * Valid input data with {@link SchemaNode}
57      *
58      * @param schemaNode
59      *            - {@link SchemaNode}
60      * @param payload
61      *            - input data
62      */
63     public static void validInputData(final SchemaNode schemaNode, final NormalizedNodeContext payload) {
64         if ((schemaNode != null) && (payload.getData() == null)) {
65             throw new RestconfDocumentedException("Input is required.", ErrorType.PROTOCOL, ErrorTag.MALFORMED_MESSAGE);
66         } else if ((schemaNode == null) && (payload.getData() != null)) {
67             throw new RestconfDocumentedException("No input expected.", ErrorType.PROTOCOL, ErrorTag.MALFORMED_MESSAGE);
68         }
69     }
70
71     /**
72      * Valid top level node name
73      *
74      * @param path
75      *            - path of node
76      * @param payload
77      *            - data
78      */
79     public static void validTopLevelNodeName(final YangInstanceIdentifier path, final NormalizedNodeContext payload) {
80         final String payloadName = payload.getData().getNodeType().getLocalName();
81
82         if (path.isEmpty()) {
83             if (!payload.getData().getNodeType().equals(RestconfDataServiceConstant.NETCONF_BASE_QNAME)) {
84                 throw new RestconfDocumentedException("Instance identifier has to contain at least one path argument",
85                         ErrorType.PROTOCOL, ErrorTag.MALFORMED_MESSAGE);
86             }
87         } else {
88             final String identifierName = path.getLastPathArgument().getNodeType().getLocalName();
89             if (!payloadName.equals(identifierName)) {
90                 throw new RestconfDocumentedException(
91                         "Payload name (" + payloadName + ") is different from identifier name (" + identifierName + ")",
92                         ErrorType.PROTOCOL, ErrorTag.MALFORMED_MESSAGE);
93             }
94         }
95     }
96
97     /**
98      * Validates whether keys in {@code payload} are equal to values of keys in
99      * {@code iiWithData} for list schema node
100      *
101      * @throws RestconfDocumentedException
102      *             if key values or key count in payload and URI isn't equal
103      *
104      */
105     public static void validateListKeysEqualityInPayloadAndUri(final NormalizedNodeContext payload) {
106         final InstanceIdentifierContext<?> iiWithData = payload.getInstanceIdentifierContext();
107         final PathArgument lastPathArgument = iiWithData.getInstanceIdentifier().getLastPathArgument();
108         final SchemaNode schemaNode = iiWithData.getSchemaNode();
109         final NormalizedNode<?, ?> data = payload.getData();
110         if (schemaNode instanceof ListSchemaNode) {
111             final List<QName> keyDefinitions = ((ListSchemaNode) schemaNode).getKeyDefinition();
112             if ((lastPathArgument instanceof NodeIdentifierWithPredicates) && (data instanceof MapEntryNode)) {
113                 final Map<QName, Object> uriKeyValues = ((NodeIdentifierWithPredicates) lastPathArgument)
114                         .getKeyValues();
115                 isEqualUriAndPayloadKeyValues(uriKeyValues, (MapEntryNode) data, keyDefinitions);
116             }
117         }
118     }
119
120     private static void isEqualUriAndPayloadKeyValues(final Map<QName, Object> uriKeyValues, final MapEntryNode payload,
121             final List<QName> keyDefinitions) {
122         final Map<QName, Object> mutableCopyUriKeyValues = Maps.newHashMap(uriKeyValues);
123         for (final QName keyDefinition : keyDefinitions) {
124             final Object uriKeyValue = mutableCopyUriKeyValues.remove(keyDefinition);
125             RestconfValidationUtils.checkDocumentedError(uriKeyValue != null, ErrorType.PROTOCOL, ErrorTag.DATA_MISSING,
126                     "Missing key " + keyDefinition + " in URI.");
127
128             final Object dataKeyValue = payload.getIdentifier().getKeyValues().get(keyDefinition);
129
130             if (!uriKeyValue.equals(dataKeyValue)) {
131                 final String errMsg = "The value '" + uriKeyValue + "' for key '" + keyDefinition.getLocalName()
132                         + "' specified in the URI doesn't match the value '" + dataKeyValue
133                         + "' specified in the message body. ";
134                 throw new RestconfDocumentedException(errMsg, ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE);
135             }
136         }
137     }
138
139     /**
140      * Check mount point and prepare variables for put data to DS
141      *
142      * @param payload
143      *            - data to put
144      * @param schemaCtxRef
145      *            - reference to {@link SchemaContext}
146      * @param transactionNode
147      *            - wrapper of variables for transaction
148      * @param point
149      *            - query parameter
150      * @param insert
151      *            - query parameter
152      * @return {@link CheckedFuture}
153      */
154     public static Response putData(final NormalizedNodeContext payload,
155             final SchemaContextRef schemaCtxRef, final TransactionVarsWrapper transactionNode, final String insert, final String point) {
156         final YangInstanceIdentifier path = payload.getInstanceIdentifierContext().getInstanceIdentifier();
157         final ResponseFactory responseFactory = new ResponseFactory(
158                 ReadDataTransactionUtil.readData(RestconfDataServiceConstant.ReadData.CONFIG, transactionNode));
159         final CheckedFuture<Void, TransactionCommitFailedException> submitData = submitData(path, schemaCtxRef.get(),
160                 transactionNode.getTransactionChain(), payload.getData(), insert, point);
161         FutureCallbackTx.addCallback(submitData, RestconfDataServiceConstant.PutData.PUT_TX_TYPE, responseFactory);
162         return responseFactory.build();
163     }
164
165     /**
166      * Put data to DS
167      *
168      * @param path
169      *            - path of data
170      * @param schemaContext
171      *            - {@link SchemaContext}
172      * @param domTransactionChain
173      *            - write transaction
174      * @param data
175      *            - data
176      * @param point
177      *            - query parameter
178      * @param insert
179      *            - query parameter
180      * @return {@link CheckedFuture}
181      */
182     private static CheckedFuture<Void, TransactionCommitFailedException> submitData(final YangInstanceIdentifier path,
183             final SchemaContext schemaContext, final DOMTransactionChain domTransactionChain,
184             final NormalizedNode<?, ?> data, final String insert, final String point) {
185         final DOMDataReadWriteTransaction newReadWriteTransaction = domTransactionChain.newReadWriteTransaction();
186         if (insert == null) {
187             return makePut(path, schemaContext, newReadWriteTransaction, data);
188         } else {
189             final DataSchemaNode schemaNode = checkListAndOrderedType(schemaContext, path);
190             switch (insert) {
191                 case "first":
192                     if (schemaNode instanceof ListSchemaNode) {
193                         final NormalizedNode<?, ?> readData =
194                                 readList(path, schemaContext, domTransactionChain, schemaNode);
195                         final OrderedMapNode readList = (OrderedMapNode) readData;
196                         if ((readList == null) || readList.getValue().isEmpty()) {
197                             return makePut(path, schemaContext, newReadWriteTransaction, data);
198                         } else {
199                             newReadWriteTransaction.delete(LogicalDatastoreType.CONFIGURATION, path.getParent());
200                             simplePut(LogicalDatastoreType.CONFIGURATION, path, newReadWriteTransaction, schemaContext, data);
201                             listPut(LogicalDatastoreType.CONFIGURATION, path.getParent(), newReadWriteTransaction, schemaContext,
202                                     readList);
203                             return newReadWriteTransaction.submit();
204                         }
205                     } else {
206                         final NormalizedNode<?, ?> readData =
207                                 readList(path, schemaContext, domTransactionChain, schemaNode);
208
209                         final OrderedLeafSetNode<?> readLeafList = (OrderedLeafSetNode<?>) readData;
210                         if ((readLeafList == null) || readLeafList.getValue().isEmpty()) {
211                             return makePut(path, schemaContext, newReadWriteTransaction, data);
212                         } else {
213                             newReadWriteTransaction.delete(LogicalDatastoreType.CONFIGURATION, path.getParent());
214                             simplePut(LogicalDatastoreType.CONFIGURATION, path, newReadWriteTransaction, schemaContext, data);
215                             listPut(LogicalDatastoreType.CONFIGURATION, path.getParent(), newReadWriteTransaction, schemaContext,
216                                     readLeafList);
217                             return newReadWriteTransaction.submit();
218                         }
219                     }
220                 case "last":
221                     return makePut(path, schemaContext, newReadWriteTransaction, data);
222                 case "before":
223                     if (schemaNode instanceof ListSchemaNode) {
224                         final NormalizedNode<?, ?> readData =
225                                 readList(path, schemaContext, domTransactionChain, schemaNode);
226                         final OrderedMapNode readList = (OrderedMapNode) readData;
227                         if ((readList == null) || readList.getValue().isEmpty()) {
228                             return makePut(path, schemaContext, newReadWriteTransaction, data);
229                         } else {
230                             insertWithPointListPut(newReadWriteTransaction, LogicalDatastoreType.CONFIGURATION, path, data,
231                                     schemaContext, point, readList, true);
232                             return newReadWriteTransaction.submit();
233                         }
234                     } else {
235                         final NormalizedNode<?, ?> readData =
236                                 readList(path, schemaContext, domTransactionChain, schemaNode);
237
238                         final OrderedLeafSetNode<?> readLeafList = (OrderedLeafSetNode<?>) readData;
239                         if ((readLeafList == null) || readLeafList.getValue().isEmpty()) {
240                             return makePut(path, schemaContext, newReadWriteTransaction, data);
241                         } else {
242                             insertWithPointLeafListPut(newReadWriteTransaction, LogicalDatastoreType.CONFIGURATION, path, data,
243                                     schemaContext, point, readLeafList, true);
244                             return newReadWriteTransaction.submit();
245                         }
246                     }
247                 case "after":
248                     if (schemaNode instanceof ListSchemaNode) {
249                         final NormalizedNode<?, ?> readData =
250                                 readList(path, schemaContext, domTransactionChain, schemaNode);
251                         final OrderedMapNode readList = (OrderedMapNode) readData;
252                         if ((readList == null) || readList.getValue().isEmpty()) {
253                             return makePut(path, schemaContext, newReadWriteTransaction, data);
254                         } else {
255                             insertWithPointListPut(newReadWriteTransaction, LogicalDatastoreType.CONFIGURATION, path, data,
256                                     schemaContext, point, readList, false);
257                             return newReadWriteTransaction.submit();
258                         }
259                     } else {
260                         final NormalizedNode<?, ?> readData =
261                                 readList(path, schemaContext, domTransactionChain, schemaNode);
262
263                         final OrderedLeafSetNode<?> readLeafList = (OrderedLeafSetNode<?>) readData;
264                         if ((readLeafList == null) || readLeafList.getValue().isEmpty()) {
265                             return makePut(path, schemaContext, newReadWriteTransaction, data);
266                         } else {
267                             insertWithPointLeafListPut(newReadWriteTransaction, LogicalDatastoreType.CONFIGURATION, path, data,
268                                     schemaContext, point, readLeafList, true);
269                             return newReadWriteTransaction.submit();
270                         }
271                     }
272                 default:
273                     throw new RestconfDocumentedException(
274                             "Used bad value of insert parameter. Possible values are first, last, before or after, "
275                                     + "but was: " + insert);
276             }
277         }
278     }
279
280     public static NormalizedNode<?, ?> readList(final YangInstanceIdentifier path, final SchemaContext schemaContext,
281             final DOMTransactionChain domTransactionChain, final DataSchemaNode schemaNode) {
282         final InstanceIdentifierContext<?> iid = new InstanceIdentifierContext<SchemaNode>(
283                 path.getParent(), schemaNode, null, schemaContext);
284         final TransactionVarsWrapper transactionNode =
285                 new TransactionVarsWrapper(iid, null, domTransactionChain);
286         final NormalizedNode<?, ?> readData = ReadDataTransactionUtil
287                 .readData(RestconfDataServiceConstant.ReadData.CONFIG, transactionNode);
288         return readData;
289     }
290
291     private static void insertWithPointLeafListPut(final DOMDataReadWriteTransaction rWTransaction,
292             final LogicalDatastoreType datastore, final YangInstanceIdentifier path,
293             final NormalizedNode<?, ?> data, final SchemaContext schemaContext, final String point,
294             final OrderedLeafSetNode<?> readLeafList, final boolean before) {
295         rWTransaction.delete(datastore, path.getParent());
296         final InstanceIdentifierContext<?> instanceIdentifier =
297                 ControllerContext.getInstance().toInstanceIdentifier(point);
298         int p = 0;
299         for (final LeafSetEntryNode<?> nodeChild : readLeafList.getValue()) {
300             if (nodeChild.getIdentifier().equals(instanceIdentifier.getInstanceIdentifier().getLastPathArgument())) {
301                 break;
302             }
303             p++;
304         }
305         if (!before) {
306             p++;
307         }
308         int h = 0;
309         final NormalizedNode<?, ?> emptySubtree = ImmutableNodes.fromInstanceId(schemaContext, path.getParent());
310         rWTransaction.merge(datastore, YangInstanceIdentifier.create(emptySubtree.getIdentifier()), emptySubtree);
311         for (final LeafSetEntryNode<?> nodeChild : readLeafList.getValue()) {
312             if (h == p) {
313                 simplePut(datastore, path, rWTransaction, schemaContext, data);
314             }
315             final YangInstanceIdentifier childPath = path.getParent().node(nodeChild.getIdentifier());
316             rWTransaction.put(datastore, childPath, nodeChild);
317             h++;
318         }
319     }
320
321     private static void insertWithPointListPut(final DOMDataReadWriteTransaction writeTx,
322             final LogicalDatastoreType datastore, final YangInstanceIdentifier path,
323             final NormalizedNode<?, ?> data, final SchemaContext schemaContext, final String point,
324             final OrderedMapNode readList, final boolean before) {
325         writeTx.delete(datastore, path.getParent());
326         final InstanceIdentifierContext<?> instanceIdentifier =
327                 ControllerContext.getInstance().toInstanceIdentifier(point);
328         int p = 0;
329         for (final MapEntryNode mapEntryNode : readList.getValue()) {
330             if (mapEntryNode.getIdentifier().equals(instanceIdentifier.getInstanceIdentifier().getLastPathArgument())) {
331                 break;
332             }
333             p++;
334         }
335         if (!before) {
336             p++;
337         }
338         int h = 0;
339         final NormalizedNode<?, ?> emptySubtree = ImmutableNodes.fromInstanceId(schemaContext, path.getParent());
340         writeTx.merge(datastore, YangInstanceIdentifier.create(emptySubtree.getIdentifier()), emptySubtree);
341         for (final MapEntryNode mapEntryNode : readList.getValue()) {
342             if (h == p) {
343                 simplePut(datastore, path, writeTx, schemaContext, data);
344             }
345             final YangInstanceIdentifier childPath = path.getParent().node(mapEntryNode.getIdentifier());
346             writeTx.put(datastore, childPath, mapEntryNode);
347             h++;
348         }
349     }
350
351     private static void listPut(final LogicalDatastoreType datastore, final YangInstanceIdentifier path,
352             final DOMDataReadWriteTransaction writeTx, final SchemaContext schemaContext,
353             final OrderedLeafSetNode<?> payload) {
354         final NormalizedNode<?, ?> emptySubtree = ImmutableNodes.fromInstanceId(schemaContext, path);
355         writeTx.merge(datastore, YangInstanceIdentifier.create(emptySubtree.getIdentifier()), emptySubtree);
356         TransactionUtil.ensureParentsByMerge(path, schemaContext, writeTx);
357         for (final LeafSetEntryNode<?> child : ((LeafSetNode<?>) payload).getValue()) {
358             final YangInstanceIdentifier childPath = path.node(child.getIdentifier());
359             writeTx.put(datastore, childPath, child);
360         }
361     }
362
363     private static void listPut(final LogicalDatastoreType datastore, final YangInstanceIdentifier path,
364             final DOMDataReadWriteTransaction writeTx, final SchemaContext schemaContext,
365             final OrderedMapNode payload) {
366         final NormalizedNode<?, ?> emptySubtree = ImmutableNodes.fromInstanceId(schemaContext, path);
367         writeTx.merge(datastore, YangInstanceIdentifier.create(emptySubtree.getIdentifier()), emptySubtree);
368         TransactionUtil.ensureParentsByMerge(path, schemaContext, writeTx);
369         for (final MapEntryNode child : ((MapNode) payload).getValue()) {
370             final YangInstanceIdentifier childPath = path.node(child.getIdentifier());
371             writeTx.put(datastore, childPath, child);
372         }
373     }
374
375     private static void simplePut(final LogicalDatastoreType configuration, final YangInstanceIdentifier path,
376             final DOMDataReadWriteTransaction writeTx, final SchemaContext schemaContext,
377             final NormalizedNode<?, ?> data) {
378         TransactionUtil.ensureParentsByMerge(path, schemaContext, writeTx);
379         writeTx.put(LogicalDatastoreType.CONFIGURATION, path, data);
380     }
381
382     private static CheckedFuture<Void, TransactionCommitFailedException> makePut(final YangInstanceIdentifier path,
383             final SchemaContext schemaContext, final DOMDataWriteTransaction writeTx, final NormalizedNode<?, ?> data) {
384         TransactionUtil.ensureParentsByMerge(path, schemaContext, writeTx);
385         writeTx.put(LogicalDatastoreType.CONFIGURATION, path, data);
386         return writeTx.submit();
387     }
388
389     public static DataSchemaNode checkListAndOrderedType(final SchemaContext ctx, final YangInstanceIdentifier path) {
390         final YangInstanceIdentifier parent = path.getParent();
391         final DataSchemaContextNode<?> node = DataSchemaContextTree.from(ctx).getChild(parent);
392         final DataSchemaNode dataSchemaNode = node.getDataSchemaNode();
393
394         if (dataSchemaNode instanceof ListSchemaNode) {
395             if (!((ListSchemaNode) dataSchemaNode).isUserOrdered()) {
396                 throw new RestconfDocumentedException("Insert parameter can be used only with ordered-by user list.");
397             }
398             return dataSchemaNode;
399         }
400         if (dataSchemaNode instanceof LeafListSchemaNode) {
401             if (!((LeafListSchemaNode) dataSchemaNode).isUserOrdered()) {
402                 throw new RestconfDocumentedException(
403                         "Insert parameter can be used only with ordered-by user leaf-list.");
404             }
405             return dataSchemaNode;
406         }
407         throw new RestconfDocumentedException("Insert parameter can be used only with list or leaf-list");
408     }
409 }