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