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