2 * Copyright (c) 2010-2024 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
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
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.yamahareceiver.internal.protocol.xml;
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;
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;
29 import org.openhab.binding.yamahareceiver.internal.protocol.AbstractConnection;
30 import org.slf4j.Logger;
31 import org.slf4j.LoggerFactory;
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.
37 * @author David Graeff - Initial contribution
38 * @author Tomasz Maruszak - Minor refactor
41 public class XMLConnection extends AbstractConnection {
42 private Logger logger = LoggerFactory.getLogger(XMLConnection.class);
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=";
49 private static final int CONNECTION_TIMEOUT_MS = 5000;
51 public XMLConnection(String host) {
56 public interface CheckedConsumer<T, R> {
57 R apply(T t) throws IOException;
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!");
65 message = prefix + message + suffix;
67 writeTraceFile(message);
69 URL url = createCrlUrl();
70 logger.debug("Making POST to {} with payload: {}", url, message);
72 HttpURLConnection connection = null;
74 connection = (HttpURLConnection) url.openConnection();
75 connection.setRequestMethod("POST");
76 connection.setRequestProperty("Content-Length", Integer.toString(message.length()));
78 // Set a timeout in case the device is not reachable (went offline)
79 connection.setConnectTimeout(CONNECTION_TIMEOUT_MS);
81 connection.setUseCaches(false);
82 connection.setDoInput(true);
83 connection.setDoOutput(true);
86 try (DataOutputStream wr = new DataOutputStream(connection.getOutputStream())) {
87 wr.writeBytes(message);
91 if (connection.getResponseCode() != 200) {
92 throw new IOException("Changing a value on the Yamaha AVR failed: " + message);
95 return responseConsumer.apply(connection);
98 if (connection != null) {
99 connection.disconnect();
105 * Post the given xml message
107 * @param message XML formatted message excluding {@code <?xml>} or {@code <YAMAHA_AV>} tags.
108 * @throws IOException
111 public void send(String message) throws IOException {
112 postMessage(XML_PUT, message, XML_END, c -> null);
116 * Post the given xml message and return the response as string.
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
123 public String sendReceive(final String message) throws IOException {
124 return postMessage(XML_GET, message, XML_END, c -> consumeResponse(c));
127 private String consumeResponse(HttpURLConnection connection) throws IOException {
130 Charset responseCharset = getResponseCharset(connection, StandardCharsets.UTF_8);
131 try (BufferedReader rd = new BufferedReader(
132 new InputStreamReader(connection.getInputStream(), responseCharset))) {
134 StringBuilder responseBuffer = new StringBuilder();
135 while ((line = rd.readLine()) != null) {
136 responseBuffer.append(line);
137 responseBuffer.append('\r');
139 String response = responseBuffer.toString();
140 writeTraceFile(response);
145 public String getResponse(String path) throws IOException {
146 URL url = createBaseUrl(path);
147 logger.debug("Making GET to {}", url);
149 HttpURLConnection connection = null;
151 connection = (HttpURLConnection) url.openConnection();
152 connection.setRequestMethod("GET");
154 connection.setUseCaches(false);
155 connection.setDoInput(true);
156 connection.setDoOutput(false);
158 if (connection.getResponseCode() != 200) {
159 throw new IOException("Request failed");
162 return consumeResponse(connection);
164 if (connection != null) {
165 connection.disconnect();
170 private Charset getResponseCharset(HttpURLConnection connection, Charset defaultCharset) {
171 // See https://stackoverflow.com/a/3934280/1906057
173 Charset charset = defaultCharset;
175 String contentType = connection.getContentType();
176 String[] values = contentType.split(";"); // values.length should be 2
179 // Content-Type:text/xml; charset="utf-8"
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();
185 if (charsetName.isPresent() && !charsetName.get().isEmpty()) {
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);
193 logger.trace("The charset {} will be used to parse the response", charset);
198 * Creates an {@link URL} object to the Yamaha control endpoint
201 * @throws MalformedURLException
203 private URL createCrlUrl() throws MalformedURLException {
204 return createBaseUrl("/YamahaRemoteControl/ctrl");
208 * Creates an {@link URL} object to Yamaha
211 * @throws MalformedURLException
213 private URL createBaseUrl(String path) throws MalformedURLException {
214 return new URL("http://" + host + path);