6a55bdb9aa663c10545bb3cd5cbb4c1e1b83c1da
[netconf.git] / netconf / 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 edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
15 import io.netty.channel.Channel;
16 import io.netty.channel.ChannelHandlerContext;
17 import io.netty.channel.ChannelInboundHandlerAdapter;
18 import io.netty.util.Timer;
19 import io.netty.util.concurrent.Promise;
20 import java.util.Set;
21 import javax.xml.xpath.XPathConstants;
22 import javax.xml.xpath.XPathExpression;
23 import org.opendaylight.netconf.api.NetconfClientSessionPreferences;
24 import org.opendaylight.netconf.api.NetconfDocumentedException;
25 import org.opendaylight.netconf.api.NetconfMessage;
26 import org.opendaylight.netconf.api.messages.NetconfHelloMessage;
27 import org.opendaylight.netconf.api.xml.XmlNetconfConstants;
28 import org.opendaylight.netconf.api.xml.XmlUtil;
29 import org.opendaylight.netconf.nettyutil.AbstractChannelInitializer;
30 import org.opendaylight.netconf.nettyutil.AbstractNetconfSessionNegotiator;
31 import org.opendaylight.netconf.nettyutil.handler.exi.NetconfStartExiMessage;
32 import org.opendaylight.netconf.util.messages.NetconfMessageUtil;
33 import org.opendaylight.netconf.util.xml.XMLNetconfUtil;
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 public class NetconfClientSessionNegotiator extends
41         AbstractNetconfSessionNegotiator<NetconfClientSessionPreferences, NetconfClientSession,
42                 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     protected NetconfClientSessionNegotiator(final NetconfClientSessionPreferences sessionPreferences,
56                                              final Promise<NetconfClientSession> promise,
57                                              final Channel channel,
58                                              final Timer timer,
59                                              final NetconfClientSessionListener sessionListener,
60                                              final long connectionTimeoutMillis) {
61         super(sessionPreferences, promise, channel, timer, sessionListener, connectionTimeoutMillis);
62     }
63
64     @SuppressWarnings("checkstyle:IllegalCatch")
65     @SuppressFBWarnings(value = "BC_UNCONFIRMED_CAST",
66         justification = "SpotBugs does not understand generic cast of sessionPreferences")
67     @Override
68     protected void handleMessage(final NetconfHelloMessage netconfMessage) throws NetconfDocumentedException {
69         if (!ifNegotiatedAlready()) {
70             LOG.debug("Server hello message received, starting negotiation on channel {}", channel);
71             try {
72                 startNegotiation();
73             } catch (final Exception e) {
74                 LOG.warn("Unexpected negotiation failure on channel {}", channel, e);
75                 negotiationFailed(e);
76                 return;
77             }
78         }
79         final NetconfClientSession session = getSessionForHelloMessage(netconfMessage);
80         replaceHelloMessageInboundHandler(session);
81
82         // If exi should be used, try to initiate exi communication
83         // Call negotiationSuccessFul after exi negotiation is finished successfully or not
84         final NetconfMessage startExiMessage = sessionPreferences.getStartExiMessage();
85         if (shouldUseExi(netconfMessage) && startExiMessage instanceof NetconfStartExiMessage) {
86             LOG.debug("Netconf session {} should use exi.", session);
87             tryToInitiateExi(session, (NetconfStartExiMessage) startExiMessage);
88         } else {
89             // Exi is not supported, release session immediately
90             LOG.debug("Netconf session {} isn't capable of using exi.", session);
91             negotiationSuccessful(session);
92         }
93     }
94
95     /**
96      * Initiates exi communication by sending start-exi message and waiting for positive/negative response.
97      *
98      * @param startExiMessage Exi message for initilization of exi communication.
99      */
100     void tryToInitiateExi(final NetconfClientSession session, final NetconfStartExiMessage startExiMessage) {
101         channel.pipeline().addAfter(AbstractChannelInitializer.NETCONF_MESSAGE_DECODER,
102                 ExiConfirmationInboundHandler.EXI_CONFIRMED_HANDLER,
103                 new ExiConfirmationInboundHandler(session, startExiMessage));
104
105         session.sendMessage(startExiMessage).addListener(channelFuture -> {
106             if (!channelFuture.isSuccess()) {
107                 LOG.warn("Failed to send start-exi message {} on session {}", startExiMessage, session,
108                         channelFuture.cause());
109                 channel.pipeline().remove(ExiConfirmationInboundHandler.EXI_CONFIRMED_HANDLER);
110             } else {
111                 LOG.trace("Start-exi message {} sent to socket on session {}", startExiMessage, session);
112             }
113         });
114     }
115
116     @SuppressFBWarnings(value = "BC_UNCONFIRMED_CAST",
117         justification = "SpotBugs does not understand generic cast of sessionPreferences")
118     private boolean shouldUseExi(final NetconfHelloMessage helloMsg) {
119         return containsExi10Capability(helloMsg.getDocument())
120                 && containsExi10Capability(sessionPreferences.getHelloMessage().getDocument());
121     }
122
123     private static boolean containsExi10Capability(final Document doc) {
124         final NodeList nList = doc.getElementsByTagName(XmlNetconfConstants.CAPABILITY);
125         for (int i = 0; i < nList.getLength(); i++) {
126             if (nList.item(i).getTextContent().contains(EXI_1_0_CAPABILITY_MARKER)) {
127                 return true;
128             }
129         }
130         return false;
131     }
132
133     private static long extractSessionId(final Document doc) {
134         String textContent = getSessionIdWithXPath(doc, SESSION_ID_X_PATH);
135         if (Strings.isNullOrEmpty(textContent)) {
136             textContent = getSessionIdWithXPath(doc, SESSION_ID_X_PATH_NO_NAMESPACE);
137             if (Strings.isNullOrEmpty(textContent)) {
138                 throw new IllegalStateException("Session id not received from server, hello message: " + XmlUtil
139                         .toString(doc));
140             }
141         }
142
143         return Long.parseLong(textContent);
144     }
145
146     private static String getSessionIdWithXPath(final Document doc, final XPathExpression sessionIdXPath) {
147         final Node sessionIdNode = (Node) XmlUtil.evaluateXPath(sessionIdXPath, doc, XPathConstants.NODE);
148         return sessionIdNode != null ? sessionIdNode.getTextContent() : null;
149     }
150
151     @Override
152     protected NetconfClientSession getSession(final NetconfClientSessionListener sessionListener, final Channel channel,
153                                               final NetconfHelloMessage message) {
154         final long sessionId = extractSessionId(message.getDocument());
155
156         // Copy here is important: it disconnects the strings from the document
157         Set<String> capabilities = ImmutableSet.copyOf(NetconfMessageUtil.extractCapabilitiesFromHello(message
158                 .getDocument()));
159
160         capabilities = INTERNER.intern(capabilities);
161
162         return new NetconfClientSession(sessionListener, channel, sessionId, capabilities);
163     }
164
165     /**
166      * Handler to process response for start-exi message.
167      */
168     private final class ExiConfirmationInboundHandler extends ChannelInboundHandlerAdapter {
169         private static final String EXI_CONFIRMED_HANDLER = "exiConfirmedHandler";
170
171         private final NetconfClientSession session;
172         private final NetconfStartExiMessage startExiMessage;
173
174         ExiConfirmationInboundHandler(final NetconfClientSession session,
175                                       final NetconfStartExiMessage startExiMessage) {
176             this.session = session;
177             this.startExiMessage = startExiMessage;
178         }
179
180         @SuppressWarnings("checkstyle:IllegalCatch")
181         @Override
182         public void channelRead(final ChannelHandlerContext ctx, final Object msg) throws Exception {
183             ctx.pipeline().remove(ExiConfirmationInboundHandler.EXI_CONFIRMED_HANDLER);
184
185             NetconfMessage netconfMessage = (NetconfMessage) msg;
186
187             // Ok response to start-exi, try to add exi handlers
188             if (NetconfMessageUtil.isOKMessage(netconfMessage)) {
189                 LOG.trace("Positive response on start-exi call received on session {}", session);
190                 try {
191                     session.startExiCommunication(startExiMessage);
192                 } catch (RuntimeException e) {
193                     // Unable to add exi, continue without exi
194                     LOG.warn("Unable to start exi communication, Communication will continue without exi on session "
195                             + "{}", session, e);
196                 }
197
198                 // Error response
199             } else if (NetconfMessageUtil.isErrorMessage(netconfMessage)) {
200                 LOG.warn(
201                         "Error response to start-exi message {}, Communication will continue without exi on session {}",
202                         netconfMessage, session);
203
204                 // Unexpected response to start-exi, throwing message away, continue without exi
205             } else {
206                 LOG.warn("Unexpected response to start-exi message, should be ok, was {}, "
207                         + "Communication will continue without exi "
208                         + "and response message will be thrown away on session {}",
209                         netconfMessage, session);
210             }
211
212             negotiationSuccessful(session);
213         }
214     }
215
216 }