]> git.basschouten.com Git - openhab-addons.git/blob
e32f0de01486cff05368580e39f7a000ee0c56ea
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2024 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.yamahareceiver.internal.protocol.xml;
14
15 import java.io.BufferedReader;
16 import java.io.DataOutputStream;
17 import java.io.IOException;
18 import java.io.InputStreamReader;
19 import java.net.HttpURLConnection;
20 import java.net.MalformedURLException;
21 import java.net.URL;
22 import java.nio.charset.Charset;
23 import java.nio.charset.IllegalCharsetNameException;
24 import java.nio.charset.StandardCharsets;
25 import java.nio.charset.UnsupportedCharsetException;
26 import java.util.Arrays;
27 import java.util.Optional;
28
29 import org.openhab.binding.yamahareceiver.internal.protocol.AbstractConnection;
30 import org.slf4j.Logger;
31 import org.slf4j.LoggerFactory;
32
33 /**
34  * All other protocol classes in this directory use this class for communication. An object
35  * of HttpXMLSendReceive is always bound to a specific host.
36  *
37  * @author David Graeff - Initial contribution
38  * @author Tomasz Maruszak - Minor refactor
39  *
40  */
41 public class XMLConnection extends AbstractConnection {
42     private Logger logger = LoggerFactory.getLogger(XMLConnection.class);
43
44     private static final String XML_GET = "<?xml version=\"1.0\" encoding=\"utf-8\"?><YAMAHA_AV cmd=\"GET\">";
45     private static final String XML_PUT = "<?xml version=\"1.0\" encoding=\"utf-8\"?><YAMAHA_AV cmd=\"PUT\">";
46     private static final String XML_END = "</YAMAHA_AV>";
47     private static final String HEADER_CHARSET_PART = "charset=";
48
49     private static final int CONNECTION_TIMEOUT_MS = 5000;
50
51     public XMLConnection(String host) {
52         super(host);
53     }
54
55     @FunctionalInterface
56     public interface CheckedConsumer<T, R> {
57         R apply(T t) throws IOException;
58     }
59
60     private <T> T postMessage(String prefix, String message, String suffix,
61             CheckedConsumer<HttpURLConnection, T> responseConsumer) throws IOException {
62         if (message.startsWith("<?xml")) {
63             throw new IOException("No pre-formatted xml allowed!");
64         }
65         message = prefix + message + suffix;
66
67         writeTraceFile(message);
68
69         URL url = createCrlUrl();
70         logger.debug("Making POST to {} with payload: {}", url, message);
71
72         HttpURLConnection connection = null;
73         try {
74             connection = (HttpURLConnection) url.openConnection();
75             connection.setRequestMethod("POST");
76             connection.setRequestProperty("Content-Length", Integer.toString(message.length()));
77
78             // Set a timeout in case the device is not reachable (went offline)
79             connection.setConnectTimeout(CONNECTION_TIMEOUT_MS);
80
81             connection.setUseCaches(false);
82             connection.setDoInput(true);
83             connection.setDoOutput(true);
84
85             // Send request
86             try (DataOutputStream wr = new DataOutputStream(connection.getOutputStream())) {
87                 wr.writeBytes(message);
88                 wr.flush();
89             }
90
91             if (connection.getResponseCode() != 200) {
92                 throw new IOException("Changing a value on the Yamaha AVR failed: " + message);
93             }
94
95             return responseConsumer.apply(connection);
96
97         } finally {
98             if (connection != null) {
99                 connection.disconnect();
100             }
101         }
102     }
103
104     /**
105      * Post the given xml message
106      *
107      * @param message XML formatted message excluding {@code <?xml>} or {@code <YAMAHA_AV>} tags.
108      * @throws IOException
109      */
110     @Override
111     public void send(String message) throws IOException {
112         postMessage(XML_PUT, message, XML_END, c -> null);
113     }
114
115     /**
116      * Post the given xml message and return the response as string.
117      *
118      * @param message XML formatted message excluding {@code <?xml>} or {@code <YAMAHA_AV>} tags.
119      * @return Return the response as text or throws an exception if the connection failed.
120      * @throws IOException
121      */
122     @Override
123     public String sendReceive(final String message) throws IOException {
124         return postMessage(XML_GET, message, XML_END, c -> consumeResponse(c));
125     }
126
127     private String consumeResponse(HttpURLConnection connection) throws IOException {
128         // Read response
129
130         Charset responseCharset = getResponseCharset(connection, StandardCharsets.UTF_8);
131         try (BufferedReader rd = new BufferedReader(
132                 new InputStreamReader(connection.getInputStream(), responseCharset))) {
133             String line;
134             StringBuilder responseBuffer = new StringBuilder();
135             while ((line = rd.readLine()) != null) {
136                 responseBuffer.append(line);
137                 responseBuffer.append('\r');
138             }
139             String response = responseBuffer.toString();
140             writeTraceFile(response);
141             return response;
142         }
143     }
144
145     public String getResponse(String path) throws IOException {
146         URL url = createBaseUrl(path);
147         logger.debug("Making GET to {}", url);
148
149         HttpURLConnection connection = null;
150         try {
151             connection = (HttpURLConnection) url.openConnection();
152             connection.setRequestMethod("GET");
153
154             connection.setUseCaches(false);
155             connection.setDoInput(true);
156             connection.setDoOutput(false);
157
158             if (connection.getResponseCode() != 200) {
159                 throw new IOException("Request failed");
160             }
161
162             return consumeResponse(connection);
163         } finally {
164             if (connection != null) {
165                 connection.disconnect();
166             }
167         }
168     }
169
170     private Charset getResponseCharset(HttpURLConnection connection, Charset defaultCharset) {
171         // See https://stackoverflow.com/a/3934280/1906057
172
173         Charset charset = defaultCharset;
174
175         String contentType = connection.getContentType();
176         String[] values = contentType.split(";"); // values.length should be 2
177
178         // Example:
179         // Content-Type:text/xml; charset="utf-8"
180
181         Optional<String> charsetName = Arrays.stream(values).map(x -> x.trim())
182                 .filter(x -> x.toLowerCase().startsWith(HEADER_CHARSET_PART))
183                 .map(x -> x.substring(HEADER_CHARSET_PART.length() + 1, x.length() - 1)).findFirst();
184
185         if (charsetName.isPresent() && !charsetName.get().isEmpty()) {
186             try {
187                 charset = Charset.forName(charsetName.get());
188             } catch (UnsupportedCharsetException | IllegalCharsetNameException e) {
189                 logger.warn("The charset {} provided in the response {} is not supported", charsetName, contentType);
190             }
191         }
192
193         logger.trace("The charset {} will be used to parse the response", charset);
194         return charset;
195     }
196
197     /**
198      * Creates an {@link URL} object to the Yamaha control endpoint
199      *
200      * @return
201      * @throws MalformedURLException
202      */
203     private URL createCrlUrl() throws MalformedURLException {
204         return createBaseUrl("/YamahaRemoteControl/ctrl");
205     }
206
207     /**
208      * Creates an {@link URL} object to Yamaha
209      *
210      * @return
211      * @throws MalformedURLException
212      */
213     private URL createBaseUrl(String path) throws MalformedURLException {
214         return new URL("http://" + host + path);
215     }
216 }