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