Bug: 8037 YANG Patch using "replace" instead of "merge"
[netconf.git] / restconf / sal-rest-connector / src / main / java / org / opendaylight / restconf / restful / utils / PatchDataTransactionUtil.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
9 package org.opendaylight.restconf.restful.utils;
10
11 import com.google.common.collect.ImmutableList;
12 import com.google.common.collect.Lists;
13 import com.google.common.util.concurrent.CheckedFuture;
14 import java.util.ArrayList;
15 import java.util.List;
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.netconf.sal.restconf.impl.PATCHContext;
22 import org.opendaylight.netconf.sal.restconf.impl.PATCHEditOperation;
23 import org.opendaylight.netconf.sal.restconf.impl.PATCHEntity;
24 import org.opendaylight.netconf.sal.restconf.impl.PATCHStatusContext;
25 import org.opendaylight.netconf.sal.restconf.impl.PATCHStatusEntity;
26 import org.opendaylight.netconf.sal.restconf.impl.RestconfDocumentedException;
27 import org.opendaylight.netconf.sal.restconf.impl.RestconfError;
28 import org.opendaylight.netconf.sal.restconf.impl.RestconfError.ErrorTag;
29 import org.opendaylight.netconf.sal.restconf.impl.RestconfError.ErrorType;
30 import org.opendaylight.restconf.RestConnectorProvider;
31 import org.opendaylight.restconf.common.references.SchemaContextRef;
32 import org.opendaylight.restconf.restful.transaction.TransactionVarsWrapper;
33 import org.opendaylight.restconf.restful.utils.RestconfDataServiceConstant.PatchData;
34 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
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.impl.schema.ImmutableNodes;
39 import org.opendaylight.yangtools.yang.model.api.SchemaContext;
40 import org.slf4j.Logger;
41 import org.slf4j.LoggerFactory;
42
43 public final class PatchDataTransactionUtil {
44     private static final Logger LOG = LoggerFactory.getLogger(PatchDataTransactionUtil.class);
45
46     private PatchDataTransactionUtil() {
47         throw new UnsupportedOperationException("Util class.");
48     }
49
50     /**
51      * Process edit operations of one {@link PATCHContext}.
52      * @param context Patch context to be processed
53      * @param transactionNode Wrapper for transaction
54      * @param schemaContextRef Soft reference for global schema context
55      * @return {@link PATCHStatusContext}
56      */
57     public static PATCHStatusContext patchData(final PATCHContext context, final TransactionVarsWrapper transactionNode,
58                                                final SchemaContextRef schemaContextRef) {
59         final List<PATCHStatusEntity> editCollection = new ArrayList<>();
60         boolean noError = true;
61         final DOMDataReadWriteTransaction tx = transactionNode.getTransactionChain().newReadWriteTransaction();
62
63         for (final PATCHEntity patchEntity : context.getData()) {
64             final PATCHEditOperation operation = PATCHEditOperation.valueOf(patchEntity.getOperation().toUpperCase());
65             if (noError) {
66                 switch (operation) {
67                     case CREATE:
68                         try {
69                             createDataWithinTransaction(LogicalDatastoreType.CONFIGURATION,
70                                     patchEntity.getTargetNode(), patchEntity.getNode(), tx, schemaContextRef);
71                             editCollection.add(new PATCHStatusEntity(patchEntity.getEditId(), true, null));
72                         } catch (final RestconfDocumentedException e) {
73                             editCollection.add(new PATCHStatusEntity(patchEntity.getEditId(),
74                                     false, Lists.newArrayList(e.getErrors())));
75                             noError = false;
76                         }
77                         break;
78                     case DELETE:
79                         try {
80                             deleteDataWithinTransaction(LogicalDatastoreType.CONFIGURATION, patchEntity.getTargetNode(),
81                                     tx);
82                             editCollection.add(new PATCHStatusEntity(patchEntity.getEditId(), true, null));
83                         } catch (final RestconfDocumentedException e) {
84                             editCollection.add(new PATCHStatusEntity(patchEntity.getEditId(),
85                                     false, Lists.newArrayList(e.getErrors())));
86                             noError = false;
87                         }
88                         break;
89                     case MERGE:
90                         try {
91                             mergeDataWithinTransaction(LogicalDatastoreType.CONFIGURATION,
92                                     patchEntity.getTargetNode(), patchEntity.getNode(), tx, schemaContextRef);
93                             editCollection.add(new PATCHStatusEntity(patchEntity.getEditId(), true, null));
94                         } catch (final RestconfDocumentedException e) {
95                             editCollection.add(new PATCHStatusEntity(patchEntity.getEditId(),
96                                     false, Lists.newArrayList(e.getErrors())));
97                             noError = false;
98                         }
99                         break;
100                     case REPLACE:
101                         try {
102                             replaceDataWithinTransaction(LogicalDatastoreType.CONFIGURATION,
103                                     patchEntity.getTargetNode(), patchEntity.getNode(), schemaContextRef, tx);
104                             editCollection.add(new PATCHStatusEntity(patchEntity.getEditId(), true, null));
105                         } catch (final RestconfDocumentedException e) {
106                             editCollection.add(new PATCHStatusEntity(patchEntity.getEditId(),
107                                     false, Lists.newArrayList(e.getErrors())));
108                             noError = false;
109                         }
110                         break;
111                     case REMOVE:
112                         try {
113                             removeDataWithinTransaction(LogicalDatastoreType.CONFIGURATION, patchEntity.getTargetNode(),
114                                     tx);
115                             editCollection.add(new PATCHStatusEntity(patchEntity.getEditId(), true, null));
116                         } catch (final RestconfDocumentedException e) {
117                             editCollection.add(new PATCHStatusEntity(patchEntity.getEditId(),
118                                     false, Lists.newArrayList(e.getErrors())));
119                             noError = false;
120                         }
121                         break;
122                     default:
123                         editCollection.add(new PATCHStatusEntity(patchEntity.getEditId(),
124                                 false, Lists.newArrayList(new RestconfError(ErrorType.PROTOCOL,
125                                 ErrorTag.OPERATION_NOT_SUPPORTED, "Not supported Yang PATCH operation"))));
126                         noError = false;
127                         break;
128                 }
129             } else {
130                 break;
131             }
132         }
133
134         // if no errors then submit transaction, otherwise cancel
135         if (noError) {
136             final ResponseFactory response = new ResponseFactory();
137             final CheckedFuture<Void, TransactionCommitFailedException> future = tx.submit();
138
139             try {
140                 FutureCallbackTx.addCallback(future, PatchData.PATCH_TX_TYPE, response);
141             } catch (final RestconfDocumentedException e) {
142                 // if errors occurred during transaction commit then patch failed and global errors are reported
143                 return new PATCHStatusContext(context.getPatchId(), ImmutableList.copyOf(editCollection), false,
144                         Lists.newArrayList(e.getErrors()));
145             }
146
147             return new PATCHStatusContext(context.getPatchId(), ImmutableList.copyOf(editCollection), true, null);
148         } else {
149             tx.cancel();
150             RestConnectorProvider.resetTransactionChainForAdapaters(transactionNode.getTransactionChain());
151             return new PATCHStatusContext(context.getPatchId(), ImmutableList.copyOf(editCollection),
152                     false, null);
153         }
154     }
155
156     /**
157      * Create data within one transaction, return error if already exists.
158      * @param dataStore Datastore to write data to
159      * @param path Path for data to be created
160      * @param payload Data to be created
161      * @param rWTransaction Transaction
162      * @param schemaContextRef Soft reference for global schema context
163      */
164     private static void createDataWithinTransaction(final LogicalDatastoreType dataStore,
165                                                     final YangInstanceIdentifier path,
166                                                     final NormalizedNode<?, ?> payload,
167                                                     final DOMDataReadWriteTransaction rWTransaction,
168                                                     final SchemaContextRef schemaContextRef) {
169         LOG.trace("POST {} within Restconf PATCH: {} with payload {}", dataStore.name(), path, payload);
170         createData(payload, schemaContextRef.get(), path, rWTransaction, dataStore, true);
171     }
172
173     /**
174      * Check if data exists and remove it within one transaction.
175      * @param dataStore Datastore to delete data from
176      * @param path Path for data to be deleted
177      * @param readWriteTransaction Transaction
178      */
179     private static void deleteDataWithinTransaction(final LogicalDatastoreType dataStore,
180                                                     final YangInstanceIdentifier path,
181                                                     final DOMDataReadWriteTransaction readWriteTransaction) {
182         LOG.trace("Delete {} within Restconf PATCH: {}", dataStore.name(), path);
183         checkItemExistsWithinTransaction(readWriteTransaction, dataStore, path);
184         readWriteTransaction.delete(dataStore, path);
185     }
186
187     /**
188      * Merge data within one transaction.
189      * @param dataStore Datastore to merge data to
190      * @param path Path for data to be merged
191      * @param payload Data to be merged
192      * @param writeTransaction Transaction
193      * @param schemaContextRef Soft reference for global schema context
194      */
195     private static void mergeDataWithinTransaction(final LogicalDatastoreType dataStore,
196                                                    final YangInstanceIdentifier path,
197                                                    final NormalizedNode<?, ?> payload,
198                                                    final DOMDataReadWriteTransaction writeTransaction,
199                                                    final SchemaContextRef schemaContextRef) {
200         LOG.trace("Merge {} within Restconf PATCH: {} with payload {}", dataStore.name(), path, payload);
201         TransactionUtil.ensureParentsByMerge(path, schemaContextRef.get(), writeTransaction);
202         writeTransaction.merge(dataStore, path, payload);
203     }
204
205     /**
206      * Do NOT check if data exists and remove it within one transaction.
207      * @param dataStore Datastore to delete data from
208      * @param path Path for data to be deleted
209      * @param writeTransaction Transaction
210      */
211     private static void removeDataWithinTransaction(final LogicalDatastoreType dataStore,
212                                                     final YangInstanceIdentifier path,
213                                                     final DOMDataWriteTransaction writeTransaction) {
214         LOG.trace("Remove {} within Restconf PATCH: {}", dataStore.name(), path);
215         writeTransaction.delete(dataStore, path);
216     }
217
218     /**
219      * Create data within one transaction, replace if already exists.
220      * @param dataStore Datastore to write data to
221      * @param path Path for data to be created
222      * @param payload Data to be created
223      * @param schemaContextRef Soft reference for global schema context
224      * @param rWTransaction Transaction
225      */
226     private static void replaceDataWithinTransaction(final LogicalDatastoreType dataStore,
227                                                      final YangInstanceIdentifier path,
228                                                      final NormalizedNode<?, ?> payload,
229                                                      final SchemaContextRef schemaContextRef,
230                                                      final DOMDataReadWriteTransaction rWTransaction) {
231         LOG.trace("PUT {} within Restconf PATCH: {} with payload {}", dataStore.name(), path, payload);
232         createData(payload, schemaContextRef.get(), path, rWTransaction, dataStore, false);
233     }
234
235     /**
236      * Create data within one transaction. If {@code errorIfExists} is set to {@code true} then data will be checked
237      * for existence before created, otherwise they will be overwritten.
238      * @param payload Data to be created
239      * @param schemaContext Global schema context
240      * @param path Path for data to be created
241      * @param rWTransaction Transaction
242      * @param dataStore Datastore to write data to
243      * @param errorIfExists Enable checking for existence of data (throws error if already exists)
244      */
245     private static void createData(final NormalizedNode<?, ?> payload, final SchemaContext schemaContext,
246                                    final YangInstanceIdentifier path, final DOMDataReadWriteTransaction rWTransaction,
247                                    final LogicalDatastoreType dataStore, final boolean errorIfExists) {
248         if (payload instanceof MapNode) {
249             final NormalizedNode<?, ?> emptySubtree = ImmutableNodes.fromInstanceId(schemaContext, path);
250             rWTransaction.merge(dataStore, YangInstanceIdentifier.create(emptySubtree.getIdentifier()), emptySubtree);
251             TransactionUtil.ensureParentsByMerge(path, schemaContext, rWTransaction);
252             for (final MapEntryNode child : ((MapNode) payload).getValue()) {
253                 final YangInstanceIdentifier childPath = path.node(child.getIdentifier());
254
255                 if (errorIfExists) {
256                     checkItemDoesNotExistsWithinTransaction(rWTransaction, dataStore, childPath);
257                 }
258
259                 rWTransaction.put(dataStore, childPath, child);
260             }
261         } else {
262             if (errorIfExists) {
263                 checkItemDoesNotExistsWithinTransaction(rWTransaction, dataStore, path);
264             }
265
266             TransactionUtil.ensureParentsByMerge(path, schemaContext, rWTransaction);
267             rWTransaction.put(dataStore, path, payload);
268         }
269     }
270
271     /**
272      * Check if items already exists at specified {@code path}. Throws {@link RestconfDocumentedException} if
273      * data does NOT already exists.
274      * @param rWTransaction Transaction
275      * @param store Datastore
276      * @param path Path to be checked
277      */
278     public static void checkItemExistsWithinTransaction(final DOMDataReadWriteTransaction rWTransaction,
279                                                         final LogicalDatastoreType store, final YangInstanceIdentifier path) {
280         final CheckedFuture<Boolean, ReadFailedException> future = rWTransaction.exists(store, path);
281         final FutureDataFactory<Boolean> response = new FutureDataFactory<>();
282
283         FutureCallbackTx.addCallback(future, PatchData.PATCH_TX_TYPE, response);
284
285         if (!response.result) {
286             final String errMsg = "Operation via Restconf was not executed because data does not exist";
287             LOG.trace("{}:{}", errMsg, path);
288             throw new RestconfDocumentedException(
289                     "Data does not exist", ErrorType.PROTOCOL, ErrorTag.DATA_MISSING, path);
290         }
291     }
292
293     /**
294      * Check if items do NOT already exists at specified {@code path}. Throws {@link RestconfDocumentedException} if
295      * data already exists.
296      * @param rWTransaction Transaction
297      * @param store Datastore
298      * @param path Path to be checked
299      */
300     public static void checkItemDoesNotExistsWithinTransaction(final DOMDataReadWriteTransaction rWTransaction,
301                                                                final LogicalDatastoreType store, final YangInstanceIdentifier path) {
302         final CheckedFuture<Boolean, ReadFailedException> future = rWTransaction.exists(store, path);
303         final FutureDataFactory<Boolean> response = new FutureDataFactory<>();
304
305         FutureCallbackTx.addCallback(future, PatchData.PATCH_TX_TYPE, response);
306
307         if (response.result) {
308             final String errMsg = "Operation via Restconf was not executed because data already exists";
309             LOG.trace("{}:{}", errMsg, path);
310             throw new RestconfDocumentedException(
311                     "Data already exists", ErrorType.PROTOCOL, ErrorTag.DATA_EXISTS, path);
312         }
313     }
314 }