import com.google.common.base.MoreObjects;
import com.google.common.base.MoreObjects.ToStringHelper;
-import java.io.Serial;
import java.util.HashMap;
import java.util.Map;
import javax.xml.parsers.DocumentBuilderFactory;
import org.opendaylight.yangtools.yang.common.ErrorSeverity;
import org.opendaylight.yangtools.yang.common.ErrorTag;
import org.opendaylight.yangtools.yang.common.ErrorType;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
public static final String ERROR_MESSAGE = "error-message";
public static final String ERROR_INFO = "error-info";
- @Serial
+ @java.io.Serial
private static final long serialVersionUID = 1L;
- private static final Logger LOG = LoggerFactory.getLogger(DocumentedException.class);
+
private static final DocumentBuilderFactory BUILDER_FACTORY;
static {
// FIXME: this really should be an spi/util method (or even netconf-util-w3c-dom?) as this certainly is not the
// primary interface we want to expose -- it is inherently mutable and its API is a pure nightmare.
- public Document toXMLDocument() {
- Document doc = null;
+ public final Document toXMLDocument() {
+ final Document doc;
try {
doc = BUILDER_FACTORY.newDocumentBuilder().newDocument();
+ } catch (final ParserConfigurationException e) {
+ throw new IllegalStateException("Error outputting to XML document", e);
+ }
- final var rpcReply = doc.createElementNS(NamespaceURN.BASE, RPC_REPLY_KEY);
- doc.appendChild(rpcReply);
-
- final var rpcError = doc.createElementNS(NamespaceURN.BASE, RPC_ERROR);
- rpcReply.appendChild(rpcError);
-
- rpcError.appendChild(createTextNode(doc, ERROR_TYPE, getErrorType().elementBody()));
- rpcError.appendChild(createTextNode(doc, ERROR_TAG, getErrorTag().elementBody()));
- rpcError.appendChild(createTextNode(doc, ERROR_SEVERITY, getErrorSeverity().elementBody()));
- rpcError.appendChild(createTextNode(doc, ERROR_MESSAGE, getLocalizedMessage()));
-
- final var errorInfoMap = getErrorInfo();
- if (errorInfoMap != null && !errorInfoMap.isEmpty()) {
- /*
- * <error-info> <bad-attribute>message-id</bad-attribute>
- * <bad-element>rpc</bad-element> </error-info>
- */
- final var errorInfoNode = doc.createElementNS(NamespaceURN.BASE, ERROR_INFO);
- errorInfoNode.setPrefix(rpcReply.getPrefix());
- rpcError.appendChild(errorInfoNode);
-
- for (var entry : errorInfoMap.entrySet()) {
- errorInfoNode.appendChild(createTextNode(doc, entry.getKey(), entry.getValue()));
- }
+ final var rpcReply = doc.createElementNS(NamespaceURN.BASE, RPC_REPLY_KEY);
+ doc.appendChild(rpcReply);
+
+ final var rpcError = doc.createElementNS(NamespaceURN.BASE, RPC_ERROR);
+ rpcReply.appendChild(rpcError);
+
+ rpcError.appendChild(createTextNode(doc, ERROR_TYPE, getErrorType().elementBody()));
+ rpcError.appendChild(createTextNode(doc, ERROR_TAG, getErrorTag().elementBody()));
+ rpcError.appendChild(createTextNode(doc, ERROR_SEVERITY, getErrorSeverity().elementBody()));
+ rpcError.appendChild(createTextNode(doc, ERROR_MESSAGE, getLocalizedMessage()));
+
+ final var errorInfoMap = getErrorInfo();
+ if (errorInfoMap != null && !errorInfoMap.isEmpty()) {
+ /*
+ * <error-info> <bad-attribute>message-id</bad-attribute>
+ * <bad-element>rpc</bad-element> </error-info>
+ */
+ final var errorInfoNode = doc.createElementNS(NamespaceURN.BASE, ERROR_INFO);
+ errorInfoNode.setPrefix(rpcReply.getPrefix());
+ rpcError.appendChild(errorInfoNode);
+
+ for (var entry : errorInfoMap.entrySet()) {
+ errorInfoNode.appendChild(createTextNode(doc, entry.getKey(), entry.getValue()));
}
- } catch (final ParserConfigurationException e) {
- // this shouldn't happen
- LOG.error("Error outputting to XML document", e);
}
return doc;
--- /dev/null
+/*
+ * Copyright (c) 2023 PANTHEON.tech s.r.o. 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.netconf.api.messages;
+
+import static java.util.Objects.requireNonNull;
+
+import com.google.common.collect.ImmutableMap;
+import org.eclipse.jdt.annotation.NonNull;
+import org.opendaylight.netconf.api.DocumentedException;
+import org.opendaylight.netconf.api.NamespaceURN;
+import org.opendaylight.netconf.api.xml.XmlNetconfConstants;
+import org.opendaylight.yangtools.yang.common.ErrorSeverity;
+import org.opendaylight.yangtools.yang.common.ErrorTag;
+import org.opendaylight.yangtools.yang.common.ErrorType;
+import org.w3c.dom.Document;
+
+/**
+ * A NETCONF RPC request message, as defined in
+ * <a href="https://www.rfc-editor.org/rfc/rfc6241#section-4.1">RFC6241, section 4.1</a>. Its document element is
+ * guaranteed to be an {@code <rpc/>} in the {@value NamespaceURN#BASE} namespace.
+ */
+public final class RpcMessage extends NetconfMessage {
+ private RpcMessage(final Document document) {
+ super(document);
+ }
+
+ /**
+ * Return an {@link RpcMessage} backed by specified {@link Document}.
+ *
+ * @param document Backing document
+ * @return An {@link RpcMessage}
+ * @throws DocumentedException if the document's structure does not form a valid {@code rpc} message
+ * @throws NullPointerException if {@code document} is {@code null}
+ */
+ public static @NonNull RpcMessage of(final Document document) throws DocumentedException {
+ final var root = document.getDocumentElement();
+ final var rootName = root.getLocalName();
+ if (!XmlNetconfConstants.RPC_KEY.equals(rootName)) {
+ throw new DocumentedException("Unexpected element name " + rootName, ErrorType.PROTOCOL,
+ ErrorTag.UNKNOWN_ELEMENT, ErrorSeverity.ERROR, ImmutableMap.of("bad-element", rootName));
+ }
+ final var rootNs = root.getNamespaceURI();
+ if (!NamespaceURN.BASE.equals(rootNs)) {
+ throw new DocumentedException("Unexpected element namespace " + rootNs, ErrorType.PROTOCOL,
+ ErrorTag.UNKNOWN_NAMESPACE, ErrorSeverity.ERROR, ImmutableMap.of(
+ "bad-element", rootName,
+ "bad-namespace", rootNs));
+ }
+
+ final var messageIdAttr = root.getAttributeNode(XmlNetconfConstants.MESSAGE_ID);
+ if (messageIdAttr == null) {
+ throw new DocumentedException("Missing message-id attribute", ErrorType.RPC, ErrorTag.MISSING_ATTRIBUTE,
+ ErrorSeverity.ERROR, ImmutableMap.of(
+ "bad-attribute", XmlNetconfConstants.MESSAGE_ID,
+ "bad-element", XmlNetconfConstants.RPC_KEY));
+ }
+ if (!messageIdAttr.getSpecified()) {
+ throw new IllegalArgumentException("Document element's message-id attribute is not specified");
+ }
+ return new RpcMessage(document);
+ }
+
+ /**
+ * Return an {@link RpcMessage} wrapping an RPC operation supplied as {@link Document}. The supplied document is
+ * modified to have its document element replaced with a {@code <rpc/>} element which contains it.
+ *
+ * @param document Backing operation document
+ * @return An {@link RpcMessage}
+ * @throws NullPointerException if any argument is {@code null}
+ */
+ public static @NonNull RpcMessage ofOperation(final String messageId, final Document document) {
+ final var rpcElem = document.createElementNS(NamespaceURN.BASE, XmlNetconfConstants.RPC_KEY);
+ rpcElem.appendChild(document.getDocumentElement());
+ rpcElem.setAttribute(XmlNetconfConstants.MESSAGE_ID, requireNonNull(messageId));
+ document.appendChild(rpcElem);
+ return new RpcMessage(document);
+ }
+
+ public @NonNull String messageId() {
+ return getDocument().getDocumentElement().getAttribute(XmlNetconfConstants.MESSAGE_ID);
+ }
+}
--- /dev/null
+/*
+ * Copyright (c) 2023 PANTHEON.tech s.r.o. 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.netconf.api.messages;
+
+import com.google.common.collect.ImmutableMap;
+import org.eclipse.jdt.annotation.NonNull;
+import org.eclipse.jdt.annotation.Nullable;
+import org.opendaylight.netconf.api.DocumentedException;
+import org.opendaylight.netconf.api.NamespaceURN;
+import org.opendaylight.netconf.api.xml.XmlNetconfConstants;
+import org.opendaylight.yangtools.yang.common.ErrorSeverity;
+import org.opendaylight.yangtools.yang.common.ErrorTag;
+import org.opendaylight.yangtools.yang.common.ErrorType;
+import org.w3c.dom.Document;
+
+/**
+ * A NETCONF RPC reply message, as defined in
+ * <a href="https://www.rfc-editor.org/rfc/rfc6241#section-4.2">RFC6241, section 4.2</a>. Its document element is
+ * guaranteed to be an {@code <rpc-reply/>} in the {@value NamespaceURN#BASE} namespace.
+ */
+public final class RpcReplyMessage extends NetconfMessage {
+ private RpcReplyMessage(final Document document) {
+ super(document);
+ }
+
+ /**
+ * Return an {@link RpcReplyMessage} backed by specified {@link Document}.
+ *
+ * @param document Backing document
+ * @return An {@link RpcReplyMessage}
+ * @throws DocumentedException if the document's structure does not form a valid {@code rpc-reply} message
+ * @throws NullPointerException if {@code document} is {@code null}
+ */
+ public static @NonNull RpcReplyMessage of(final Document document) throws DocumentedException {
+ final var root = document.getDocumentElement();
+ final var rootName = root.getLocalName();
+ if (!XmlNetconfConstants.RPC_REPLY_KEY.equals(rootName)) {
+ throw new DocumentedException("Unexpected element name " + rootName, ErrorType.PROTOCOL,
+ ErrorTag.UNKNOWN_ELEMENT, ErrorSeverity.ERROR, ImmutableMap.of("bad-element", rootName));
+ }
+ final var rootNs = root.getNamespaceURI();
+ if (!NamespaceURN.BASE.equals(rootNs)) {
+ throw new DocumentedException("Unexpected element namespace " + rootNs, ErrorType.PROTOCOL,
+ ErrorTag.UNKNOWN_NAMESPACE, ErrorSeverity.ERROR, ImmutableMap.of(
+ "bad-element", rootName,
+ "bad-namespace", rootNs));
+ }
+
+ return new RpcReplyMessage(document);
+ }
+
+ /**
+ * Return an {@link RpcReplyMessage} representation.
+ *
+ * @param ex DocumentedException specifying the error
+ * @return An {@link RpcReplyMessage}
+ * @throws NullPointerException if {@code ex} is {@code null}
+ */
+ public static @NonNull RpcReplyMessage of(final DocumentedException ex) {
+ return new RpcReplyMessage(ex.toXMLDocument());
+ }
+
+ public @Nullable String messageId() {
+ final var attr = getDocument().getDocumentElement().getAttributeNode(XmlNetconfConstants.MESSAGE_ID);
+ return attr == null ? null : attr.getValue();
+ }
+}
--- /dev/null
+/*
+ * Copyright (c) 2023 PANTHEON.tech s.r.o. 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.netconf.api.messages;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import com.google.common.collect.ImmutableMap;
+import org.junit.jupiter.api.Test;
+import org.opendaylight.netconf.api.DocumentedException;
+import org.opendaylight.yangtools.util.xml.UntrustedXML;
+import org.opendaylight.yangtools.yang.common.ErrorSeverity;
+import org.opendaylight.yangtools.yang.common.ErrorTag;
+import org.opendaylight.yangtools.yang.common.ErrorType;
+import org.w3c.dom.Document;
+
+class RpcMessageTest {
+ private final Document document = UntrustedXML.newDocumentBuilder().newDocument();
+
+ @Test
+ void testOf() throws Exception {
+ final var rootElement = document.createElementNS("urn:ietf:params:xml:ns:netconf:base:1.0", "rpc");
+ rootElement.setAttribute("message-id", "foo");
+ document.appendChild(rootElement);
+
+ final var msg = RpcMessage.of(document);
+ assertEquals("foo", msg.messageId());
+ assertEquals("""
+ <rpc xmlns="urn:ietf:params:xml:ns:netconf:base:1.0" message-id="foo"/>
+ """, msg.toString());
+ }
+
+ @Test
+ void testOfBadElementName() {
+ final var rootElement = document.createElementNS("urn:ietf:params:xml:ns:netconf:base:1.0", "bad");
+ rootElement.setAttribute("message-id", "foo");
+ document.appendChild(rootElement);
+
+ final var ex = assertThrows(DocumentedException.class, () -> RpcMessage.of(document));
+ assertEquals("Unexpected element name bad", ex.getMessage());
+ assertEquals(ErrorSeverity.ERROR, ex.getErrorSeverity());
+ assertEquals(ErrorType.PROTOCOL, ex.getErrorType());
+ assertEquals(ErrorTag.UNKNOWN_ELEMENT, ex.getErrorTag());
+ assertEquals(ImmutableMap.of("bad-element", "bad"), ex.getErrorInfo());
+ }
+
+ @Test
+ void testOfBadElementNs() {
+ final var rootElement = document.createElementNS("bad", "rpc");
+ rootElement.setAttribute("message-id", "foo");
+ document.appendChild(rootElement);
+
+ final var ex = assertThrows(DocumentedException.class, () -> RpcMessage.of(document));
+ assertEquals("Unexpected element namespace bad", ex.getMessage());
+ assertEquals(ErrorSeverity.ERROR, ex.getErrorSeverity());
+ assertEquals(ErrorType.PROTOCOL, ex.getErrorType());
+ assertEquals(ErrorTag.UNKNOWN_NAMESPACE, ex.getErrorTag());
+ assertEquals(ImmutableMap.of("bad-element", "rpc", "bad-namespace", "bad"), ex.getErrorInfo());
+ }
+
+ @Test
+ void testOfMissingMessageId() {
+ document.appendChild(document.createElementNS("urn:ietf:params:xml:ns:netconf:base:1.0", "rpc"));
+
+ final var ex = assertThrows(DocumentedException.class, () -> RpcMessage.of(document));
+ assertEquals("Missing message-id attribute", ex.getMessage());
+ assertEquals(ErrorSeverity.ERROR, ex.getErrorSeverity());
+ assertEquals(ErrorType.RPC, ex.getErrorType());
+ assertEquals(ErrorTag.MISSING_ATTRIBUTE, ex.getErrorTag());
+ assertEquals(ImmutableMap.of("bad-element", "rpc", "bad-attribute", "message-id"), ex.getErrorInfo());
+ }
+
+ @Test
+ void testOfOperation() {
+ final var rootElement = document.createElement("test-root");
+ document.appendChild(rootElement);
+
+ final var msg = RpcMessage.ofOperation("1014", document);
+ assertEquals("1014", msg.messageId());
+ final var msgRoot = msg.getDocument().getDocumentElement();
+ assertEquals("urn:ietf:params:xml:ns:netconf:base:1.0", msgRoot.getNamespaceURI());
+ assertEquals("rpc", msgRoot.getLocalName());
+
+ final var rootList = msgRoot.getChildNodes();
+ assertEquals(1, rootList.getLength());
+ assertSame(rootElement, rootList.item(0));
+ assertEquals("""
+ <rpc xmlns="urn:ietf:params:xml:ns:netconf:base:1.0" message-id="1014">
+ <test-root/>
+ </rpc>
+ """, msg.toString());
+ }
+}
--- /dev/null
+/*
+ * Copyright (c) 2023 PANTHEON.tech s.r.o. 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.netconf.api.messages;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import com.google.common.collect.ImmutableMap;
+import org.junit.jupiter.api.Test;
+import org.opendaylight.netconf.api.DocumentedException;
+import org.opendaylight.netconf.api.xml.XmlNetconfConstants;
+import org.opendaylight.yangtools.util.xml.UntrustedXML;
+import org.opendaylight.yangtools.yang.common.ErrorSeverity;
+import org.opendaylight.yangtools.yang.common.ErrorTag;
+import org.opendaylight.yangtools.yang.common.ErrorType;
+import org.w3c.dom.Document;
+
+class RpcReplyMessageTest {
+ private final Document document = UntrustedXML.newDocumentBuilder().newDocument();
+
+ @Test
+ void testOfDocument() throws Exception {
+ final var rootElement = document.createElementNS("urn:ietf:params:xml:ns:netconf:base:1.0", "rpc-reply");
+ rootElement.setAttribute("message-id", "foo");
+ document.appendChild(rootElement);
+
+ final var msg = RpcReplyMessage.of(document);
+ assertEquals("foo", msg.messageId());
+ assertEquals("""
+ <rpc-reply xmlns="urn:ietf:params:xml:ns:netconf:base:1.0" message-id="foo"/>
+ """, msg.toString());
+ }
+
+ @Test
+ void testOfException() throws Exception {
+ final var msg = RpcReplyMessage.of(new DocumentedException("Missing message-id attribute",
+ ErrorType.RPC, ErrorTag.MISSING_ATTRIBUTE, ErrorSeverity.ERROR, ImmutableMap.of(
+ "bad-attribute", XmlNetconfConstants.MESSAGE_ID,
+ "bad-element", XmlNetconfConstants.RPC_KEY)));
+ assertEquals("""
+ <rpc-reply xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
+ <rpc-error>
+ <error-type>rpc</error-type>
+ <error-tag>missing-attribute</error-tag>
+ <error-severity>error</error-severity>
+ <error-message>Missing message-id attribute</error-message>
+ <error-info>
+ <bad-attribute>message-id</bad-attribute>
+ <bad-element>rpc</bad-element>
+ </error-info>
+ </rpc-error>
+ </rpc-reply>
+ """, msg.toString());
+ }
+
+ @Test
+ void testOfBadElementName() {
+ final var rootElement = document.createElementNS("urn:ietf:params:xml:ns:netconf:base:1.0", "bad");
+ rootElement.setAttribute("message-id", "foo");
+ document.appendChild(rootElement);
+
+ final var ex = assertThrows(DocumentedException.class, () -> RpcReplyMessage.of(document));
+ assertEquals("Unexpected element name bad", ex.getMessage());
+ assertEquals(ErrorSeverity.ERROR, ex.getErrorSeverity());
+ assertEquals(ErrorType.PROTOCOL, ex.getErrorType());
+ assertEquals(ErrorTag.UNKNOWN_ELEMENT, ex.getErrorTag());
+ assertEquals(ImmutableMap.of("bad-element", "bad"), ex.getErrorInfo());
+ }
+
+ @Test
+ void testOfBadElementNs() {
+ final var rootElement = document.createElementNS("bad", "rpc-reply");
+ rootElement.setAttribute("message-id", "foo");
+ document.appendChild(rootElement);
+
+ final var ex = assertThrows(DocumentedException.class, () -> RpcReplyMessage.of(document));
+ assertEquals("Unexpected element namespace bad", ex.getMessage());
+ assertEquals(ErrorSeverity.ERROR, ex.getErrorSeverity());
+ assertEquals(ErrorType.PROTOCOL, ex.getErrorType());
+ assertEquals(ErrorTag.UNKNOWN_NAMESPACE, ex.getErrorTag());
+ assertEquals(ImmutableMap.of("bad-element", "rpc-reply", "bad-namespace", "bad"), ex.getErrorInfo());
+ }
+}
channelFuture.addListener(new SendErrorVerifyingListener(sendErrorException));
}
+ // FIXME: this should be handled through RpcMessage.toReply(DocumentedException)
@SuppressWarnings("checkstyle:IllegalCatch")
private static void tryToCopyAttributes(final Document incommingDocument, final Document errorDocument,
final DocumentedException sendErrorException) {