2 * Copyright (c) 2010-2020 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.apache.commons.lang.StringUtils;
30 import org.openhab.binding.yamahareceiver.internal.protocol.AbstractConnection;
31 import org.slf4j.Logger;
32 import org.slf4j.LoggerFactory;
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.
38 * @author David Graeff - Initial contribution
39 * @author Tomasz Maruszak - Minor refactor
42 public class XMLConnection extends AbstractConnection {
43 private Logger logger = LoggerFactory.getLogger(XMLConnection.class);
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=";
50 private static final int CONNECTION_TIMEOUT_MS = 5000;
52 public XMLConnection(String host) {
57 public interface CheckedConsumer<T, R> {
58 R apply(T t) throws IOException;
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!");
66 message = prefix + message + suffix;
68 writeTraceFile(message);
70 URL url = createCrlUrl();
71 logger.debug("Making POST to {} with payload: {}", url, message);
73 HttpURLConnection connection = null;
75 connection = (HttpURLConnection) url.openConnection();
76 connection.setRequestMethod("POST");
77 connection.setRequestProperty("Content-Length", Integer.toString(message.length()));
79 // Set a timeout in case the device is not reachable (went offline)
80 connection.setConnectTimeout(CONNECTION_TIMEOUT_MS);
82 connection.setUseCaches(false);
83 connection.setDoInput(true);
84 connection.setDoOutput(true);
87 try (DataOutputStream wr = new DataOutputStream(connection.getOutputStream())) {
88 wr.writeBytes(message);
92 if (connection.getResponseCode() != 200) {
93 throw new IOException("Changing a value on the Yamaha AVR failed: " + message);
96 return responseConsumer.apply(connection);
99 if (connection != null) {
100 connection.disconnect();
106 * Post the given xml message
108 * @param message XML formatted message excluding < ?xml > or <YAMAHA_AV> tags.
109 * @throws IOException
112 public void send(String message) throws IOException {
113 postMessage(XML_PUT, message, XML_END, c -> null);
117 * Post the given xml message and return the response as string.
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
124 public String sendReceive(final String message) throws IOException {
125 return postMessage(XML_GET, message, XML_END, c -> consumeResponse(c));
128 private String consumeResponse(HttpURLConnection connection) throws IOException {
131 Charset responseCharset = getResponseCharset(connection, StandardCharsets.UTF_8);
132 try (BufferedReader rd = new BufferedReader(
133 new InputStreamReader(connection.getInputStream(), responseCharset))) {
135 StringBuilder responseBuffer = new StringBuilder();
136 while ((line = rd.readLine()) != null) {
137 responseBuffer.append(line);
138 responseBuffer.append('\r');
140 String response = responseBuffer.toString();
141 writeTraceFile(response);
146 public String getResponse(String path) throws IOException {
147 URL url = createBaseUrl(path);
148 logger.debug("Making GET to {}", url);
150 HttpURLConnection connection = null;
152 connection = (HttpURLConnection) url.openConnection();
153 connection.setRequestMethod("GET");
155 connection.setUseCaches(false);
156 connection.setDoInput(true);
157 connection.setDoOutput(false);
159 if (connection.getResponseCode() != 200) {
160 throw new IOException("Request failed");
163 return consumeResponse(connection);
165 if (connection != null) {
166 connection.disconnect();
171 private Charset getResponseCharset(HttpURLConnection connection, Charset defaultCharset) {
172 // See https://stackoverflow.com/a/3934280/1906057
174 Charset charset = defaultCharset;
176 String contentType = connection.getContentType();
177 String[] values = contentType.split(";"); // values.length should be 2
180 // Content-Type:text/xml; charset="utf-8"
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();
186 if (charsetName.isPresent() && !StringUtils.isEmpty(charsetName.get())) {
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);
194 logger.trace("The charset {} will be used to parse the response", charset);
199 * Creates an {@link URL} object to the Yamaha control endpoint
202 * @throws MalformedURLException
204 private URL createCrlUrl() throws MalformedURLException {
205 return createBaseUrl("/YamahaRemoteControl/ctrl");
209 * Creates an {@link URL} object to Yamaha
212 * @throws MalformedURLException
214 private URL createBaseUrl(String path) throws MalformedURLException {
215 return new URL("http://" + host + path);