Split transaction lifecycle
[netconf.git] / restconf / restconf-nb-rfc8040 / src / main / java / org / opendaylight / restconf / nb / rfc8040 / rests / utils / PostDataTransactionUtil.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.nb.rfc8040.rests.utils;
9
10 import com.google.common.util.concurrent.FluentFuture;
11 import java.net.URI;
12 import java.util.Collection;
13 import java.util.Optional;
14 import javax.ws.rs.core.Response;
15 import javax.ws.rs.core.Response.Status;
16 import javax.ws.rs.core.UriInfo;
17 import org.opendaylight.mdsal.common.api.CommitInfo;
18 import org.opendaylight.mdsal.common.api.LogicalDatastoreType;
19 import org.opendaylight.mdsal.dom.api.DOMTransactionChain;
20 import org.opendaylight.restconf.common.context.InstanceIdentifierContext;
21 import org.opendaylight.restconf.common.context.NormalizedNodeContext;
22 import org.opendaylight.restconf.common.errors.RestconfDocumentedException;
23 import org.opendaylight.restconf.common.errors.RestconfError;
24 import org.opendaylight.restconf.common.errors.RestconfError.ErrorTag;
25 import org.opendaylight.restconf.common.errors.RestconfError.ErrorType;
26 import org.opendaylight.restconf.nb.rfc8040.rests.transactions.RestconfStrategy;
27 import org.opendaylight.restconf.nb.rfc8040.rests.transactions.RestconfTransaction;
28 import org.opendaylight.restconf.nb.rfc8040.rests.utils.RestconfDataServiceConstant.PostPutQueryParameters.Insert;
29 import org.opendaylight.restconf.nb.rfc8040.utils.parser.ParserIdentifier;
30 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
31 import org.opendaylight.yangtools.yang.data.api.schema.MapEntryNode;
32 import org.opendaylight.yangtools.yang.data.api.schema.MapNode;
33 import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode;
34 import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNodeContainer;
35 import org.opendaylight.yangtools.yang.data.impl.schema.ImmutableNodes;
36 import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext;
37 import org.opendaylight.yangtools.yang.model.api.SchemaContext;
38 import org.slf4j.Logger;
39 import org.slf4j.LoggerFactory;
40
41 /**
42  * Util class to post data to DS.
43  *
44  */
45 public final class PostDataTransactionUtil {
46     private static final Logger LOG = LoggerFactory.getLogger(PostDataTransactionUtil.class);
47     // FIXME: why is this being reused from other places?
48     static final String POST_TX_TYPE = "POST";
49
50     private PostDataTransactionUtil() {
51         // Hidden on purpose
52     }
53
54     /**
55      * Check mount point and prepare variables for post data. Close {@link DOMTransactionChain} if any inside of object
56      * {@link RestconfStrategy} provided as a parameter.
57      *
58      * @param uriInfo       uri info
59      * @param payload       data
60      * @param strategy      Object that perform the actual DS operations
61      * @param schemaContext reference to actual {@link EffectiveModelContext}
62      * @param point         point
63      * @param insert        insert
64      * @return {@link Response}
65      */
66     public static Response postData(final UriInfo uriInfo, final NormalizedNodeContext payload,
67                                     final RestconfStrategy strategy,
68                                     final EffectiveModelContext schemaContext, final Insert insert,
69                                     final String point) {
70         final YangInstanceIdentifier path = payload.getInstanceIdentifierContext().getInstanceIdentifier();
71         final FluentFuture<? extends CommitInfo> future = submitData(path, payload.getData(),
72                 strategy, schemaContext, insert, point);
73         final URI location = resolveLocation(uriInfo, path, schemaContext, payload.getData());
74         final ResponseFactory dataFactory = new ResponseFactory(Status.CREATED).location(location);
75         //This method will close transactionChain if any
76         FutureCallbackTx.addCallback(future, POST_TX_TYPE, dataFactory, strategy, path);
77         return dataFactory.build();
78     }
79
80     /**
81      * Post data by type.
82      *
83      * @param path          path
84      * @param data          data
85      * @param strategy      object that perform the actual DS operations
86      * @param schemaContext schema context of data
87      * @param point         query parameter
88      * @param insert        query parameter
89      * @return {@link FluentFuture}
90      */
91     private static FluentFuture<? extends CommitInfo> submitData(final YangInstanceIdentifier path,
92                                                                  final NormalizedNode<?, ?> data,
93                                                                  final RestconfStrategy strategy,
94                                                                  final EffectiveModelContext schemaContext,
95                                                                  final Insert insert, final String point) {
96         final RestconfTransaction transaction = strategy.prepareWriteExecution();
97         if (insert == null) {
98             makePost(path, data, schemaContext, transaction);
99             return transaction.commit();
100         }
101
102         PutDataTransactionUtil.checkListAndOrderedType(schemaContext, path);
103         final NormalizedNode<?, ?> readData;
104         switch (insert) {
105             case FIRST:
106                 readData = PutDataTransactionUtil.readList(strategy, path.getParent().getParent());
107                 if (readData == null || ((NormalizedNodeContainer<?, ?, ?>) readData).getValue().isEmpty()) {
108                     transaction.replace(LogicalDatastoreType.CONFIGURATION, path, data, schemaContext);
109                     return transaction.commit();
110                 }
111                 checkItemDoesNotExists(strategy.exists(LogicalDatastoreType.CONFIGURATION, path), path);
112                 transaction.remove(LogicalDatastoreType.CONFIGURATION, path.getParent().getParent());
113                 transaction.replace(LogicalDatastoreType.CONFIGURATION, path, data, schemaContext);
114                 transaction.replace(LogicalDatastoreType.CONFIGURATION, path.getParent().getParent(), readData,
115                     schemaContext);
116                 return transaction.commit();
117             case LAST:
118                 makePost(path, data, schemaContext, transaction);
119                 return transaction.commit();
120             case BEFORE:
121                 readData = PutDataTransactionUtil.readList(strategy, path.getParent().getParent());
122                 if (readData == null || ((NormalizedNodeContainer<?, ?, ?>) readData).getValue().isEmpty()) {
123                     transaction.replace(LogicalDatastoreType.CONFIGURATION, path, data, schemaContext);
124                     return transaction.commit();
125                 }
126                 checkItemDoesNotExists(strategy.exists(LogicalDatastoreType.CONFIGURATION, path), path);
127                 insertWithPointPost(path, data, schemaContext, point,
128                     (NormalizedNodeContainer<?, ?, NormalizedNode<?, ?>>) readData, true, transaction);
129                 return transaction.commit();
130             case AFTER:
131                 readData = PutDataTransactionUtil.readList(strategy, path.getParent().getParent());
132                 if (readData == null || ((NormalizedNodeContainer<?, ?, ?>) readData).getValue().isEmpty()) {
133                     transaction.replace(LogicalDatastoreType.CONFIGURATION, path, data, schemaContext);
134                     return transaction.commit();
135                 }
136                 checkItemDoesNotExists(strategy.exists(LogicalDatastoreType.CONFIGURATION, path), path);
137                 insertWithPointPost(path, data, schemaContext, point,
138                     (NormalizedNodeContainer<?, ?, NormalizedNode<?, ?>>) readData, false, transaction);
139                 return transaction.commit();
140             default:
141                 throw new RestconfDocumentedException(
142                     "Used bad value of insert parameter. Possible values are first, last, before or after, but was: "
143                         + insert, RestconfError.ErrorType.PROTOCOL, RestconfError.ErrorTag.BAD_ATTRIBUTE);
144         }
145     }
146
147     private static void insertWithPointPost(final YangInstanceIdentifier path,
148                                             final NormalizedNode<?, ?> data,
149                                             final EffectiveModelContext schemaContext, final String point,
150                                             final NormalizedNodeContainer<?, ?, NormalizedNode<?, ?>> readList,
151                                             final boolean before, final RestconfTransaction transaction) {
152         final YangInstanceIdentifier parent = path.getParent().getParent();
153         transaction.remove(LogicalDatastoreType.CONFIGURATION, parent);
154         final InstanceIdentifierContext<?> instanceIdentifier =
155             ParserIdentifier.toInstanceIdentifier(point, schemaContext, Optional.empty());
156         int lastItemPosition = 0;
157         for (final NormalizedNode<?, ?> nodeChild : readList.getValue()) {
158             if (nodeChild.getIdentifier().equals(instanceIdentifier.getInstanceIdentifier().getLastPathArgument())) {
159                 break;
160             }
161             lastItemPosition++;
162         }
163         if (!before) {
164             lastItemPosition++;
165         }
166         int lastInsertedPosition = 0;
167         final NormalizedNode<?, ?> emptySubtree = ImmutableNodes.fromInstanceId(schemaContext, parent);
168         transaction.merge(LogicalDatastoreType.CONFIGURATION,
169             YangInstanceIdentifier.create(emptySubtree.getIdentifier()), emptySubtree);
170         for (final NormalizedNode<?, ?> nodeChild : readList.getValue()) {
171             if (lastInsertedPosition == lastItemPosition) {
172                 transaction.replace(LogicalDatastoreType.CONFIGURATION, path, data, schemaContext);
173             }
174             final YangInstanceIdentifier childPath = parent.node(nodeChild.getIdentifier());
175             transaction.replace(LogicalDatastoreType.CONFIGURATION, childPath, nodeChild, schemaContext);
176             lastInsertedPosition++;
177         }
178     }
179
180     private static void makePost(final YangInstanceIdentifier path, final NormalizedNode<?, ?> data,
181                                  final SchemaContext schemaContext, final RestconfTransaction transaction) {
182         try {
183             transaction.create(LogicalDatastoreType.CONFIGURATION, path, data, schemaContext);
184         } catch (RestconfDocumentedException e) {
185             // close transaction if any and pass exception further
186             transaction.cancel();
187             throw e;
188         }
189     }
190
191     /**
192      * Get location from {@link YangInstanceIdentifier} and {@link UriInfo}.
193      *
194      * @param uriInfo       uri info
195      * @param initialPath   data path
196      * @param schemaContext reference to {@link SchemaContext}
197      * @return {@link URI}
198      */
199     private static URI resolveLocation(final UriInfo uriInfo, final YangInstanceIdentifier initialPath,
200                                        final EffectiveModelContext schemaContext, final NormalizedNode<?, ?> data) {
201         if (uriInfo == null) {
202             return null;
203         }
204
205         YangInstanceIdentifier path = initialPath;
206         if (data instanceof MapNode) {
207             final Collection<MapEntryNode> children = ((MapNode) data).getValue();
208             if (!children.isEmpty()) {
209                 path = path.node(children.iterator().next().getIdentifier());
210             }
211         }
212
213         return uriInfo.getBaseUriBuilder()
214                 .path("data")
215                 .path(ParserIdentifier.stringFromYangInstanceIdentifier(path, schemaContext))
216                 .build();
217     }
218
219     /**
220      * Check if items do NOT already exists at specified {@code path}. Throws {@link RestconfDocumentedException} if
221      * data already exists.
222      *
223      * @param isExistsFuture if checked data exists
224      * @param path           Path to be checked
225      */
226     public static void checkItemDoesNotExists(final FluentFuture<Boolean> isExistsFuture,
227                                               final YangInstanceIdentifier path) {
228         final FutureDataFactory<Boolean> response = new FutureDataFactory<>();
229         FutureCallbackTx.addCallback(isExistsFuture, POST_TX_TYPE, response);
230
231         if (response.result) {
232             LOG.trace("Operation via Restconf was not executed because data at {} already exists", path);
233             throw new RestconfDocumentedException(
234                 "Data already exists", ErrorType.PROTOCOL, ErrorTag.DATA_EXISTS, path);
235         }
236     }
237 }