Rework NetconfStartExiMessage
[netconf.git] / protocol / netconf-client / src / main / java / org / opendaylight / netconf / client / NetconfClientSessionNegotiator.java
1 /*
2  * Copyright (c) 2013 Cisco Systems, Inc. and others.  All rights reserved.
3  *
4  * This program and the accompanying materials are made available under the
5  * terms of the Eclipse Public License v1.0 which accompanies this distribution,
6  * and is available at http://www.eclipse.org/legal/epl-v10.html
7  */
8 package org.opendaylight.netconf.client;
9
10 import com.google.common.base.Strings;
11 import com.google.common.collect.ImmutableSet;
12 import com.google.common.collect.Interner;
13 import com.google.common.collect.Interners;
14 import io.netty.channel.Channel;
15 import io.netty.channel.ChannelHandlerContext;
16 import io.netty.channel.ChannelInboundHandlerAdapter;
17 import io.netty.util.Timer;
18 import io.netty.util.concurrent.Promise;
19 import java.util.Set;
20 import javax.xml.xpath.XPathConstants;
21 import javax.xml.xpath.XPathExpression;
22 import org.checkerframework.checker.index.qual.NonNegative;
23 import org.eclipse.jdt.annotation.NonNull;
24 import org.opendaylight.netconf.api.NetconfDocumentedException;
25 import org.opendaylight.netconf.api.messages.HelloMessage;
26 import org.opendaylight.netconf.api.messages.NetconfMessage;
27 import org.opendaylight.netconf.api.messages.RpcMessage;
28 import org.opendaylight.netconf.api.xml.XmlNetconfConstants;
29 import org.opendaylight.netconf.api.xml.XmlUtil;
30 import org.opendaylight.netconf.nettyutil.AbstractChannelInitializer;
31 import org.opendaylight.netconf.nettyutil.AbstractNetconfSessionNegotiator;
32 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.netconf.base._1._0.rev110601.SessionIdType;
33 import org.opendaylight.yangtools.yang.common.Uint32;
34 import org.slf4j.Logger;
35 import org.slf4j.LoggerFactory;
36 import org.w3c.dom.Document;
37 import org.w3c.dom.Node;
38 import org.w3c.dom.NodeList;
39
40 // Non-final for mocking
41 class NetconfClientSessionNegotiator
42         extends AbstractNetconfSessionNegotiator<NetconfClientSession, NetconfClientSessionListener> {
43     private static final Logger LOG = LoggerFactory.getLogger(NetconfClientSessionNegotiator.class);
44
45     private static final XPathExpression SESSION_ID_X_PATH = XMLNetconfUtil
46             .compileXPath("/netconf:hello/netconf:session-id");
47
48     private static final XPathExpression SESSION_ID_X_PATH_NO_NAMESPACE = XMLNetconfUtil
49             .compileXPath("/hello/session-id");
50
51     private static final String EXI_1_0_CAPABILITY_MARKER = "exi:1.0";
52
53     private static final Interner<Set<String>> INTERNER = Interners.newWeakInterner();
54
55     private final RpcMessage startExi;
56
57     NetconfClientSessionNegotiator(final HelloMessage hello, final RpcMessage startExi,
58             final Promise<NetconfClientSession> promise, final Channel channel, final Timer timer,
59             final NetconfClientSessionListener sessionListener, final long connectionTimeoutMillis,
60             final @NonNegative int maximumIncomingChunkSize) {
61         super(hello, promise, channel, timer, sessionListener, connectionTimeoutMillis, maximumIncomingChunkSize);
62         this.startExi = startExi;
63     }
64
65     @SuppressWarnings("checkstyle:IllegalCatch")
66     @Override
67     protected void handleMessage(final HelloMessage netconfMessage) throws NetconfDocumentedException {
68         if (!ifNegotiatedAlready()) {
69             LOG.debug("Server hello message received, starting negotiation on channel {}", channel);
70             try {
71                 startNegotiation();
72             } catch (final Exception e) {
73                 LOG.warn("Unexpected negotiation failure on channel {}", channel, e);
74                 negotiationFailed(e);
75                 return;
76             }
77         }
78         final NetconfClientSession session = getSessionForHelloMessage(netconfMessage);
79         replaceHelloMessageInboundHandler(session);
80
81         // If exi should be used, try to initiate exi communication
82         // Call negotiationSuccessFul after exi negotiation is finished successfully or not
83         if (startExi != null && shouldUseExi(netconfMessage)) {
84             LOG.debug("Netconf session {} should use exi.", session);
85             tryToInitiateExi(session, startExi);
86         } else {
87             // Exi is not supported, release session immediately
88             LOG.debug("Netconf session {} isn't capable of using exi.", session);
89             negotiationSuccessful(session);
90         }
91     }
92
93     /**
94      * Initiates exi communication by sending start-exi message and waiting for positive/negative response.
95      *
96      * @param startExiMessage Exi message for initilization of exi communication.
97      */
98     void tryToInitiateExi(final NetconfClientSession session, final RpcMessage startExiMessage) {
99         channel.pipeline().addAfter(AbstractChannelInitializer.NETCONF_MESSAGE_DECODER,
100                 ExiConfirmationInboundHandler.EXI_CONFIRMED_HANDLER,
101                 new ExiConfirmationInboundHandler(session, startExiMessage));
102
103         session.sendMessage(startExiMessage).addListener(channelFuture -> {
104             if (!channelFuture.isSuccess()) {
105                 LOG.warn("Failed to send start-exi message {} on session {}", startExiMessage, session,
106                         channelFuture.cause());
107                 channel.pipeline().remove(ExiConfirmationInboundHandler.EXI_CONFIRMED_HANDLER);
108             } else {
109                 LOG.trace("Start-exi message {} sent to socket on session {}", startExiMessage, session);
110             }
111         });
112     }
113
114     private boolean shouldUseExi(final HelloMessage helloMsg) {
115         return containsExi10Capability(helloMsg.getDocument()) && containsExi10Capability(localHello().getDocument());
116     }
117
118     private static boolean containsExi10Capability(final Document doc) {
119         final NodeList nList = doc.getElementsByTagName(XmlNetconfConstants.CAPABILITY);
120         for (int i = 0; i < nList.getLength(); i++) {
121             if (nList.item(i).getTextContent().contains(EXI_1_0_CAPABILITY_MARKER)) {
122                 return true;
123             }
124         }
125         return false;
126     }
127
128     private static @NonNull SessionIdType extractSessionId(final Document doc) {
129         String textContent = getSessionIdWithXPath(doc, SESSION_ID_X_PATH);
130         if (Strings.isNullOrEmpty(textContent)) {
131             textContent = getSessionIdWithXPath(doc, SESSION_ID_X_PATH_NO_NAMESPACE);
132             if (Strings.isNullOrEmpty(textContent)) {
133                 throw new IllegalStateException("Session id not received from server, hello message: " + XmlUtil
134                         .toString(doc));
135             }
136         }
137         return new SessionIdType(Uint32.valueOf(textContent));
138     }
139
140     private static String getSessionIdWithXPath(final Document doc, final XPathExpression sessionIdXPath) {
141         final Node sessionIdNode = (Node) XmlUtil.evaluateXPath(sessionIdXPath, doc, XPathConstants.NODE);
142         return sessionIdNode != null ? sessionIdNode.getTextContent() : null;
143     }
144
145     @Override
146     protected NetconfClientSession getSession(final NetconfClientSessionListener sessionListener, final Channel channel,
147                                               final HelloMessage message) {
148         final var sessionId = extractSessionId(message.getDocument());
149
150         // Copy here is important: it disconnects the strings from the document
151         final var capabilities = INTERNER.intern(ImmutableSet.copyOf(
152             NetconfMessageUtil.extractCapabilitiesFromHello(message .getDocument())));
153
154         return new NetconfClientSession(sessionListener, channel, sessionId, capabilities);
155     }
156
157     /**
158      * Handler to process response for start-exi message.
159      */
160     private final class ExiConfirmationInboundHandler extends ChannelInboundHandlerAdapter {
161         private static final String EXI_CONFIRMED_HANDLER = "exiConfirmedHandler";
162
163         private final NetconfClientSession session;
164         private final RpcMessage startExiMessage;
165
166         ExiConfirmationInboundHandler(final NetconfClientSession session,
167                                       final RpcMessage startExiMessage) {
168             this.session = session;
169             this.startExiMessage = startExiMessage;
170         }
171
172         @SuppressWarnings("checkstyle:IllegalCatch")
173         @Override
174         public void channelRead(final ChannelHandlerContext ctx, final Object msg) {
175             ctx.pipeline().remove(ExiConfirmationInboundHandler.EXI_CONFIRMED_HANDLER);
176
177             NetconfMessage netconfMessage = (NetconfMessage) msg;
178
179             // Ok response to start-exi, try to add exi handlers
180             if (NetconfMessageUtil.isOKMessage(netconfMessage)) {
181                 LOG.trace("Positive response on start-exi call received on session {}", session);
182                 try {
183                     session.startExiCommunication(startExiMessage);
184                 } catch (RuntimeException e) {
185                     // Unable to add exi, continue without exi
186                     LOG.warn("Unable to start exi communication, Communication will continue without exi on session {}",
187                         session, e);
188                 }
189
190                 // Error response
191             } else if (NetconfMessageUtil.isErrorMessage(netconfMessage)) {
192                 LOG.warn(
193                         "Error response to start-exi message {}, Communication will continue without exi on session {}",
194                         netconfMessage, session);
195
196                 // Unexpected response to start-exi, throwing message away, continue without exi
197             } else {
198                 LOG.warn("Unexpected response to start-exi message, should be ok, was {}, "
199                         + "Communication will continue without exi "
200                         + "and response message will be thrown away on session {}",
201                         netconfMessage, session);
202             }
203
204             negotiationSuccessful(session);
205         }
206     }
207
208 }