diff --git a/yangkit-data-xml-codec/src/main/java/org/yangcentral/yangkit/data/codec/xml/YangDataDocumentXmlCodec.java b/yangkit-data-xml-codec/src/main/java/org/yangcentral/yangkit/data/codec/xml/YangDataDocumentXmlCodec.java index 6116b0b0..8f64cd50 100644 --- a/yangkit-data-xml-codec/src/main/java/org/yangcentral/yangkit/data/codec/xml/YangDataDocumentXmlCodec.java +++ b/yangkit-data-xml-codec/src/main/java/org/yangcentral/yangkit/data/codec/xml/YangDataDocumentXmlCodec.java @@ -7,12 +7,15 @@ import org.yangcentral.yangkit.data.api.codec.AnydataValidationOptions; import org.yangcentral.yangkit.data.api.codec.YangDataDocumentCodec; import org.yangcentral.yangkit.data.api.exception.YangDataException; +import org.yangcentral.yangkit.data.api.model.LeafData; +import org.yangcentral.yangkit.data.api.model.ListData; import org.yangcentral.yangkit.data.api.model.YangData; import org.yangcentral.yangkit.data.api.model.YangDataContainer; import org.yangcentral.yangkit.data.api.model.YangDataDocument; import org.yangcentral.yangkit.data.impl.model.YangDataDocumentImpl; import org.yangcentral.yangkit.data.impl.util.YangDataUtil; import org.yangcentral.yangkit.model.api.schema.YangSchemaContext; +import org.yangcentral.yangkit.model.api.stmt.Leaf; import org.yangcentral.yangkit.model.api.stmt.SchemaNode; import org.yangcentral.yangkit.model.api.stmt.SchemaNodeContainer; import org.yangcentral.yangkit.utils.xml.Converter; @@ -74,6 +77,10 @@ protected ValidatorResult buildChildrenData( continue; // Skip non-config data } + if (addExistingListKey(yangDataContainer, sonSchemaNode, child, validatorResultBuilder)) { + continue; + } + YangDataXmlCodec xmlCodec = YangDataXmlCodec.getInstance(sonSchemaNode, resolver, child.getUniquePath()); if (xmlCodec != null) { @@ -87,13 +94,7 @@ protected ValidatorResult buildChildrenData( buildChildrenData((YangDataContainer) addedChild, child, resolver)); } } catch (YangDataException e) { - ValidatorRecordBuilder recordBuilder = - new ValidatorRecordBuilder<>(); - recordBuilder.setErrorTag(e.getErrorTag()); - recordBuilder.setErrorPath(child.getUniquePath()); - recordBuilder.setBadElement(child); - recordBuilder.setErrorMessage(e.getErrorMsg()); - validatorResultBuilder.addRecord(recordBuilder.build()); + recordAddChildFailure(e, child, validatorResultBuilder); } } } @@ -101,6 +102,39 @@ protected ValidatorResult buildChildrenData( return validatorResultBuilder.build(); } + private boolean addExistingListKey(YangDataContainer parent, SchemaNode schemaNode, + Element child, ValidatorResultBuilder validatorResultBuilder) { + if (!(parent instanceof ListData) || !(schemaNode instanceof Leaf) + || !((Leaf) schemaNode).isKey()) { + return false; + } + ListData listData = (ListData) parent; + for (LeafData key : listData.getKeys()) { + if (!key.getQName().equals(schemaNode.getIdentifier())) { + continue; + } + if (listData.getDataChild(key.getIdentifier()) == null) { + try { + listData.addDataChild(key); + } catch (YangDataException e) { + recordAddChildFailure(e, child, validatorResultBuilder); + } + } + return true; + } + return false; + } + + private void recordAddChildFailure(YangDataException exception, Element child, + ValidatorResultBuilder validatorResultBuilder) { + ValidatorRecordBuilder recordBuilder = new ValidatorRecordBuilder<>(); + recordBuilder.setErrorTag(exception.getErrorTag()); + recordBuilder.setErrorPath(child.getUniquePath()); + recordBuilder.setBadElement(child); + recordBuilder.setErrorMessage(exception.getErrorMsg()); + validatorResultBuilder.addRecord(recordBuilder.build()); + } + @Override public YangDataDocument deserialize(Element root, ValidatorResultBuilder validatorResultBuilder) { return deserialize(root, validatorResultBuilder, (AnydataValidationContextResolver) null); diff --git a/yangkit-data-xml-codec/src/test/java/org/yangcentral/yangkit/data/codec/xml/test/XmlCodecBasicTest.java b/yangkit-data-xml-codec/src/test/java/org/yangcentral/yangkit/data/codec/xml/test/XmlCodecBasicTest.java index 7e48dbad..3a6ebba8 100644 --- a/yangkit-data-xml-codec/src/test/java/org/yangcentral/yangkit/data/codec/xml/test/XmlCodecBasicTest.java +++ b/yangkit-data-xml-codec/src/test/java/org/yangcentral/yangkit/data/codec/xml/test/XmlCodecBasicTest.java @@ -6,9 +6,13 @@ import org.junit.jupiter.api.Test; import org.yangcentral.yangkit.common.api.AbsolutePath; import org.yangcentral.yangkit.common.api.QName; +import org.yangcentral.yangkit.common.api.exception.ErrorMessage; +import org.yangcentral.yangkit.common.api.exception.ErrorTag; import org.yangcentral.yangkit.common.api.validate.ValidatorResult; import org.yangcentral.yangkit.common.api.validate.ValidatorResultBuilder; +import org.yangcentral.yangkit.data.api.exception.YangDataException; import org.yangcentral.yangkit.data.api.model.LeafData; +import org.yangcentral.yangkit.data.api.model.ListData; import org.yangcentral.yangkit.data.api.model.YangData; import org.yangcentral.yangkit.data.api.model.YangDataContainer; import org.yangcentral.yangkit.data.api.model.YangDataDocument; @@ -21,6 +25,8 @@ import java.net.URI; import java.net.URL; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Proxy; import static org.junit.jupiter.api.Assertions.*; @@ -106,6 +112,88 @@ public void testDeserializationRecursivelyBuildsContainerAndListChildren() throw assertEquals("active", onlyLeafValue(settings, "mode", namespace)); } + @Test + public void testParsedListKeysAreTheSameChildrenAsIdentifierKeys() throws Exception { + URL yangUrl = getClass().getClassLoader().getResource("yang/test-basic.yang"); + assertNotNull(yangUrl); + YangSchemaContext schemaContext = YangYinParser.parse(yangUrl.getFile()); + assertTrue(schemaContext.validate().isOk()); + String namespace = "urn:test:basic"; + Document xml = DocumentHelper.parseText( + "" + + "onefirst" + + "twosecond" + + ""); + ValidatorResultBuilder builder = new ValidatorResultBuilder(); + YangDataDocument document = new YangDataDocumentXmlCodec(schemaContext).deserialize(xml, builder); + + assertTrue(builder.build().isOk()); + java.util.List> servers = onlyContainerChild(document, "config", namespace) + .getDataChildren("server", namespace); + assertEquals(2, servers.size()); + for (int i = 0; i < servers.size(); i++) { + ListData server = (ListData) servers.get(i); + assertEquals(1, server.getKeys().size()); + LeafData key = server.getKeys().get(0); + assertSame(key, server.getDataChild(key.getIdentifier())); + assertEquals(1, server.getDataChildren("id", namespace).size()); + assertEquals(i == 0 ? "one" : "two", key.getStringValue()); + assertEquals(i == 0 ? "first" : "second", + onlyLeafValue(server, "description", namespace)); + } + } + + @Test + public void testFailedListKeyInsertionRecordsValidationAndContinues() throws Exception { + URL yangUrl = getClass().getClassLoader().getResource("yang/test-basic.yang"); + assertNotNull(yangUrl); + YangSchemaContext schemaContext = YangYinParser.parse(yangUrl.getFile()); + assertTrue(schemaContext.validate().isOk()); + String namespace = "urn:test:basic"; + Element serverXml = DocumentHelper.parseText( + "one" + + "first").getRootElement(); + org.yangcentral.yangkit.model.api.stmt.Module module = schemaContext.getModules().stream() + .filter(m -> "test-basic".equals(m.getArgStr())).findFirst().orElseThrow(AssertionError::new); + org.yangcentral.yangkit.model.api.stmt.SchemaNode configSchema = module.getTreeNodeChild( + new QName(namespace, "config")); + org.yangcentral.yangkit.model.api.stmt.SchemaNode serverSchema = + ((org.yangcentral.yangkit.model.api.stmt.SchemaNodeContainer) configSchema) + .getTreeNodeChild(new QName(namespace, "server")); + ListData realList = (ListData) org.yangcentral.yangkit.data.codec.xml.YangDataXmlCodec + .getInstance(serverSchema).deserialize(serverXml, new ValidatorResultBuilder()); + assertNotNull(realList); + ListData rejectingList = (ListData) Proxy.newProxyInstance(ListData.class.getClassLoader(), + new Class[]{ListData.class}, (proxy, method, args) -> { + if ("addDataChild".equals(method.getName()) + && args != null && args[0] instanceof LeafData + && "id".equals(((LeafData) args[0]).getQName().getLocalName())) { + throw new YangDataException(ErrorTag.BAD_ELEMENT, null, + new ErrorMessage("cannot add list key")); + } + try { + return method.invoke(realList, args); + } catch (InvocationTargetException e) { + throw e.getCause(); + } + }); + class ExposedDocumentCodec extends YangDataDocumentXmlCodec { + ExposedDocumentCodec() { + super(schemaContext); + } + ValidatorResult buildForList(ListData list, Element element) { + return buildChildrenData(list, element); + } + } + ValidatorResult result = new ExposedDocumentCodec().buildForList(rejectingList, serverXml); + assertFalse(result.isOk()); + assertEquals(1, result.getRecords().size()); + assertEquals(ErrorTag.BAD_ELEMENT, result.getRecords().get(0).getErrorTag()); + assertEquals(serverXml.elements().get(0).getUniquePath(), result.getRecords().get(0).getErrorPath()); + assertEquals("cannot add list key", result.getRecords().get(0).getErrorMsg().getMessage()); + assertEquals("first", onlyLeafValue(realList, "description", namespace)); + } + private static YangDataContainer onlyContainerChild( YangDataContainer parent, String name, String namespace) { java.util.List> children = parent.getDataChildren(name, namespace);