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