-
Notifications
You must be signed in to change notification settings - Fork 66
Add namespace aware parsing for <hello> #50
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| # See https://editorconfig.org/ | ||
| [*] | ||
| charset = utf-8 | ||
| end_of_line = lf | ||
| indent_size = 4 | ||
| indent_style = space | ||
| insert_final_newline = false | ||
| max_line_length = 120 | ||
| tab_width = 4 | ||
|
|
||
| [{*.bat,*.cmd}] | ||
| end_of_line = crlf | ||
|
|
||
| [*.java] | ||
| ij_java_blank_lines_after_imports = 1 | ||
| ij_java_blank_lines_before_imports = 1 | ||
| ij_java_class_count_to_use_import_on_demand = 999 | ||
| ij_java_imports_layout = *,|,javax.**,java.**,|,$* | ||
| ij_java_layout_static_imports_separately = true | ||
| ij_java_names_count_to_use_import_on_demand = 999 | ||
| ij_java_use_single_class_imports = true | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -17,6 +17,7 @@ | |
| import lombok.NonNull; | ||
| import lombok.ToString; | ||
| import lombok.extern.slf4j.Slf4j; | ||
| import net.juniper.netconf.element.Hello; | ||
| import org.w3c.dom.Document; | ||
| import org.xml.sax.SAXException; | ||
|
|
||
|
|
@@ -28,7 +29,7 @@ | |
| import java.io.InputStream; | ||
| import java.io.InputStreamReader; | ||
| import java.nio.charset.Charset; | ||
| import java.util.ArrayList; | ||
| import java.util.Arrays; | ||
| import java.util.List; | ||
|
|
||
| /** | ||
|
|
@@ -61,6 +62,13 @@ public class Device implements AutoCloseable { | |
|
|
||
| private static final int DEFAULT_NETCONF_PORT = 830; | ||
| private static final int DEFAULT_TIMEOUT = 5000; | ||
| private static final List<String> DEFAULT_CLIENT_CAPABILITIES = Arrays.asList( | ||
| NetconfConstants.URN_IETF_PARAMS_NETCONF_BASE_1_0, | ||
| NetconfConstants.URN_IETF_PARAMS_NETCONF_BASE_1_0 + "#candidate", | ||
| NetconfConstants.URN_IETF_PARAMS_NETCONF_BASE_1_0 + "#confirmed-commit", | ||
| NetconfConstants.URN_IETF_PARAMS_NETCONF_BASE_1_0 + "#validate", | ||
| NetconfConstants.URN_IETF_PARAMS_NETCONF_BASE_1_0 + "#url?protocol=http,ftp,file" | ||
| ); | ||
|
|
||
| private final JSch sshClient; | ||
| private final String hostName; | ||
|
|
@@ -148,13 +156,7 @@ public Device(JSch sshClient, | |
| * @return List of default client capabilities. | ||
| */ | ||
| private List<String> getDefaultClientCapabilities() { | ||
| List<String> defaultCap = new ArrayList<>(); | ||
| defaultCap.add(NetconfConstants.URN_IETF_PARAMS_NETCONF_BASE_1_0); | ||
| defaultCap.add(NetconfConstants.URN_IETF_PARAMS_NETCONF_BASE_1_0 + "#candidate"); | ||
| defaultCap.add(NetconfConstants.URN_IETF_PARAMS_NETCONF_BASE_1_0 + "#confirmed-commit"); | ||
| defaultCap.add(NetconfConstants.URN_IETF_PARAMS_NETCONF_BASE_1_0 + "#validate"); | ||
| defaultCap.add(NetconfConstants.URN_IETF_PARAMS_NETCONF_BASE_1_0 + "#url?protocol=http,ftp,file"); | ||
| return defaultCap; | ||
| return DEFAULT_CLIENT_CAPABILITIES; | ||
| } | ||
|
|
||
| /** | ||
|
|
@@ -165,20 +167,11 @@ private List<String> getDefaultClientCapabilities() { | |
| * @return the hello RPC that represents those capabilities. | ||
| */ | ||
| private String createHelloRPC(List<String> capabilities) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is cool. I wonder if it could have been part of the Hello.java?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It was an existing method - I just updated it to use the new Hello.java. I'd be reluctant to add the The remaining code, Hello.builder()
.capabilities(capabilities)
.build()
.toXML()could be a static method of (there's another question in my mind if
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👍 |
||
| StringBuilder helloRPC = new StringBuilder(); | ||
| helloRPC.append("<hello xmlns=\"" + NetconfConstants.URN_XML_NS_NETCONF_BASE_1_0 + "\">\n"); | ||
| helloRPC.append("<capabilities>\n"); | ||
| for (Object o : capabilities) { | ||
| String capability = (String) o; | ||
| helloRPC | ||
| .append("<capability>") | ||
| .append(capability) | ||
| .append("</capability>\n"); | ||
| } | ||
| helloRPC.append("</capabilities>\n"); | ||
| helloRPC.append("</hello>\n"); | ||
| helloRPC.append(NetconfConstants.DEVICE_PROMPT); | ||
| return helloRPC.toString(); | ||
| return Hello.builder() | ||
| .capabilities(capabilities) | ||
| .build() | ||
| .toXML() | ||
| + NetconfConstants.DEVICE_PROMPT; | ||
| } | ||
|
|
||
| /** | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -14,12 +14,15 @@ | |
| import com.jcraft.jsch.JSchException; | ||
| import lombok.NonNull; | ||
| import lombok.extern.slf4j.Slf4j; | ||
| import net.juniper.netconf.element.Hello; | ||
| import org.w3c.dom.Document; | ||
| import org.w3c.dom.Element; | ||
| import org.xml.sax.InputSource; | ||
| import org.xml.sax.SAXException; | ||
|
|
||
| import javax.xml.parsers.DocumentBuilder; | ||
| import javax.xml.parsers.ParserConfigurationException; | ||
| import javax.xml.xpath.XPathExpressionException; | ||
| import java.io.BufferedReader; | ||
| import java.io.FileNotFoundException; | ||
| import java.io.IOException; | ||
|
|
@@ -59,9 +62,10 @@ public class NetconfSession { | |
|
|
||
| private final Channel netconfChannel; | ||
| private String serverCapability; | ||
| private Hello serverHello; | ||
|
|
||
| private InputStream stdInStreamFromDevice; | ||
| private OutputStream stdOutStreamToDevice; | ||
| private final InputStream stdInStreamFromDevice; | ||
| private final OutputStream stdOutStreamToDevice; | ||
|
|
||
| private String lastRpcReply; | ||
| private final DocumentBuilder builder; | ||
|
|
@@ -116,6 +120,11 @@ private void sendHello(String hello) throws IOException { | |
| String reply = getRpcReply(hello); | ||
| serverCapability = reply; | ||
| lastRpcReply = reply; | ||
| try { | ||
| serverHello = Hello.from(reply); | ||
| } catch (final ParserConfigurationException | SAXException | XPathExpressionException e) { | ||
| throw new NetconfException("Invalid <hello> message from server: " + reply, e); | ||
| } | ||
|
Comment on lines
+123
to
+127
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. May I ask about a pack of
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Effectively final was introduced in Java 8. I've been developing in Java on and off since about 1.3, so it's become a fairly reflexive habit for me (*). I'm happy to remove it from where it's unnecessary if you think that's the right thing to do in this case. (*) Kind of scary now I've written that to realise how long I've been at this! Probably spent more time developing with Java 1.6 than anything else, but that's probably a reflection of how long Java 1.6 was "current"! |
||
| } | ||
|
|
||
| @VisibleForTesting | ||
|
|
@@ -128,7 +137,7 @@ String getRpcReply(String rpc) throws IOException { | |
| final long startTime = System.nanoTime(); | ||
| final Reader in = new InputStreamReader(stdInStreamFromDevice, Charsets.UTF_8); | ||
| boolean timeoutNotExceeded = true; | ||
| int promptPosition = -1; | ||
| int promptPosition; | ||
| while ((promptPosition = rpcReply.indexOf(NetconfConstants.DEVICE_PROMPT)) < 0 && | ||
| (timeoutNotExceeded = (TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime) < commandTimeout))) { | ||
| int charsRead = in.read(buffer, 0, buffer.length); | ||
|
|
@@ -314,6 +323,14 @@ public String getServerCapability() { | |
| return serverCapability; | ||
| } | ||
|
|
||
| /** | ||
| * Returns the <hello> message received from the server. See https://datatracker.ietf.org/doc/html/rfc6241#section-8.1 | ||
| * @return the <hello> message received from the server. | ||
| */ | ||
| public Hello getServerHello() { | ||
| return serverHello; | ||
| } | ||
|
|
||
| /** | ||
| * Send an RPC(as String object) over the default Netconf session and get | ||
| * the response as an XML object. | ||
|
|
@@ -450,13 +467,7 @@ public BufferedReader executeRPCRunning(Document rpcDoc) throws IOException { | |
| * @return Session ID as a string. | ||
| */ | ||
| public String getSessionId() { | ||
| String[] split = serverCapability.split("<session-id>"); | ||
| if (split.length != 2) | ||
| return null; | ||
| String[] idSplit = split[1].split("</session-id>"); | ||
| if (idSplit.length != 2) | ||
| return null; | ||
| return idSplit[0]; | ||
| return serverHello.getSessionId(); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. just curious, could this be a static method in Hello.java?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This couldn't be, no. An instance of the Hello object represents a single
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👍 |
||
| } | ||
|
|
||
| /** | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,165 @@ | ||
| package net.juniper.netconf.element; | ||
|
|
||
| import lombok.Builder; | ||
| import lombok.Data; | ||
| import lombok.EqualsAndHashCode; | ||
| import lombok.Singular; | ||
| import lombok.ToString; | ||
| import lombok.extern.slf4j.Slf4j; | ||
| import net.juniper.netconf.NetconfConstants; | ||
| import org.w3c.dom.Document; | ||
| import org.w3c.dom.Element; | ||
| import org.w3c.dom.Node; | ||
| import org.w3c.dom.NodeList; | ||
| import org.xml.sax.InputSource; | ||
| import org.xml.sax.SAXException; | ||
|
|
||
| import javax.xml.parsers.DocumentBuilderFactory; | ||
| import javax.xml.parsers.ParserConfigurationException; | ||
| import javax.xml.transform.OutputKeys; | ||
| import javax.xml.transform.Transformer; | ||
| import javax.xml.transform.TransformerException; | ||
| import javax.xml.transform.TransformerFactory; | ||
| import javax.xml.transform.dom.DOMSource; | ||
| import javax.xml.transform.stream.StreamResult; | ||
| import javax.xml.xpath.XPath; | ||
| import javax.xml.xpath.XPathConstants; | ||
| import javax.xml.xpath.XPathExpressionException; | ||
| import javax.xml.xpath.XPathFactory; | ||
| import java.io.IOException; | ||
| import java.io.StringReader; | ||
| import java.io.StringWriter; | ||
| import java.util.List; | ||
|
|
||
| /** | ||
| * Class to represent a NETCONF hello element - https://datatracker.ietf.org/doc/html/rfc6241#section-8.1 | ||
| */ | ||
| @Data | ||
| @Slf4j | ||
| public class Hello { | ||
|
|
||
| @ToString.Exclude | ||
| @EqualsAndHashCode.Exclude | ||
| private final Document document; | ||
|
|
||
| @ToString.Exclude | ||
| private final String xml; | ||
|
|
||
| private final String sessionId; | ||
|
|
||
| @Singular("capability") | ||
| private final List<String> capabilities; | ||
|
|
||
| public boolean hasCapability(final String capability) { | ||
| return capabilities.contains(capability); | ||
| } | ||
|
|
||
| public String toXML() { | ||
| return xml; | ||
| } | ||
|
|
||
| /** | ||
| * Creates a Hello object based on the supplied XML. | ||
| * | ||
| * @param xml The XML of the NETCONF <hello> | ||
| * @return an new, immutable, Hello object. | ||
| * @throws ParserConfigurationException If the XML parser cannot be created | ||
| * @throws IOException If the XML cannot be read | ||
| * @throws SAXException If the XML cannot be parsed | ||
| * @throws XPathExpressionException If there is a problem in the parsing expressions | ||
| */ | ||
| public static Hello from(final String xml) | ||
| throws ParserConfigurationException, IOException, SAXException, XPathExpressionException { | ||
|
|
||
| final DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance(); | ||
| documentBuilderFactory.setNamespaceAware(true); | ||
| final Document document = documentBuilderFactory.newDocumentBuilder() | ||
| .parse(new InputSource(new StringReader(xml))); | ||
| final XPath xPath = XPathFactory.newInstance().newXPath(); | ||
| final String sessionId = xPath.evaluate("/*[namespace-uri()='urn:ietf:params:xml:ns:netconf:base:1.0' and local-name()='hello']/*[namespace-uri()='urn:ietf:params:xml:ns:netconf:base:1.0' and local-name()='session-id']", document); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. that is a super long string. Would it make sense to add this to the NetconfConstants.java?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There’s certainly some refactoring that could be done here. However, I plan to do it with the implementation. My current thinking is that both Hello and RpcReply will inherit from AbstractNetconfElement which will contain various utility methods that can be shared between them. So I’d like to put it off for now, if that’s OK.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👍 |
||
| final HelloBuilder builder = Hello.builder() | ||
| .originalDocument(document) | ||
| .sessionId(sessionId); | ||
| final NodeList capabilities = (NodeList) xPath.evaluate("/*[namespace-uri()='urn:ietf:params:xml:ns:netconf:base:1.0' and local-name()='hello']/*[namespace-uri()='urn:ietf:params:xml:ns:netconf:base:1.0' and local-name()='capabilities']/*[namespace-uri()='urn:ietf:params:xml:ns:netconf:base:1.0' and local-name()='capability']", document, XPathConstants.NODESET); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. that is a super long string. Would it make sense to add this to the NetconfConstants.java?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As above … |
||
| for (int i = 0; i < capabilities.getLength(); i++) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit, this is fine.. I would have been tempted to do something like capabilites.foreach(capability -> {
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Unfortunately the NodeList doesn’t implement iterable so it has to be an old-style ‘for’ statement. Though it does highlight that there is a lot of reliance on old-style, pretty much deprecated javax packages. But I think changing the public API of the library is beyond the scope of this PR! |
||
| final Node node = capabilities.item(i); | ||
| builder.capability(node.getTextContent()); | ||
| } | ||
| final Hello hello = builder.build(); | ||
| if (log.isInfoEnabled()) { | ||
| log.info("hello is: {}", hello.toXML()); | ||
| } | ||
| return hello; | ||
| } | ||
|
|
||
| @Builder | ||
| private Hello( | ||
| final Document originalDocument, | ||
| final String namespacePrefix, | ||
| final String sessionId, | ||
| @Singular("capability") final List<String> capabilities) { | ||
| this.sessionId = sessionId; | ||
| this.capabilities = capabilities; | ||
| if (originalDocument != null) { | ||
| this.document = originalDocument; | ||
| } else { | ||
| this.document = createDocument(namespacePrefix, sessionId, capabilities); | ||
| } | ||
| this.xml = createXml(document); | ||
| } | ||
|
|
||
| private static Document createDocument( | ||
| final String namespacePrefix, | ||
| final String sessionId, | ||
| final List<String> capabilities) { | ||
|
|
||
| final DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance(); | ||
| documentBuilderFactory.setNamespaceAware(true); | ||
| final Document createdDocument; | ||
| try { | ||
| createdDocument = documentBuilderFactory.newDocumentBuilder().newDocument(); | ||
| } catch (final ParserConfigurationException e) { | ||
| throw new IllegalStateException("Unable to create document builder", e); | ||
| } | ||
|
|
||
| final Element helloElement | ||
| = createdDocument.createElementNS(NetconfConstants.URN_XML_NS_NETCONF_BASE_1_0, "hello"); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When I see four lines that are very similar, I wonder if it could be turned into a private function. it looks like the difference is the name space name? So if you could pass in a Document and name space name and it would return an Element? I'd also be tempted to make the name space name an enum and use that instead of the string. |
||
| helloElement.setPrefix(namespacePrefix); | ||
| createdDocument.appendChild(helloElement); | ||
|
|
||
| final Element capabilitiesElement | ||
| = createdDocument.createElementNS(NetconfConstants.URN_XML_NS_NETCONF_BASE_1_0, "capabilities"); | ||
| capabilitiesElement.setPrefix(namespacePrefix); | ||
| capabilities.forEach(capability -> { | ||
| final Element capabilityElement = | ||
| createdDocument.createElementNS(NetconfConstants.URN_XML_NS_NETCONF_BASE_1_0, "capability"); | ||
| capabilityElement.setTextContent(capability); | ||
| capabilityElement.setPrefix(namespacePrefix); | ||
| capabilitiesElement.appendChild(capabilityElement); | ||
| }); | ||
| helloElement.appendChild(capabilitiesElement); | ||
|
|
||
| if (sessionId != null) { | ||
| final Element sessionIdElement | ||
| = createdDocument.createElementNS(NetconfConstants.URN_XML_NS_NETCONF_BASE_1_0, "session-id"); | ||
| sessionIdElement.setPrefix(namespacePrefix); | ||
| sessionIdElement.setTextContent(sessionId); | ||
| helloElement.appendChild(sessionIdElement); | ||
| } | ||
| return createdDocument; | ||
| } | ||
|
|
||
| private static String createXml(final Document document) { | ||
| try { | ||
| final TransformerFactory transformerFactory = TransformerFactory.newInstance(); | ||
| final Transformer transformer = transformerFactory.newTransformer(); | ||
| transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes"); | ||
| final StringWriter stringWriter = new StringWriter(); | ||
| transformer.transform(new DOMSource(document), new StreamResult(stringWriter)); | ||
| return stringWriter.toString(); | ||
| } catch (final TransformerException e) { | ||
| throw new IllegalStateException("Unable to transform document to XML", e); | ||
| } | ||
| } | ||
|
|
||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
are you sure that this file should be in the remote repo?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That was a deliberate choice by me; an earlier comment from @peterjhill highlighted that I had inadvertently shuffled around the order of import statements. This file should stop that happening for in the future for users of IntelliJ at least by configuring it to use the defaults used in the project.
The file also defines standard editorconfig settings for things like line-endings, tab width etc., also in line with the project. These should work with most, if not all IDEs, and should make it easier for contributors to maintain the project style.
I'm happy to remove it if the consensus is it is unnecessary.