Split transaction lifecycle
[netconf.git] / restconf / restconf-nb-rfc8040 / src / main / java / org / opendaylight / restconf / nb / rfc8040 / rests / 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.nb.rfc8040.rests.utils;
9
10 import com.google.common.util.concurrent.FluentFuture;
11 import java.util.HashMap;
12 import java.util.List;
13 import java.util.Map;
14 import java.util.Optional;
15 import javax.ws.rs.core.Response;
16 import javax.ws.rs.core.Response.Status;
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.common.QName;
31 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
32 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifierWithPredicates;
33 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.PathArgument;
34 import org.opendaylight.yangtools.yang.data.api.schema.MapEntryNode;
35 import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode;
36 import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNodeContainer;
37 import org.opendaylight.yangtools.yang.data.impl.schema.ImmutableNodes;
38 import org.opendaylight.yangtools.yang.data.util.DataSchemaContextNode;
39 import org.opendaylight.yangtools.yang.data.util.DataSchemaContextTree;
40 import org.opendaylight.yangtools.yang.model.api.DataSchemaNode;
41 import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext;
42 import org.opendaylight.yangtools.yang.model.api.LeafListSchemaNode;
43 import org.opendaylight.yangtools.yang.model.api.ListSchemaNode;
44 import org.opendaylight.yangtools.yang.model.api.SchemaContext;
45 import org.opendaylight.yangtools.yang.model.api.SchemaNode;
46
47 /**
48  * Util class for put data to DS.
49  *
50  */
51 public final class PutDataTransactionUtil {
52     private static final String PUT_TX_TYPE = "PUT";
53
54     private PutDataTransactionUtil() {
55     }
56
57     /**
58      * Valid input data with {@link SchemaNode}.
59      *
60      * @param schemaNode {@link SchemaNode}
61      * @param payload    input data
62      */
63     public static void validInputData(final SchemaNode schemaNode, final NormalizedNodeContext payload) {
64         if (schemaNode != null && payload.getData() == null) {
65             throw new RestconfDocumentedException("Input is required.", ErrorType.PROTOCOL, ErrorTag.MALFORMED_MESSAGE);
66         } else if (schemaNode == null && payload.getData() != null) {
67             throw new RestconfDocumentedException("No input expected.", ErrorType.PROTOCOL, ErrorTag.MALFORMED_MESSAGE);
68         }
69     }
70
71     /**
72      * Valid top level node name.
73      *
74      * @param path    path of node
75      * @param payload data
76      */
77     public static void validTopLevelNodeName(final YangInstanceIdentifier path, final NormalizedNodeContext payload) {
78         final String payloadName = payload.getData().getNodeType().getLocalName();
79
80         if (path.isEmpty()) {
81             if (!payload.getData().getNodeType().equals(RestconfDataServiceConstant.NETCONF_BASE_QNAME)) {
82                 throw new RestconfDocumentedException("Instance identifier has to contain at least one path argument",
83                         ErrorType.PROTOCOL, ErrorTag.MALFORMED_MESSAGE);
84             }
85         } else {
86             final String identifierName = path.getLastPathArgument().getNodeType().getLocalName();
87             if (!payloadName.equals(identifierName)) {
88                 throw new RestconfDocumentedException(
89                         "Payload name (" + payloadName + ") is different from identifier name (" + identifierName + ")",
90                         ErrorType.PROTOCOL, ErrorTag.MALFORMED_MESSAGE);
91             }
92         }
93     }
94
95     /**
96      * Validates whether keys in {@code payload} are equal to values of keys in
97      * {@code iiWithData} for list schema node.
98      *
99      * @throws RestconfDocumentedException if key values or key count in payload and URI isn't equal
100      */
101     public static void validateListKeysEqualityInPayloadAndUri(final NormalizedNodeContext payload) {
102         final InstanceIdentifierContext<?> iiWithData = payload.getInstanceIdentifierContext();
103         final PathArgument lastPathArgument = iiWithData.getInstanceIdentifier().getLastPathArgument();
104         final SchemaNode schemaNode = iiWithData.getSchemaNode();
105         final NormalizedNode<?, ?> data = payload.getData();
106         if (schemaNode instanceof ListSchemaNode) {
107             final List<QName> keyDefinitions = ((ListSchemaNode) schemaNode).getKeyDefinition();
108             if (lastPathArgument instanceof NodeIdentifierWithPredicates && data instanceof MapEntryNode) {
109                 final Map<QName, Object> uriKeyValues = ((NodeIdentifierWithPredicates) lastPathArgument).asMap();
110                 isEqualUriAndPayloadKeyValues(uriKeyValues, (MapEntryNode) data, keyDefinitions);
111             }
112         }
113     }
114
115     private static void isEqualUriAndPayloadKeyValues(final Map<QName, Object> uriKeyValues, final MapEntryNode payload,
116                                                       final List<QName> keyDefinitions) {
117         final Map<QName, Object> mutableCopyUriKeyValues = new HashMap<>(uriKeyValues);
118         for (final QName keyDefinition : keyDefinitions) {
119             final Object uriKeyValue = RestconfDocumentedException.throwIfNull(
120                     mutableCopyUriKeyValues.remove(keyDefinition), ErrorType.PROTOCOL, ErrorTag.DATA_MISSING,
121                     "Missing key %s in URI.", keyDefinition);
122
123             final Object dataKeyValue = payload.getIdentifier().getValue(keyDefinition);
124
125             if (!uriKeyValue.equals(dataKeyValue)) {
126                 final String errMsg = "The value '" + uriKeyValue + "' for key '" + keyDefinition.getLocalName()
127                         + "' specified in the URI doesn't match the value '" + dataKeyValue
128                         + "' specified in the message body. ";
129                 throw new RestconfDocumentedException(errMsg, ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE);
130             }
131         }
132     }
133
134     /**
135      * Check mount point and prepare variables for put data to DS. Close {@link DOMTransactionChain} if any
136      * inside of object {@link RestconfStrategy} provided as a parameter if any.
137      *
138      * @param payload       data to put
139      * @param schemaContext reference to {@link EffectiveModelContext}
140      * @param strategy      object that perform the actual DS operations
141      * @param point         query parameter
142      * @param insert        query parameter
143      * @return {@link Response}
144      */
145     public static Response putData(final NormalizedNodeContext payload, final EffectiveModelContext schemaContext,
146                                    final RestconfStrategy strategy, final Insert insert, final String point) {
147         final YangInstanceIdentifier path = payload.getInstanceIdentifierContext().getInstanceIdentifier();
148
149         final FluentFuture<Boolean> existsFuture = strategy.exists(LogicalDatastoreType.CONFIGURATION, path);
150         final FutureDataFactory<Boolean> existsResponse = new FutureDataFactory<>();
151         FutureCallbackTx.addCallback(existsFuture, PUT_TX_TYPE, existsResponse);
152
153         final ResponseFactory responseFactory =
154             new ResponseFactory(existsResponse.result ? Status.NO_CONTENT : Status.CREATED);
155         final FluentFuture<? extends CommitInfo> submitData = submitData(path, schemaContext, strategy,
156             payload.getData(), insert, point);
157         //This method will close transactionChain if any
158         FutureCallbackTx.addCallback(submitData, PUT_TX_TYPE, responseFactory, strategy, path);
159         return responseFactory.build();
160     }
161
162     /**
163      * Put data to DS.
164      *
165      * @param path          path of data
166      * @param schemaContext {@link SchemaContext}
167      * @param strategy      object that perform the actual DS operations
168      * @param data          data
169      * @param point         query parameter
170      * @param insert        query parameter
171      * @return {@link FluentFuture}
172      */
173     private static FluentFuture<? extends CommitInfo> submitData(final YangInstanceIdentifier path,
174                                                                  final EffectiveModelContext schemaContext,
175                                                                  final RestconfStrategy strategy,
176                                                                  final NormalizedNode<?, ?> data,
177                                                                  final Insert insert, final String point) {
178         final RestconfTransaction transaction = strategy.prepareWriteExecution();
179         if (insert == null) {
180             return makePut(path, schemaContext, transaction, data);
181         }
182
183         checkListAndOrderedType(schemaContext, path);
184         final NormalizedNode<?, ?> readData;
185         switch (insert) {
186             case FIRST:
187                 readData = readList(strategy, path.getParent());
188                 if (readData == null || ((NormalizedNodeContainer<?, ?, ?>) readData).getValue().isEmpty()) {
189                     return makePut(path, schemaContext, transaction, data);
190                 }
191                 transaction.remove(LogicalDatastoreType.CONFIGURATION, path.getParent());
192                 transaction.replace(LogicalDatastoreType.CONFIGURATION, path, data, schemaContext);
193                 transaction.replace(LogicalDatastoreType.CONFIGURATION, path.getParent(), readData, schemaContext);
194                 return transaction.commit();
195             case LAST:
196                 return makePut(path, schemaContext, transaction, data);
197             case BEFORE:
198                 readData = readList(strategy, path.getParent());
199                 if (readData == null || ((NormalizedNodeContainer<?, ?, ?>) readData).getValue().isEmpty()) {
200                     return makePut(path, schemaContext, transaction, data);
201                 }
202                 insertWithPointPut(transaction, path, data, schemaContext, point,
203                     (NormalizedNodeContainer<?, ?, NormalizedNode<?, ?>>) readData, true);
204                 return transaction.commit();
205             case AFTER:
206                 readData = readList(strategy, path.getParent());
207                 if (readData == null || ((NormalizedNodeContainer<?, ?, ?>) readData).getValue().isEmpty()) {
208                     return makePut(path, schemaContext, transaction, data);
209                 }
210                 insertWithPointPut(transaction, path, data, schemaContext, point,
211                     (NormalizedNodeContainer<?, ?, NormalizedNode<?, ?>>) readData, false);
212                 return transaction.commit();
213             default:
214                 throw new RestconfDocumentedException(
215                         "Used bad value of insert parameter. Possible values are first, last, before or after, "
216                                 + "but was: " + insert, RestconfError.ErrorType.PROTOCOL, ErrorTag.BAD_ATTRIBUTE);
217         }
218     }
219
220     // FIXME: this method is only called from a context where we are modifying data. This should be part of strategy,
221     //        requiring an already-open transaction. It also must return a future, so it can be properly composed.
222     static NormalizedNode<?, ?> readList(final RestconfStrategy strategy, final YangInstanceIdentifier path) {
223         return ReadDataTransactionUtil.readDataViaTransaction(strategy, LogicalDatastoreType.CONFIGURATION,
224             path,false);
225     }
226
227     private static void insertWithPointPut(final RestconfTransaction transaction,
228                                            final YangInstanceIdentifier path,
229                                            final NormalizedNode<?, ?> data,
230                                            final EffectiveModelContext schemaContext, final String point,
231                                            final NormalizedNodeContainer<?, ?, NormalizedNode<?, ?>> readList,
232                                            final boolean before) {
233         transaction.remove(LogicalDatastoreType.CONFIGURATION, path.getParent());
234         final InstanceIdentifierContext<?> instanceIdentifier =
235             ParserIdentifier.toInstanceIdentifier(point, schemaContext, Optional.empty());
236         int lastItemPosition = 0;
237         for (final NormalizedNode<?, ?> nodeChild : readList.getValue()) {
238             if (nodeChild.getIdentifier().equals(instanceIdentifier.getInstanceIdentifier().getLastPathArgument())) {
239                 break;
240             }
241             lastItemPosition++;
242         }
243         if (!before) {
244             lastItemPosition++;
245         }
246         int lastInsertedPosition = 0;
247         final NormalizedNode<?, ?> emptySubtree = ImmutableNodes.fromInstanceId(schemaContext, path.getParent());
248         transaction.merge(LogicalDatastoreType.CONFIGURATION,
249             YangInstanceIdentifier.create(emptySubtree.getIdentifier()),
250             emptySubtree);
251         for (final NormalizedNode<?, ?> nodeChild : readList.getValue()) {
252             if (lastInsertedPosition == lastItemPosition) {
253                 transaction.replace(LogicalDatastoreType.CONFIGURATION, path, data, schemaContext);
254             }
255             final YangInstanceIdentifier childPath = path.getParent().node(nodeChild.getIdentifier());
256             transaction.replace(LogicalDatastoreType.CONFIGURATION, childPath, nodeChild, schemaContext);
257             lastInsertedPosition++;
258         }
259     }
260
261     private static FluentFuture<? extends CommitInfo> makePut(final YangInstanceIdentifier path,
262                                                               final SchemaContext schemaContext,
263                                                               final RestconfTransaction transaction,
264                                                               final NormalizedNode<?, ?> data) {
265         transaction.replace(LogicalDatastoreType.CONFIGURATION, path, data, schemaContext);
266         return transaction.commit();
267     }
268
269     public static DataSchemaNode checkListAndOrderedType(final EffectiveModelContext ctx,
270             final YangInstanceIdentifier path) {
271         final YangInstanceIdentifier parent = path.getParent();
272         final DataSchemaContextNode<?> node = DataSchemaContextTree.from(ctx).findChild(parent).orElseThrow();
273         final DataSchemaNode dataSchemaNode = node.getDataSchemaNode();
274
275         if (dataSchemaNode instanceof ListSchemaNode) {
276             if (!((ListSchemaNode) dataSchemaNode).isUserOrdered()) {
277                 throw new RestconfDocumentedException("Insert parameter can be used only with ordered-by user list.",
278                         RestconfError.ErrorType.PROTOCOL, ErrorTag.BAD_ELEMENT);
279             }
280             return dataSchemaNode;
281         }
282         if (dataSchemaNode instanceof LeafListSchemaNode) {
283             if (!((LeafListSchemaNode) dataSchemaNode).isUserOrdered()) {
284                 throw new RestconfDocumentedException(
285                         "Insert parameter can be used only with ordered-by user leaf-list.",
286                         RestconfError.ErrorType.PROTOCOL, ErrorTag.BAD_ELEMENT);
287             }
288             return dataSchemaNode;
289         }
290         throw new RestconfDocumentedException("Insert parameter can be used only with list or leaf-list",
291                 RestconfError.ErrorType.PROTOCOL, ErrorTag.BAD_ELEMENT);
292     }
293 }