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