import org.opendaylight.yangtools.yang.data.util.AnyXmlNodeDataWithSchema;
import org.opendaylight.yangtools.yang.data.util.AnydataNodeDataWithSchema;
import org.opendaylight.yangtools.yang.data.util.CompositeNodeDataWithSchema;
+import org.opendaylight.yangtools.yang.data.util.CompositeNodeDataWithSchema.ChildReusePolicy;
import org.opendaylight.yangtools.yang.data.util.ContainerNodeDataWithSchema;
import org.opendaylight.yangtools.yang.data.util.LeafListEntryNodeDataWithSchema;
import org.opendaylight.yangtools.yang.data.util.LeafListNodeDataWithSchema;
switch (in.nextTag()) {
case XMLStreamConstants.START_ELEMENT:
- // FIXME: why do we even need this tracker? either document it or remove it
+ // FIXME: 6.0.0: why do we even need this tracker? either document it or remove it.
+ // it looks like it is a crude duplicate finder, which should really be handled via
+ // ChildReusePolicy.REJECT
final Set<Entry<String, String>> namesakes = new HashSet<>();
while (in.hasNext()) {
final String xmlElementName = in.getLocalName();
}
final String elementNS = in.getNamespaceURI();
- if (!namesakes.add(new SimpleImmutableEntry<>(elementNS, xmlElementName))) {
- throw new XMLStreamException(String.format(
- "Duplicate namespace \"%s\" element \"%s\" in XML input", elementNS, xmlElementName),
- in.getLocation());
- }
+ final boolean added = namesakes.add(new SimpleImmutableEntry<>(elementNS, xmlElementName));
final URI nsUri;
try {
final Deque<DataSchemaNode> childDataSchemaNodes =
ParserStreamUtils.findSchemaNodeByNameAndNamespace(parentSchema, xmlElementName, nsUri);
if (!childDataSchemaNodes.isEmpty()) {
+ final boolean elementList = isElementList(childDataSchemaNodes);
+ if (!added && !elementList) {
+ throw new XMLStreamException(String.format(
+ "Duplicate element \"%s\" in namespace \"%s\" with parent \"%s\" in XML input",
+ xmlElementName, elementNS, parent.getSchema()), in.getLocation());
+ }
+
// We have a match, proceed with it
- read(in, ((CompositeNodeDataWithSchema<?>) parent).addChild(childDataSchemaNodes), rootElement);
+ read(in, ((CompositeNodeDataWithSchema<?>) parent).addChild(childDataSchemaNodes,
+ elementList ? ChildReusePolicy.REUSE : ChildReusePolicy.NOOP), rootElement);
continue;
}
}
}
+ // Return true if schema represents a construct which uses multiple sibling elements to represent its content. The
+ // siblings MAY be interleaved as per RFC7950.
+ private static boolean isElementList(final Deque<DataSchemaNode> childDataSchemaNodes) {
+ final DataSchemaNode last = childDataSchemaNodes.getLast();
+ return last instanceof ListSchemaNode || last instanceof LeafListSchemaNode;
+ }
+
private static void addMountPointChild(final MountPointData mount, final URI namespace, final String localName,
final DOMSource source) {
final DOMSourceMountPointChild child = new DOMSourceMountPointChild(source);
xmlParser.parse(reader);
fail("XMLStreamException should have been thrown because of duplicate leaf.");
} catch (XMLStreamException ex) {
- assertThat(ex.getMessage(), containsString("Duplicate namespace \"foo-namespace\" element "
- + "\"decimal64-leaf\" in XML input"));
+ assertThat(ex.getMessage(), containsString("Duplicate element \"decimal64-leaf\" in namespace"
+ + " \"foo-namespace\" with parent \"container leaf-container\" in XML input"));
}
}
xmlParser.parse(reader);
fail("XMLStreamException should have been thrown because of duplicate anyxml");
} catch (XMLStreamException ex) {
- assertThat(ex.getMessage(), containsString("Duplicate namespace \"foo-namespace\" element \"my-anyxml\""));
+ assertThat(ex.getMessage(), containsString("Duplicate element \"my-anyxml\" in namespace"
+ + " \"foo-namespace\" with parent \"container anyxml-container\" in XML input"));
}
}
xmlParser.parse(reader);
fail("XMLStreamException should have been thrown because of duplicate container");
} catch (XMLStreamException ex) {
- assertThat(ex.getMessage(), containsString("Duplicate namespace \"foo-namespace\" element "
- + "\"leaf-container\" in XML input"));
+ assertThat(ex.getMessage(), containsString("Duplicate element \"leaf-container\" in namespace"
+ + " \"foo-namespace\" with parent \"container parent-container\" in XML input"));
}
}
--- /dev/null
+/*
+ * Copyright (c) 2020 PANTHEON.tech, s.r.o. and others. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.yangtools.yang.data.codec.xml;
+
+import static org.junit.Assert.assertEquals;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URISyntaxException;
+import java.util.Arrays;
+import javax.xml.stream.XMLStreamException;
+import javax.xml.stream.XMLStreamReader;
+import org.junit.Test;
+import org.opendaylight.yangtools.util.xml.UntrustedXML;
+import org.opendaylight.yangtools.yang.common.QName;
+import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifier;
+import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifierWithPredicates;
+import org.opendaylight.yangtools.yang.data.api.schema.stream.NormalizedNodeStreamWriter;
+import org.opendaylight.yangtools.yang.data.impl.schema.Builders;
+import org.opendaylight.yangtools.yang.data.impl.schema.ImmutableNodes;
+import org.opendaylight.yangtools.yang.data.impl.schema.ImmutableNormalizedNodeStreamWriter;
+import org.opendaylight.yangtools.yang.data.impl.schema.NormalizedNodeResult;
+import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext;
+import org.opendaylight.yangtools.yang.model.util.SchemaContextUtil;
+import org.opendaylight.yangtools.yang.test.util.YangParserTestUtils;
+import org.xml.sax.SAXException;
+
+public class YT1107Test {
+ private static final QName PARENT = QName.create("yt1107", "parent");
+ private static final QName ADMIN = QName.create(PARENT, "admin");
+ private static final QName NAME = QName.create(PARENT, "name");
+ private static final QName USER = QName.create(PARENT, "user");
+
+ @Test
+ public void testInterleavingLists() throws XMLStreamException, URISyntaxException, IOException, SAXException {
+ final EffectiveModelContext schemaContext = YangParserTestUtils.parseYangResource("/yt1107/yt1107.yang");
+ final InputStream resourceAsStream = XmlToNormalizedNodesTest.class.getResourceAsStream("/yt1107/yt1107.xml");
+ final XMLStreamReader reader = UntrustedXML.createXMLStreamReader(resourceAsStream);
+
+ final NormalizedNodeResult result = new NormalizedNodeResult();
+ final NormalizedNodeStreamWriter streamWriter = ImmutableNormalizedNodeStreamWriter.from(result);
+ final XmlParserStream xmlParser = XmlParserStream.create(streamWriter, schemaContext,
+ SchemaContextUtil.findNodeInSchemaContext(schemaContext, Arrays.asList(PARENT)));
+ xmlParser.parse(reader);
+
+ assertEquals(Builders.containerBuilder()
+ .withNodeIdentifier(new NodeIdentifier(PARENT))
+ .withChild(Builders.mapBuilder()
+ .withNodeIdentifier(new NodeIdentifier(ADMIN))
+ .withChild(Builders.mapEntryBuilder()
+ .withNodeIdentifier(NodeIdentifierWithPredicates.of(ADMIN, NAME, "John"))
+ .withChild(ImmutableNodes.leafNode(NAME, "John"))
+ .build())
+ .build())
+ .withChild(Builders.mapBuilder()
+ .withNodeIdentifier(new NodeIdentifier(USER))
+ .withChild(Builders.mapEntryBuilder()
+ .withNodeIdentifier(NodeIdentifierWithPredicates.of(USER, NAME, "Freud"))
+ .withChild(ImmutableNodes.leafNode(NAME, "Freud"))
+ .build())
+ .withChild(Builders.mapEntryBuilder()
+ .withNodeIdentifier(NodeIdentifierWithPredicates.of(USER, NAME, "Bob"))
+ .withChild(ImmutableNodes.leafNode(NAME, "Bob"))
+ .build())
+ .build())
+ .build(), result.getResult());
+ }
+}
--- /dev/null
+<parent xmlns="yt1107">
+ <user>
+ <name>Freud</name>
+ </user>
+ <admin>
+ <name>John</name>
+ </admin>
+ <user>
+ <name>Bob</name>
+ </user>
+</parent>
--- /dev/null
+module yt1107 {
+ namespace "yt1107";
+ prefix "yt1107";
+ container parent {
+ config true;
+ list user {
+ key name;
+ leaf name {
+ type string;
+ }
+ }
+ list admin {
+ key name;
+ leaf name {
+ type string;
+ }
+ }
+ }
+}
// FIXME: 6.0.0: this should be impossible to hit
@Override
- CaseNodeDataWithSchema addCompositeChild(final DataSchemaNode schema) {
+ CaseNodeDataWithSchema addCompositeChild(final DataSchemaNode schema, final ChildReusePolicy policy) {
verify(schema instanceof CaseSchemaNode, "Unexpected schema %s", schema);
- return addCompositeChild((CaseSchemaNode) schema);
+ return addCompositeChild((CaseSchemaNode) schema, policy);
}
- CaseNodeDataWithSchema addCompositeChild(final CaseSchemaNode schema) {
+ CaseNodeDataWithSchema addCompositeChild(final CaseSchemaNode schema, final ChildReusePolicy policy) {
CaseNodeDataWithSchema newChild = new CaseNodeDataWithSchema(schema);
caseNodeDataWithSchema = newChild;
- addCompositeChild(newChild);
+ addCompositeChild(newChild, policy);
return newChild;
}
import static com.google.common.base.Preconditions.checkArgument;
+import com.google.common.annotations.Beta;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.Multimap;
import java.io.IOException;
import java.util.Deque;
import java.util.List;
import java.util.Map.Entry;
+import org.eclipse.jdt.annotation.Nullable;
import org.opendaylight.yangtools.odlext.model.api.YangModeledAnyxmlSchemaNode;
import org.opendaylight.yangtools.rfc7952.data.api.StreamWriterMetadataExtension;
import org.opendaylight.yangtools.yang.data.api.schema.stream.NormalizedNodeStreamWriter;
* Represents a node which is composed of multiple simpler nodes.
*/
public class CompositeNodeDataWithSchema<T extends DataSchemaNode> extends AbstractNodeDataWithSchema<T> {
+ /**
+ * Policy on how child nodes should be treated when an attempt is made to add them multiple times.
+ */
+ @Beta
+ public enum ChildReusePolicy {
+ /**
+ * Do not consider any existing nodes at all, just perform a straight append. Multiple occurrences of a child
+ * will result in multiple children being emitted. This is almost certainly the wrong policy unless the caller
+ * prevents such a situation from arising via some different mechanism.
+ */
+ NOOP,
+ /**
+ * Do not allow duplicate definition of a child node. This would typically be used when a child cannot be
+ * encountered multiple times, but the caller does not make any provision to detect such a conflict. If a child
+ * node would end up being defined a second time, {@link DuplicateChildNodeRejectedException} is reported.
+ */
+ REJECT {
+ @Override
+ AbstractNodeDataWithSchema<?> appendChild(final Collection<AbstractNodeDataWithSchema<?>> view,
+ final AbstractNodeDataWithSchema<?> newChild) {
+ final DataSchemaNode childSchema = newChild.getSchema();
+ final AbstractNodeDataWithSchema<?> existing = findExistingChild(view, childSchema);
+ if (existing != null) {
+ throw new DuplicateChildNodeRejectedException("Duplicate child " + childSchema.getQName());
+ }
+ return super.appendChild(view, newChild);
+ }
+ },
+ /**
+ * Reuse previously-defined child node. This is most appropriate when a child may be visited multiple times
+ * and the intent is to append content of each visit. A typical usage is list elements with RFC7950 XML
+ * encoding, where there is no encapsulating element and hence list entries may be interleaved with other
+ * children.
+ */
+ REUSE {
+ @Override
+ AbstractNodeDataWithSchema<?> appendChild(final Collection<AbstractNodeDataWithSchema<?>> view,
+ final AbstractNodeDataWithSchema<?> newChild) {
+ final AbstractNodeDataWithSchema<?> existing = findExistingChild(view, newChild.getSchema());
+ return existing != null ? existing : super.appendChild(view, newChild);
+ }
+ };
+
+ AbstractNodeDataWithSchema<?> appendChild(final Collection<AbstractNodeDataWithSchema<?>> view,
+ final AbstractNodeDataWithSchema<?> newChild) {
+ view.add(newChild);
+ return newChild;
+ }
+
+ static @Nullable AbstractNodeDataWithSchema<?> findExistingChild(
+ final Collection<AbstractNodeDataWithSchema<?>> view, final DataSchemaNode childSchema) {
+ for (AbstractNodeDataWithSchema<?> existing : view) {
+ if (childSchema.equals(existing.getSchema())) {
+ return existing;
+ }
+ }
+ return null;
+ }
+ }
+
/**
* nodes which were added to schema via augmentation and are present in data input.
*/
super(schema);
}
- private AbstractNodeDataWithSchema<?> addChild(final DataSchemaNode schema) {
- AbstractNodeDataWithSchema<?> newChild = addSimpleChild(schema);
- return newChild == null ? addCompositeChild(schema) : newChild;
- }
-
@Deprecated
public void addChild(final AbstractNodeDataWithSchema<?> newChild) {
children.add(newChild);
}
+ @Deprecated
public AbstractNodeDataWithSchema<?> addChild(final Deque<DataSchemaNode> schemas) {
+ return addChild(schemas, ChildReusePolicy.NOOP);
+ }
+
+ public AbstractNodeDataWithSchema<?> addChild(final Deque<DataSchemaNode> schemas, final ChildReusePolicy policy) {
checkArgument(!schemas.isEmpty(), "Expecting at least one schema");
// Pop the first node...
final DataSchemaNode schema = schemas.pop();
if (schemas.isEmpty()) {
// Simple, direct node
- return addChild(schema);
+ return addChild(schema, policy);
}
// The choice/case mess, reuse what we already popped
if (caseNodeDataWithSchema == null) {
ChoiceNodeDataWithSchema choiceNodeDataWithSchema = new ChoiceNodeDataWithSchema(choiceNode);
childNodes.add(choiceNodeDataWithSchema);
- caseNodeDataWithSchema = choiceNodeDataWithSchema.addCompositeChild(caseNode);
+ caseNodeDataWithSchema = choiceNodeDataWithSchema.addCompositeChild(caseNode, ChildReusePolicy.NOOP);
}
- return caseNodeDataWithSchema.addChild(schemas);
+ return caseNodeDataWithSchema.addChild(schemas, policy);
}
- private AbstractNodeDataWithSchema<?> addSimpleChild(final DataSchemaNode schema) {
+ private AbstractNodeDataWithSchema<?> addChild(final DataSchemaNode schema, final ChildReusePolicy policy) {
+ AbstractNodeDataWithSchema<?> newChild = addSimpleChild(schema, policy);
+ return newChild == null ? addCompositeChild(schema, policy) : newChild;
+ }
+
+ private AbstractNodeDataWithSchema<?> addSimpleChild(final DataSchemaNode schema, final ChildReusePolicy policy) {
final SimpleNodeDataWithSchema<?> newChild;
if (schema instanceof LeafSchemaNode) {
newChild = new LeafNodeDataWithSchema((LeafSchemaNode) schema);
} else {
augSchema = null;
}
+
+ // FIXME: 6.0.0: use policy once we have removed addChild() visibility
+
if (augSchema != null) {
augmentationsToChild.put(augSchema, newChild);
} else {
return null;
}
- AbstractNodeDataWithSchema<?> addCompositeChild(final DataSchemaNode schema) {
+ AbstractNodeDataWithSchema<?> addCompositeChild(final DataSchemaNode schema, final ChildReusePolicy policy) {
final CompositeNodeDataWithSchema<?> newChild;
if (schema instanceof ListSchemaNode) {
newChild = new CompositeNodeDataWithSchema<>(schema);
}
- addCompositeChild(newChild);
- return newChild;
+ return addCompositeChild(newChild, policy);
}
- final void addCompositeChild(final CompositeNodeDataWithSchema<?> newChild) {
+ final AbstractNodeDataWithSchema<?> addCompositeChild(final CompositeNodeDataWithSchema<?> newChild,
+ final ChildReusePolicy policy) {
final AugmentationSchemaNode augSchema = findCorrespondingAugment(getSchema(), newChild.getSchema());
- if (augSchema != null) {
- augmentationsToChild.put(augSchema, newChild);
- } else {
- addChild(newChild);
- }
+ final Collection<AbstractNodeDataWithSchema<?>> view = augSchema == null ? children
+ : augmentationsToChild.get(augSchema);
+
+ return policy.appendChild(view, newChild);
}
/**
--- /dev/null
+/*
+ * Copyright (c) 2020 PANTHEON.tech, s.r.o. and others. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.yangtools.yang.data.util;
+
+import com.google.common.annotations.Beta;
+
+@Beta
+public class DuplicateChildNodeRejectedException extends IllegalStateException {
+ private static final long serialVersionUID = 1L;
+
+ public DuplicateChildNodeRejectedException(final String message) {
+ super(message);
+ }
+}