Skip to content

Commit 80e6eca

Browse files
authored
Create a Hello.java to parse and create <hello> messages to resolve issue with XML namespace prefixes (#50)
1 parent ae74df1 commit 80e6eca

File tree

8 files changed

+468
-35
lines changed

8 files changed

+468
-35
lines changed

.editorconfig

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# See https://editorconfig.org/
2+
[*]
3+
charset = utf-8
4+
end_of_line = lf
5+
indent_size = 4
6+
indent_style = space
7+
insert_final_newline = false
8+
max_line_length = 120
9+
tab_width = 4
10+
11+
[{*.bat,*.cmd}]
12+
end_of_line = crlf
13+
14+
[*.java]
15+
ij_java_blank_lines_after_imports = 1
16+
ij_java_blank_lines_before_imports = 1
17+
ij_java_class_count_to_use_import_on_demand = 999
18+
ij_java_imports_layout = *,|,javax.**,java.**,|,$*
19+
ij_java_layout_static_imports_separately = true
20+
ij_java_names_count_to_use_import_on_demand = 999
21+
ij_java_use_single_class_imports = true

pom.xml

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
<groupId>net.juniper.netconf</groupId>
99
<artifactId>netconf-java</artifactId>
10-
<version>2.1.1.4</version>
10+
<version>2.1.1.5</version>
1111
<packaging>jar</packaging>
1212

1313
<properties>
@@ -232,5 +232,12 @@
232232
<version>30.0-jre</version>
233233
</dependency>
234234

235+
<dependency>
236+
<groupId>org.xmlunit</groupId>
237+
<artifactId>xmlunit-assertj</artifactId>
238+
<version>2.8.2</version>
239+
<scope>test</scope>
240+
</dependency>
241+
235242
</dependencies>
236243
</project>

src/main/java/net/juniper/netconf/Device.java

Lines changed: 15 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import lombok.NonNull;
1818
import lombok.ToString;
1919
import lombok.extern.slf4j.Slf4j;
20+
import net.juniper.netconf.element.Hello;
2021
import org.w3c.dom.Document;
2122
import org.xml.sax.SAXException;
2223

@@ -28,7 +29,7 @@
2829
import java.io.InputStream;
2930
import java.io.InputStreamReader;
3031
import java.nio.charset.Charset;
31-
import java.util.ArrayList;
32+
import java.util.Arrays;
3233
import java.util.List;
3334

3435
/**
@@ -61,6 +62,13 @@ public class Device implements AutoCloseable {
6162

6263
private static final int DEFAULT_NETCONF_PORT = 830;
6364
private static final int DEFAULT_TIMEOUT = 5000;
65+
private static final List<String> DEFAULT_CLIENT_CAPABILITIES = Arrays.asList(
66+
NetconfConstants.URN_IETF_PARAMS_NETCONF_BASE_1_0,
67+
NetconfConstants.URN_IETF_PARAMS_NETCONF_BASE_1_0 + "#candidate",
68+
NetconfConstants.URN_IETF_PARAMS_NETCONF_BASE_1_0 + "#confirmed-commit",
69+
NetconfConstants.URN_IETF_PARAMS_NETCONF_BASE_1_0 + "#validate",
70+
NetconfConstants.URN_IETF_PARAMS_NETCONF_BASE_1_0 + "#url?protocol=http,ftp,file"
71+
);
6472

6573
private final JSch sshClient;
6674
private final String hostName;
@@ -148,13 +156,7 @@ public Device(JSch sshClient,
148156
* @return List of default client capabilities.
149157
*/
150158
private List<String> getDefaultClientCapabilities() {
151-
List<String> defaultCap = new ArrayList<>();
152-
defaultCap.add(NetconfConstants.URN_IETF_PARAMS_NETCONF_BASE_1_0);
153-
defaultCap.add(NetconfConstants.URN_IETF_PARAMS_NETCONF_BASE_1_0 + "#candidate");
154-
defaultCap.add(NetconfConstants.URN_IETF_PARAMS_NETCONF_BASE_1_0 + "#confirmed-commit");
155-
defaultCap.add(NetconfConstants.URN_IETF_PARAMS_NETCONF_BASE_1_0 + "#validate");
156-
defaultCap.add(NetconfConstants.URN_IETF_PARAMS_NETCONF_BASE_1_0 + "#url?protocol=http,ftp,file");
157-
return defaultCap;
159+
return DEFAULT_CLIENT_CAPABILITIES;
158160
}
159161

160162
/**
@@ -165,20 +167,11 @@ private List<String> getDefaultClientCapabilities() {
165167
* @return the hello RPC that represents those capabilities.
166168
*/
167169
private String createHelloRPC(List<String> capabilities) {
168-
StringBuilder helloRPC = new StringBuilder();
169-
helloRPC.append("<hello xmlns=\"" + NetconfConstants.URN_XML_NS_NETCONF_BASE_1_0 + "\">\n");
170-
helloRPC.append("<capabilities>\n");
171-
for (Object o : capabilities) {
172-
String capability = (String) o;
173-
helloRPC
174-
.append("<capability>")
175-
.append(capability)
176-
.append("</capability>\n");
177-
}
178-
helloRPC.append("</capabilities>\n");
179-
helloRPC.append("</hello>\n");
180-
helloRPC.append(NetconfConstants.DEVICE_PROMPT);
181-
return helloRPC.toString();
170+
return Hello.builder()
171+
.capabilities(capabilities)
172+
.build()
173+
.toXML()
174+
+ NetconfConstants.DEVICE_PROMPT;
182175
}
183176

184177
/**

src/main/java/net/juniper/netconf/NetconfSession.java

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,15 @@
1414
import com.jcraft.jsch.JSchException;
1515
import lombok.NonNull;
1616
import lombok.extern.slf4j.Slf4j;
17+
import net.juniper.netconf.element.Hello;
1718
import org.w3c.dom.Document;
1819
import org.w3c.dom.Element;
1920
import org.xml.sax.InputSource;
2021
import org.xml.sax.SAXException;
2122

2223
import javax.xml.parsers.DocumentBuilder;
24+
import javax.xml.parsers.ParserConfigurationException;
25+
import javax.xml.xpath.XPathExpressionException;
2326
import java.io.BufferedReader;
2427
import java.io.FileNotFoundException;
2528
import java.io.IOException;
@@ -59,9 +62,10 @@ public class NetconfSession {
5962

6063
private final Channel netconfChannel;
6164
private String serverCapability;
65+
private Hello serverHello;
6266

63-
private InputStream stdInStreamFromDevice;
64-
private OutputStream stdOutStreamToDevice;
67+
private final InputStream stdInStreamFromDevice;
68+
private final OutputStream stdOutStreamToDevice;
6569

6670
private String lastRpcReply;
6771
private final DocumentBuilder builder;
@@ -116,6 +120,11 @@ private void sendHello(String hello) throws IOException {
116120
String reply = getRpcReply(hello);
117121
serverCapability = reply;
118122
lastRpcReply = reply;
123+
try {
124+
serverHello = Hello.from(reply);
125+
} catch (final ParserConfigurationException | SAXException | XPathExpressionException e) {
126+
throw new NetconfException("Invalid <hello> message from server: " + reply, e);
127+
}
119128
}
120129

121130
@VisibleForTesting
@@ -128,7 +137,7 @@ String getRpcReply(String rpc) throws IOException {
128137
final long startTime = System.nanoTime();
129138
final Reader in = new InputStreamReader(stdInStreamFromDevice, Charsets.UTF_8);
130139
boolean timeoutNotExceeded = true;
131-
int promptPosition = -1;
140+
int promptPosition;
132141
while ((promptPosition = rpcReply.indexOf(NetconfConstants.DEVICE_PROMPT)) < 0 &&
133142
(timeoutNotExceeded = (TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime) < commandTimeout))) {
134143
int charsRead = in.read(buffer, 0, buffer.length);
@@ -314,6 +323,14 @@ public String getServerCapability() {
314323
return serverCapability;
315324
}
316325

326+
/**
327+
* Returns the &lt;hello&gt; message received from the server. See https://datatracker.ietf.org/doc/html/rfc6241#section-8.1
328+
* @return the &lt;hello&gt; message received from the server.
329+
*/
330+
public Hello getServerHello() {
331+
return serverHello;
332+
}
333+
317334
/**
318335
* Send an RPC(as String object) over the default Netconf session and get
319336
* the response as an XML object.
@@ -450,13 +467,7 @@ public BufferedReader executeRPCRunning(Document rpcDoc) throws IOException {
450467
* @return Session ID as a string.
451468
*/
452469
public String getSessionId() {
453-
String[] split = serverCapability.split("<session-id>");
454-
if (split.length != 2)
455-
return null;
456-
String[] idSplit = split[1].split("</session-id>");
457-
if (idSplit.length != 2)
458-
return null;
459-
return idSplit[0];
470+
return serverHello.getSessionId();
460471
}
461472

462473
/**
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
package net.juniper.netconf.element;
2+
3+
import lombok.Builder;
4+
import lombok.Data;
5+
import lombok.EqualsAndHashCode;
6+
import lombok.Singular;
7+
import lombok.ToString;
8+
import lombok.extern.slf4j.Slf4j;
9+
import net.juniper.netconf.NetconfConstants;
10+
import org.w3c.dom.Document;
11+
import org.w3c.dom.Element;
12+
import org.w3c.dom.Node;
13+
import org.w3c.dom.NodeList;
14+
import org.xml.sax.InputSource;
15+
import org.xml.sax.SAXException;
16+
17+
import javax.xml.parsers.DocumentBuilderFactory;
18+
import javax.xml.parsers.ParserConfigurationException;
19+
import javax.xml.transform.OutputKeys;
20+
import javax.xml.transform.Transformer;
21+
import javax.xml.transform.TransformerException;
22+
import javax.xml.transform.TransformerFactory;
23+
import javax.xml.transform.dom.DOMSource;
24+
import javax.xml.transform.stream.StreamResult;
25+
import javax.xml.xpath.XPath;
26+
import javax.xml.xpath.XPathConstants;
27+
import javax.xml.xpath.XPathExpressionException;
28+
import javax.xml.xpath.XPathFactory;
29+
import java.io.IOException;
30+
import java.io.StringReader;
31+
import java.io.StringWriter;
32+
import java.util.List;
33+
34+
/**
35+
* Class to represent a NETCONF hello element - https://datatracker.ietf.org/doc/html/rfc6241#section-8.1
36+
*/
37+
@Data
38+
@Slf4j
39+
public class Hello {
40+
41+
@ToString.Exclude
42+
@EqualsAndHashCode.Exclude
43+
private final Document document;
44+
45+
@ToString.Exclude
46+
private final String xml;
47+
48+
private final String sessionId;
49+
50+
@Singular("capability")
51+
private final List<String> capabilities;
52+
53+
public boolean hasCapability(final String capability) {
54+
return capabilities.contains(capability);
55+
}
56+
57+
public String toXML() {
58+
return xml;
59+
}
60+
61+
/**
62+
* Creates a Hello object based on the supplied XML.
63+
*
64+
* @param xml The XML of the NETCONF &lt;hello&gt;
65+
* @return an new, immutable, Hello object.
66+
* @throws ParserConfigurationException If the XML parser cannot be created
67+
* @throws IOException If the XML cannot be read
68+
* @throws SAXException If the XML cannot be parsed
69+
* @throws XPathExpressionException If there is a problem in the parsing expressions
70+
*/
71+
public static Hello from(final String xml)
72+
throws ParserConfigurationException, IOException, SAXException, XPathExpressionException {
73+
74+
final DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
75+
documentBuilderFactory.setNamespaceAware(true);
76+
final Document document = documentBuilderFactory.newDocumentBuilder()
77+
.parse(new InputSource(new StringReader(xml)));
78+
final XPath xPath = XPathFactory.newInstance().newXPath();
79+
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);
80+
final HelloBuilder builder = Hello.builder()
81+
.originalDocument(document)
82+
.sessionId(sessionId);
83+
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);
84+
for (int i = 0; i < capabilities.getLength(); i++) {
85+
final Node node = capabilities.item(i);
86+
builder.capability(node.getTextContent());
87+
}
88+
final Hello hello = builder.build();
89+
if (log.isInfoEnabled()) {
90+
log.info("hello is: {}", hello.toXML());
91+
}
92+
return hello;
93+
}
94+
95+
@Builder
96+
private Hello(
97+
final Document originalDocument,
98+
final String namespacePrefix,
99+
final String sessionId,
100+
@Singular("capability") final List<String> capabilities) {
101+
this.sessionId = sessionId;
102+
this.capabilities = capabilities;
103+
if (originalDocument != null) {
104+
this.document = originalDocument;
105+
} else {
106+
this.document = createDocument(namespacePrefix, sessionId, capabilities);
107+
}
108+
this.xml = createXml(document);
109+
}
110+
111+
private static Document createDocument(
112+
final String namespacePrefix,
113+
final String sessionId,
114+
final List<String> capabilities) {
115+
116+
final DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
117+
documentBuilderFactory.setNamespaceAware(true);
118+
final Document createdDocument;
119+
try {
120+
createdDocument = documentBuilderFactory.newDocumentBuilder().newDocument();
121+
} catch (final ParserConfigurationException e) {
122+
throw new IllegalStateException("Unable to create document builder", e);
123+
}
124+
125+
final Element helloElement
126+
= createdDocument.createElementNS(NetconfConstants.URN_XML_NS_NETCONF_BASE_1_0, "hello");
127+
helloElement.setPrefix(namespacePrefix);
128+
createdDocument.appendChild(helloElement);
129+
130+
final Element capabilitiesElement
131+
= createdDocument.createElementNS(NetconfConstants.URN_XML_NS_NETCONF_BASE_1_0, "capabilities");
132+
capabilitiesElement.setPrefix(namespacePrefix);
133+
capabilities.forEach(capability -> {
134+
final Element capabilityElement =
135+
createdDocument.createElementNS(NetconfConstants.URN_XML_NS_NETCONF_BASE_1_0, "capability");
136+
capabilityElement.setTextContent(capability);
137+
capabilityElement.setPrefix(namespacePrefix);
138+
capabilitiesElement.appendChild(capabilityElement);
139+
});
140+
helloElement.appendChild(capabilitiesElement);
141+
142+
if (sessionId != null) {
143+
final Element sessionIdElement
144+
= createdDocument.createElementNS(NetconfConstants.URN_XML_NS_NETCONF_BASE_1_0, "session-id");
145+
sessionIdElement.setPrefix(namespacePrefix);
146+
sessionIdElement.setTextContent(sessionId);
147+
helloElement.appendChild(sessionIdElement);
148+
}
149+
return createdDocument;
150+
}
151+
152+
private static String createXml(final Document document) {
153+
try {
154+
final TransformerFactory transformerFactory = TransformerFactory.newInstance();
155+
final Transformer transformer = transformerFactory.newTransformer();
156+
transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");
157+
final StringWriter stringWriter = new StringWriter();
158+
transformer.transform(new DOMSource(document), new StreamResult(stringWriter));
159+
return stringWriter.toString();
160+
} catch (final TransformerException e) {
161+
throw new IllegalStateException("Unable to transform document to XML", e);
162+
}
163+
}
164+
165+
}

0 commit comments

Comments
 (0)