Allow list elements to be interleaved
[yangtools.git] / yang / yang-data-util / src / main / java / org / opendaylight / yangtools / yang / data / util / CompositeNodeDataWithSchema.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.yangtools.yang.data.util;
9
10 import static com.google.common.base.Preconditions.checkArgument;
11
12 import com.google.common.annotations.Beta;
13 import com.google.common.collect.ArrayListMultimap;
14 import com.google.common.collect.Multimap;
15 import java.io.IOException;
16 import java.util.ArrayList;
17 import java.util.Collection;
18 import java.util.Deque;
19 import java.util.List;
20 import java.util.Map.Entry;
21 import org.eclipse.jdt.annotation.Nullable;
22 import org.opendaylight.yangtools.odlext.model.api.YangModeledAnyxmlSchemaNode;
23 import org.opendaylight.yangtools.rfc7952.data.api.StreamWriterMetadataExtension;
24 import org.opendaylight.yangtools.yang.data.api.schema.stream.NormalizedNodeStreamWriter;
25 import org.opendaylight.yangtools.yang.model.api.AnydataSchemaNode;
26 import org.opendaylight.yangtools.yang.model.api.AnyxmlSchemaNode;
27 import org.opendaylight.yangtools.yang.model.api.AugmentationSchemaNode;
28 import org.opendaylight.yangtools.yang.model.api.AugmentationTarget;
29 import org.opendaylight.yangtools.yang.model.api.CaseSchemaNode;
30 import org.opendaylight.yangtools.yang.model.api.ChoiceSchemaNode;
31 import org.opendaylight.yangtools.yang.model.api.ContainerSchemaNode;
32 import org.opendaylight.yangtools.yang.model.api.DataSchemaNode;
33 import org.opendaylight.yangtools.yang.model.api.LeafListSchemaNode;
34 import org.opendaylight.yangtools.yang.model.api.LeafSchemaNode;
35 import org.opendaylight.yangtools.yang.model.api.ListSchemaNode;
36
37 /**
38  * Utility class used for tracking parser state as needed by a StAX-like parser.
39  * This class is to be used only by respective XML and JSON parsers in yang-data-codec-xml and yang-data-codec-gson.
40  *
41  * <p>
42  * Represents a node which is composed of multiple simpler nodes.
43  */
44 public class CompositeNodeDataWithSchema<T extends DataSchemaNode> extends AbstractNodeDataWithSchema<T> {
45     /**
46      * Policy on how child nodes should be treated when an attempt is made to add them multiple times.
47      */
48     @Beta
49     public enum ChildReusePolicy {
50         /**
51          * Do not consider any existing nodes at all, just perform a straight append. Multiple occurrences of a child
52          * will result in multiple children being emitted. This is almost certainly the wrong policy unless the caller
53          * prevents such a situation from arising via some different mechanism.
54          */
55         NOOP,
56         /**
57          * Do not allow duplicate definition of a child node. This would typically be used when a child cannot be
58          * encountered multiple times, but the caller does not make any provision to detect such a conflict. If a child
59          * node would end up being defined a second time, {@link DuplicateChildNodeRejectedException} is reported.
60          */
61         REJECT {
62             @Override
63             AbstractNodeDataWithSchema<?> appendChild(final Collection<AbstractNodeDataWithSchema<?>> view,
64                     final AbstractNodeDataWithSchema<?> newChild) {
65                 final DataSchemaNode childSchema = newChild.getSchema();
66                 final AbstractNodeDataWithSchema<?> existing = findExistingChild(view, childSchema);
67                 if (existing != null) {
68                     throw new DuplicateChildNodeRejectedException("Duplicate child " + childSchema.getQName());
69                 }
70                 return super.appendChild(view, newChild);
71             }
72         },
73         /**
74          * Reuse previously-defined child node. This is most appropriate when a child may be visited multiple times
75          * and the intent is to append content of each visit. A typical usage is list elements with RFC7950 XML
76          * encoding, where there is no encapsulating element and hence list entries may be interleaved with other
77          * children.
78          */
79         REUSE {
80             @Override
81             AbstractNodeDataWithSchema<?> appendChild(final Collection<AbstractNodeDataWithSchema<?>> view,
82                     final AbstractNodeDataWithSchema<?> newChild) {
83                 final AbstractNodeDataWithSchema<?> existing = findExistingChild(view, newChild.getSchema());
84                 return existing != null ? existing : super.appendChild(view, newChild);
85             }
86         };
87
88         AbstractNodeDataWithSchema<?> appendChild(final Collection<AbstractNodeDataWithSchema<?>> view,
89                 final AbstractNodeDataWithSchema<?> newChild) {
90             view.add(newChild);
91             return newChild;
92         }
93
94         static @Nullable AbstractNodeDataWithSchema<?> findExistingChild(
95                 final Collection<AbstractNodeDataWithSchema<?>> view, final DataSchemaNode childSchema) {
96             for (AbstractNodeDataWithSchema<?> existing : view) {
97                 if (childSchema.equals(existing.getSchema())) {
98                     return existing;
99                 }
100             }
101             return null;
102         }
103     }
104
105     /**
106      * nodes which were added to schema via augmentation and are present in data input.
107      */
108     private final Multimap<AugmentationSchemaNode, AbstractNodeDataWithSchema<?>> augmentationsToChild =
109         ArrayListMultimap.create();
110
111     /**
112      * remaining data nodes (which aren't added via augment). Every of one them should have the same QName.
113      */
114     private final List<AbstractNodeDataWithSchema<?>> children = new ArrayList<>();
115
116     public CompositeNodeDataWithSchema(final T schema) {
117         super(schema);
118     }
119
120     @Deprecated
121     public void addChild(final AbstractNodeDataWithSchema<?> newChild) {
122         children.add(newChild);
123     }
124
125     @Deprecated
126     public AbstractNodeDataWithSchema<?> addChild(final Deque<DataSchemaNode> schemas) {
127         return addChild(schemas, ChildReusePolicy.NOOP);
128     }
129
130     public AbstractNodeDataWithSchema<?> addChild(final Deque<DataSchemaNode> schemas, final ChildReusePolicy policy) {
131         checkArgument(!schemas.isEmpty(), "Expecting at least one schema");
132
133         // Pop the first node...
134         final DataSchemaNode schema = schemas.pop();
135         if (schemas.isEmpty()) {
136             // Simple, direct node
137             return addChild(schema, policy);
138         }
139
140         // The choice/case mess, reuse what we already popped
141         final DataSchemaNode choiceCandidate = schema;
142         checkArgument(choiceCandidate instanceof ChoiceSchemaNode, "Expected node of type ChoiceNode but was %s",
143             choiceCandidate.getClass());
144         final ChoiceSchemaNode choiceNode = (ChoiceSchemaNode) choiceCandidate;
145
146         final DataSchemaNode caseCandidate = schemas.pop();
147         checkArgument(caseCandidate instanceof CaseSchemaNode, "Expected node of type ChoiceCaseNode but was %s",
148             caseCandidate.getClass());
149         final CaseSchemaNode caseNode = (CaseSchemaNode) caseCandidate;
150
151         final AugmentationSchemaNode augSchema;
152         if (choiceCandidate.isAugmenting()) {
153             augSchema = findCorrespondingAugment(getSchema(), choiceCandidate);
154         } else {
155             augSchema = null;
156         }
157
158         // looking for existing choice
159         final Collection<AbstractNodeDataWithSchema<?>> childNodes;
160         if (augSchema != null) {
161             childNodes = augmentationsToChild.get(augSchema);
162         } else {
163             childNodes = children;
164         }
165
166         CompositeNodeDataWithSchema<?> caseNodeDataWithSchema = findChoice(childNodes, choiceCandidate, caseCandidate);
167         if (caseNodeDataWithSchema == null) {
168             ChoiceNodeDataWithSchema choiceNodeDataWithSchema = new ChoiceNodeDataWithSchema(choiceNode);
169             childNodes.add(choiceNodeDataWithSchema);
170             caseNodeDataWithSchema = choiceNodeDataWithSchema.addCompositeChild(caseNode, ChildReusePolicy.NOOP);
171         }
172
173         return caseNodeDataWithSchema.addChild(schemas, policy);
174     }
175
176     private AbstractNodeDataWithSchema<?> addChild(final DataSchemaNode schema, final ChildReusePolicy policy) {
177         AbstractNodeDataWithSchema<?> newChild = addSimpleChild(schema, policy);
178         return newChild == null ? addCompositeChild(schema, policy) : newChild;
179     }
180
181     private AbstractNodeDataWithSchema<?> addSimpleChild(final DataSchemaNode schema, final ChildReusePolicy policy) {
182         final SimpleNodeDataWithSchema<?> newChild;
183         if (schema instanceof LeafSchemaNode) {
184             newChild = new LeafNodeDataWithSchema((LeafSchemaNode) schema);
185         } else if (schema instanceof AnyxmlSchemaNode) {
186             // YangModeledAnyxmlSchemaNode is handled by addCompositeChild method.
187             if (schema instanceof YangModeledAnyxmlSchemaNode) {
188                 return null;
189             }
190             newChild = new AnyXmlNodeDataWithSchema((AnyxmlSchemaNode) schema);
191         } else if (schema instanceof AnydataSchemaNode) {
192             newChild = new AnydataNodeDataWithSchema((AnydataSchemaNode) schema);
193         } else {
194             return null;
195         }
196
197         final AugmentationSchemaNode augSchema;
198         if (schema.isAugmenting()) {
199             augSchema = findCorrespondingAugment(getSchema(), schema);
200         } else {
201             augSchema = null;
202         }
203
204         // FIXME: 6.0.0: use policy once we have removed addChild() visibility
205
206         if (augSchema != null) {
207             augmentationsToChild.put(augSchema, newChild);
208         } else {
209             addChild(newChild);
210         }
211         return newChild;
212     }
213
214     private static CaseNodeDataWithSchema findChoice(final Collection<AbstractNodeDataWithSchema<?>> childNodes,
215             final DataSchemaNode choiceCandidate, final DataSchemaNode caseCandidate) {
216         if (childNodes != null) {
217             for (AbstractNodeDataWithSchema<?> nodeDataWithSchema : childNodes) {
218                 if (nodeDataWithSchema instanceof ChoiceNodeDataWithSchema
219                         && nodeDataWithSchema.getSchema().getQName().equals(choiceCandidate.getQName())) {
220                     CaseNodeDataWithSchema casePrevious = ((ChoiceNodeDataWithSchema) nodeDataWithSchema).getCase();
221
222                     checkArgument(casePrevious.getSchema().getQName().equals(caseCandidate.getQName()),
223                         "Data from case %s are specified but other data from case %s were specified earlier."
224                         + " Data aren't from the same case.", caseCandidate.getQName(),
225                         casePrevious.getSchema().getQName());
226
227                     return casePrevious;
228                 }
229             }
230         }
231         return null;
232     }
233
234     AbstractNodeDataWithSchema<?> addCompositeChild(final DataSchemaNode schema, final ChildReusePolicy policy) {
235         final CompositeNodeDataWithSchema<?> newChild;
236
237         if (schema instanceof ListSchemaNode) {
238             newChild = new ListNodeDataWithSchema((ListSchemaNode) schema);
239         } else if (schema instanceof LeafListSchemaNode) {
240             newChild = new LeafListNodeDataWithSchema((LeafListSchemaNode) schema);
241         } else if (schema instanceof ContainerSchemaNode) {
242             newChild = new ContainerNodeDataWithSchema((ContainerSchemaNode) schema);
243         } else if (schema instanceof YangModeledAnyxmlSchemaNode) {
244             newChild = new YangModeledAnyXmlNodeDataWithSchema((YangModeledAnyxmlSchemaNode)schema);
245         } else {
246             newChild = new CompositeNodeDataWithSchema<>(schema);
247         }
248
249         return addCompositeChild(newChild, policy);
250     }
251
252     final AbstractNodeDataWithSchema<?> addCompositeChild(final CompositeNodeDataWithSchema<?> newChild,
253             final ChildReusePolicy policy) {
254         final AugmentationSchemaNode augSchema = findCorrespondingAugment(getSchema(), newChild.getSchema());
255         final Collection<AbstractNodeDataWithSchema<?>> view = augSchema == null ? children
256                 : augmentationsToChild.get(augSchema);
257
258         return policy.appendChild(view, newChild);
259     }
260
261     /**
262      * Return a hint about how may children we are going to generate.
263      * @return Size of currently-present node list.
264      */
265     protected final int childSizeHint() {
266         return children.size();
267     }
268
269     @Override
270     public void write(final NormalizedNodeStreamWriter writer, final StreamWriterMetadataExtension metaWriter)
271             throws IOException {
272         for (AbstractNodeDataWithSchema<?> child : children) {
273             child.write(writer, metaWriter);
274         }
275         for (Entry<AugmentationSchemaNode, Collection<AbstractNodeDataWithSchema<?>>> augmentationToChild
276                 : augmentationsToChild.asMap().entrySet()) {
277             final Collection<AbstractNodeDataWithSchema<?>> childsFromAgumentation = augmentationToChild.getValue();
278             if (!childsFromAgumentation.isEmpty()) {
279                 // FIXME: can we get the augmentation schema?
280                 writer.startAugmentationNode(DataSchemaContextNode.augmentationIdentifierFrom(
281                     augmentationToChild.getKey()));
282
283                 for (AbstractNodeDataWithSchema<?> nodeDataWithSchema : childsFromAgumentation) {
284                     nodeDataWithSchema.write(writer, metaWriter);
285                 }
286
287                 writer.endNode();
288             }
289         }
290     }
291
292     /**
293      * Tries to find in {@code parent} which is dealed as augmentation target node with QName as {@code child}. If such
294      * node is found then it is returned, else null.
295      *
296      * @param parent parent node
297      * @param child child node
298      * @return augmentation schema
299      */
300     private static AugmentationSchemaNode findCorrespondingAugment(final DataSchemaNode parent,
301             final DataSchemaNode child) {
302         if (parent instanceof AugmentationTarget && !(parent instanceof ChoiceSchemaNode)) {
303             for (AugmentationSchemaNode augmentation : ((AugmentationTarget) parent).getAvailableAugmentations()) {
304                 DataSchemaNode childInAugmentation = augmentation.getDataChildByName(child.getQName());
305                 if (childInAugmentation != null) {
306                     return augmentation;
307                 }
308             }
309         }
310         return null;
311     }
312 }