]> git.basschouten.com Git - openhab-addons.git/blob
45253e931cfcec9f5c1ddfc8720384bad246d055
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
7  * This program and the accompanying materials are made available under the
8  * terms of the Eclipse Public License 2.0 which is available at
9  * http://www.eclipse.org/legal/epl-2.0
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.networkupstools.internal.nut;
14
15 import java.io.BufferedReader;
16 import java.io.IOException;
17 import java.io.InputStreamReader;
18 import java.io.PrintWriter;
19 import java.net.InetSocketAddress;
20 import java.net.Socket;
21 import java.nio.charset.StandardCharsets;
22
23 import org.eclipse.jdt.annotation.NonNullByDefault;
24 import org.eclipse.jdt.annotation.Nullable;
25 import org.slf4j.Logger;
26 import org.slf4j.LoggerFactory;
27
28 /**
29  * Connector class manages the socket connection to the NUT server.
30  *
31  * @author Hilbrand Bouwkamp - Initial contribution
32  */
33 @NonNullByDefault
34 class NutConnector {
35
36     private static final String USERNAME = "USERNAME %s";
37     private static final String PASSWORD = "PASSWORD %s";
38     private static final String OK = "OK";
39     private static final String ERR = "ERR";
40     private static final int TIME_OUT_MILLISECONDS = 10_000;
41     private static final int MAX_RETRIES = 3;
42
43     private final Logger logger = LoggerFactory.getLogger(NutConnector.class);
44
45     private final String login;
46     private final String password;
47     private final InetSocketAddress inetSocketAddress;
48     private @Nullable Socket socket;
49     private @Nullable BufferedReader reader;
50     private @Nullable PrintWriter writer;
51
52     /**
53      * Constructor.
54      *
55      * @param host host
56      * @param port port
57      * @param username username
58      * @param password password
59      */
60     NutConnector(final String host, final int port, final String username, final String password) {
61         this.login = username.isEmpty() ? "" : String.format(USERNAME, username);
62         this.password = password.isEmpty() ? "" : String.format(PASSWORD, password);
63         inetSocketAddress = new InetSocketAddress(host, port);
64     }
65
66     /**
67      * Communicates to read the data to the NUT server. It handles the connection and authentication. Sends the command
68      * to the NUT server and passes the reading of the values to the readFunction argument.
69      *
70      * @param <R> The type of the returned data
71      * @param command The command to send to the NUT server
72      * @param readFunction Function called to handle the lines read from the NUT server
73      * @return the data read from the NUT server
74      * @throws NutException Exception thrown related to the NUT server connection and/or data.
75      */
76     public synchronized <R> R read(final String command,
77             final NutFunction<NutSupplier<String>, @Nullable R> readFunction) throws NutException {
78         int retry = 0;
79
80         while (true) {
81             try {
82                 connectIfClosed();
83                 final PrintWriter localWriter = writer;
84                 final BufferedReader localReader = reader;
85
86                 if (localWriter == null) {
87                     throw new NutException("Writer closed.");
88                 } else {
89                     localWriter.println(command);
90                 }
91                 if (localReader == null) {
92                     throw new NutException("Reader closed.");
93                 } else {
94                     return readFunction.apply(() -> readLine(localReader));
95                 }
96             } catch (final NutException | RuntimeException e) {
97                 retry++;
98                 close();
99                 if (retry < MAX_RETRIES) {
100                     logger.debug("Error during command retry {}:", retry, e);
101                 } else {
102                     throw e;
103                 }
104             }
105         }
106     }
107
108     /**
109      * Opens a Socket connection if there is no connection or if the connection is closed. Authenticates if
110      * username/password is provided.
111      *
112      * @throws NutException Exception thrown if no connection to NUT server could be made successfully.
113      */
114     public void connectIfClosed() throws NutException {
115         if (socket == null || socket.isClosed() || !socket.isConnected()) {
116             try {
117                 closeStreams();
118                 socket = newSocket();
119                 socket.connect(inetSocketAddress, TIME_OUT_MILLISECONDS);
120                 final BufferedReader localReader = new BufferedReader(
121                         new InputStreamReader(socket.getInputStream(), StandardCharsets.UTF_8));
122                 reader = localReader;
123                 final PrintWriter localWriter = new PrintWriter(socket.getOutputStream(), true);
124                 writer = localWriter;
125                 writeCommand(login, localReader, localWriter);
126                 writeCommand(password, localReader, localWriter);
127             } catch (final IOException e) {
128                 throw new NutException(e);
129             }
130         }
131     }
132
133     /**
134      * Closes the socket.
135      */
136     public synchronized void close() {
137         try {
138             if (socket != null) {
139                 socket.close();
140                 socket = null;
141             }
142         } catch (final IOException e) {
143             logger.debug("Closing socket failed", e);
144         }
145     }
146
147     private void closeStreams() {
148         try {
149             if (reader != null && socket != null && !socket.isInputShutdown()) {
150                 reader.close();
151             }
152             if (writer != null && socket != null && !socket.isOutputShutdown()) {
153                 writer.close();
154             }
155         } catch (final IOException e) {
156             logger.debug("Closing streams failed", e);
157         }
158     }
159
160     /**
161      * Protected method to be able to mock the Socket connection in unit tests.
162      *
163      * @return a new Socket object
164      */
165     protected Socket newSocket() {
166         return new Socket();
167     }
168
169     private @Nullable String readLine(final BufferedReader reader) throws NutException {
170         try {
171             final String line = reader.readLine();
172
173             if (line != null && line.startsWith(ERR)) {
174                 throw new NutException(line);
175             }
176             return line;
177         } catch (final IOException e) {
178             throw new NutException(e);
179         }
180     }
181
182     private void writeCommand(final String argument, final BufferedReader reader, final PrintWriter writer)
183             throws IOException, NutException {
184         if (!argument.isEmpty()) {
185             writer.println(argument);
186             final String result = reader.readLine();
187
188             logger.trace("Command result: {}", result);
189             if (result == null) {
190                 throw new NutException("No data read after sending command");
191             } else if (!result.startsWith(OK)) {
192                 throw new NutException(result);
193             }
194         }
195     }
196 }