Bump MRI upstreams
[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().getIdentifier().getNodeType().getLocalName();
79
80         if (path.isEmpty()) {
81             if (!payload.getData().getIdentifier().getNodeType().equals(
82                 RestconfDataServiceConstant.NETCONF_BASE_QNAME)) {
83                 throw new RestconfDocumentedException("Instance identifier has to contain at least one path argument",
84                         ErrorType.PROTOCOL, ErrorTag.MALFORMED_MESSAGE);
85             }
86         } else {
87             final String identifierName = path.getLastPathArgument().getNodeType().getLocalName();
88             if (!payloadName.equals(identifierName)) {
89                 throw new RestconfDocumentedException(
90                         "Payload name (" + payloadName + ") is different from identifier name (" + identifierName + ")",
91                         ErrorType.PROTOCOL, ErrorTag.MALFORMED_MESSAGE);
92             }
93         }
94     }
95
96     /**
97      * Validates whether keys in {@code payload} are equal to values of keys in
98      * {@code iiWithData} for list schema node.
99      *
100      * @throws RestconfDocumentedException if key values or key count in payload and URI isn't equal
101      */
102     public static void validateListKeysEqualityInPayloadAndUri(final NormalizedNodeContext payload) {
103         final InstanceIdentifierContext<?> iiWithData = payload.getInstanceIdentifierContext();
104         final PathArgument lastPathArgument = iiWithData.getInstanceIdentifier().getLastPathArgument();
105         final SchemaNode schemaNode = iiWithData.getSchemaNode();
106         final NormalizedNode data = payload.getData();
107         if (schemaNode instanceof ListSchemaNode) {
108             final List<QName> keyDefinitions = ((ListSchemaNode) schemaNode).getKeyDefinition();
109             if (lastPathArgument instanceof NodeIdentifierWithPredicates && data instanceof MapEntryNode) {
110                 final Map<QName, Object> uriKeyValues = ((NodeIdentifierWithPredicates) lastPathArgument).asMap();
111                 isEqualUriAndPayloadKeyValues(uriKeyValues, (MapEntryNode) data, keyDefinitions);
112             }
113         }
114     }
115
116     private static void isEqualUriAndPayloadKeyValues(final Map<QName, Object> uriKeyValues, final MapEntryNode payload,
117                                                       final List<QName> keyDefinitions) {
118         final Map<QName, Object> mutableCopyUriKeyValues = new HashMap<>(uriKeyValues);
119         for (final QName keyDefinition : keyDefinitions) {
120             final Object uriKeyValue = RestconfDocumentedException.throwIfNull(
121                     mutableCopyUriKeyValues.remove(keyDefinition), ErrorType.PROTOCOL, ErrorTag.DATA_MISSING,
122                     "Missing key %s in URI.", keyDefinition);
123
124             final Object dataKeyValue = payload.getIdentifier().getValue(keyDefinition);
125
126             if (!uriKeyValue.equals(dataKeyValue)) {
127                 final String errMsg = "The value '" + uriKeyValue + "' for key '" + keyDefinition.getLocalName()
128                         + "' specified in the URI doesn't match the value '" + dataKeyValue
129                         + "' specified in the message body. ";
130                 throw new RestconfDocumentedException(errMsg, ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE);
131             }
132         }
133     }
134
135     /**
136      * Check mount point and prepare variables for put data to DS. Close {@link DOMTransactionChain} if any
137      * inside of object {@link RestconfStrategy} provided as a parameter if any.
138      *
139      * @param payload       data to put
140      * @param schemaContext reference to {@link EffectiveModelContext}
141      * @param strategy      object that perform the actual DS operations
142      * @param point         query parameter
143      * @param insert        query parameter
144      * @return {@link Response}
145      */
146     public static Response putData(final NormalizedNodeContext payload, final EffectiveModelContext schemaContext,
147                                    final RestconfStrategy strategy, final Insert insert, final String point) {
148         final YangInstanceIdentifier path = payload.getInstanceIdentifierContext().getInstanceIdentifier();
149
150         final FluentFuture<Boolean> existsFuture = strategy.exists(LogicalDatastoreType.CONFIGURATION, path);
151         final FutureDataFactory<Boolean> existsResponse = new FutureDataFactory<>();
152         FutureCallbackTx.addCallback(existsFuture, PUT_TX_TYPE, existsResponse);
153
154         final ResponseFactory responseFactory =
155             new ResponseFactory(existsResponse.result ? Status.NO_CONTENT : Status.CREATED);
156         final FluentFuture<? extends CommitInfo> submitData = submitData(path, schemaContext, strategy,
157             payload.getData(), insert, point);
158         //This method will close transactionChain if any
159         FutureCallbackTx.addCallback(submitData, PUT_TX_TYPE, responseFactory, strategy, path);
160         return responseFactory.build();
161     }
162
163     /**
164      * Put data to DS.
165      *
166      * @param path          path of data
167      * @param schemaContext {@link SchemaContext}
168      * @param strategy      object that perform the actual DS operations
169      * @param data          data
170      * @param point         query parameter
171      * @param insert        query parameter
172      * @return {@link FluentFuture}
173      */
174     private static FluentFuture<? extends CommitInfo> submitData(final YangInstanceIdentifier path,
175                                                                  final EffectiveModelContext schemaContext,
176                                                                  final RestconfStrategy strategy,
177                                                                  final NormalizedNode data,
178                                                                  final Insert insert, final String point) {
179         final RestconfTransaction transaction = strategy.prepareWriteExecution();
180         if (insert == null) {
181             return makePut(path, schemaContext, transaction, data);
182         }
183
184         checkListAndOrderedType(schemaContext, path);
185         final NormalizedNode readData;
186         switch (insert) {
187             case FIRST:
188                 readData = readList(strategy, path.getParent());
189                 if (readData == null || ((NormalizedNodeContainer<?>) readData).isEmpty()) {
190                     return makePut(path, schemaContext, transaction, data);
191                 }
192                 transaction.remove(LogicalDatastoreType.CONFIGURATION, path.getParent());
193                 transaction.replace(LogicalDatastoreType.CONFIGURATION, path, data, schemaContext);
194                 transaction.replace(LogicalDatastoreType.CONFIGURATION, path.getParent(), readData, schemaContext);
195                 return transaction.commit();
196             case LAST:
197                 return makePut(path, schemaContext, transaction, data);
198             case BEFORE:
199                 readData = readList(strategy, path.getParent());
200                 if (readData == null || ((NormalizedNodeContainer<?>) readData).isEmpty()) {
201                     return makePut(path, schemaContext, transaction, data);
202                 }
203                 insertWithPointPut(transaction, path, data, schemaContext, point, (NormalizedNodeContainer<?>) readData,
204                     true);
205                 return transaction.commit();
206             case AFTER:
207                 readData = readList(strategy, path.getParent());
208                 if (readData == null || ((NormalizedNodeContainer<?>) readData).isEmpty()) {
209                     return makePut(path, schemaContext, transaction, data);
210                 }
211                 insertWithPointPut(transaction, path, data, schemaContext, point, (NormalizedNodeContainer<?>) readData,
212                     false);
213                 return transaction.commit();
214             default:
215                 throw new RestconfDocumentedException(
216                         "Used bad value of insert parameter. Possible values are first, last, before or after, "
217                                 + "but was: " + insert, RestconfError.ErrorType.PROTOCOL, ErrorTag.BAD_ATTRIBUTE);
218         }
219     }
220
221     // FIXME: this method is only called from a context where we are modifying data. This should be part of strategy,
222     //        requiring an already-open transaction. It also must return a future, so it can be properly composed.
223     static NormalizedNode readList(final RestconfStrategy strategy, final YangInstanceIdentifier path) {
224         return ReadDataTransactionUtil.readDataViaTransaction(strategy, LogicalDatastoreType.CONFIGURATION, path,
225             false);
226     }
227
228     private static void insertWithPointPut(final RestconfTransaction transaction,
229                                            final YangInstanceIdentifier path,
230                                            final NormalizedNode data,
231                                            final EffectiveModelContext schemaContext, final String point,
232                                            final NormalizedNodeContainer<?> readList, 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.body()) {
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.body()) {
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 }