Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions .editorconfig
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
Copy link
Contributor

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?

Copy link
Contributor Author

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.

9 changes: 8 additions & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

<groupId>net.juniper.netconf</groupId>
<artifactId>netconf-java</artifactId>
<version>2.1.1.4</version>
<version>2.1.1.5</version>
<packaging>jar</packaging>

<properties>
Expand Down Expand Up @@ -232,5 +232,12 @@
<version>30.0-jre</version>
</dependency>

<dependency>
<groupId>org.xmlunit</groupId>
<artifactId>xmlunit-assertj</artifactId>
<version>2.8.2</version>
<scope>test</scope>
</dependency>

</dependencies>
</project>
37 changes: 15 additions & 22 deletions src/main/java/net/juniper/netconf/Device.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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;

/**
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}

/**
Expand All @@ -165,20 +167,11 @@ private List<String> getDefaultClientCapabilities() {
* @return the hello RPC that represents those capabilities.
*/
private String createHelloRPC(List<String> capabilities) {
Copy link
Contributor

Choose a reason for hiding this comment

The 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?

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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 ]]>]]> message separator as part of the Hello.java - as that class is solely dedicated to parsing/creating XML which the separator isn't (it's purely for NETCONF over SSH).

The remaining code,

Hello.builder()
     .capabilities(capabilities)
     .build()
     .toXML()

could be a static method of Hello.java (perhaps Hello.createXmlOnlyWithCapabilities(capabilities) ?) but am not really sure that's adding anything?

(there's another question in my mind if Device.java should be doing this, anyway? Surely it's NetconfSession that controls the details of the open session, and should therefore be in control of the shape of the <hello> that is sent. Especially if there was ever a desire to support the chunked framing mechanism in NETCONF urn:ietf:params:netconf:base:1.1 - but I think that's definitely a discussion for another day!)

Copy link
Contributor

Choose a reason for hiding this comment

The 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;
}

/**
Expand Down
31 changes: 21 additions & 10 deletions src/main/java/net/juniper/netconf/NetconfSession.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

May I ask about a pack of final words in classes? I can explain the usage of this word for input parameters and in global fields but why do we need to have it for every single variable? Especially in multi-catch sections because these variables are implicitly declared final. Moreover, there is an entity like effective final probably it'd be better to make java compiler works instead of developers, what do you think?

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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
Expand All @@ -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);
Expand Down Expand Up @@ -314,6 +323,14 @@ public String getServerCapability() {
return serverCapability;
}

/**
* Returns the &lt;hello&gt; message received from the server. See https://datatracker.ietf.org/doc/html/rfc6241#section-8.1
* @return the &lt;hello&gt; 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.
Expand Down Expand Up @@ -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();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just curious, could this be a static method in Hello.java?

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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 <hello> NETCONF element. So the session-id is an instance variable.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

}

/**
Expand Down
165 changes: 165 additions & 0 deletions src/main/java/net/juniper/netconf/element/Hello.java
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 &lt;hello&gt;
* @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);
Copy link
Contributor

Choose a reason for hiding this comment

The 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?

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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.

Copy link
Contributor

Choose a reason for hiding this comment

The 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);
Copy link
Contributor

Choose a reason for hiding this comment

The 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?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As above …

for (int i = 0; i < capabilities.getLength(); i++) {
Copy link
Contributor

Choose a reason for hiding this comment

The 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 -> {

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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");
Copy link
Contributor

Choose a reason for hiding this comment

The 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);
}
}

}
Loading