import java.net.URI;
import java.net.URISyntaxException;
+import java.time.Duration;
import java.util.*;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import org.openhab.binding.tr064.internal.phonebook.PhonebookProvider;
import org.openhab.binding.tr064.internal.phonebook.Tr064PhonebookImpl;
import org.openhab.binding.tr064.internal.soap.SOAPConnector;
+import org.openhab.binding.tr064.internal.soap.SOAPRequest;
import org.openhab.binding.tr064.internal.soap.SOAPValueConverter;
import org.openhab.binding.tr064.internal.util.SCPDUtil;
import org.openhab.binding.tr064.internal.util.Util;
private final Map<ChannelUID, Tr064ChannelConfig> channels = new HashMap<>();
// caching is used to prevent excessive calls to the same action
- private final ExpiringCacheMap<ChannelUID, State> stateCache = new ExpiringCacheMap<>(2000);
- private Collection<Phonebook> phonebooks = Collections.emptyList();
+ private final ExpiringCacheMap<ChannelUID, State> stateCache = new ExpiringCacheMap<>(Duration.ofMillis(2000));
+ private Collection<Phonebook> phonebooks = List.of();
private @Nullable ScheduledFuture<?> connectFuture;
private @Nullable ScheduledFuture<?> pollFuture;
this.deviceType = device.getDeviceType();
// try to get security (https) port
- SOAPMessage soapResponse = soapConnector.doSOAPRequest(deviceService, "GetSecurityPort",
- Collections.emptyMap());
+ SOAPMessage soapResponse = soapConnector
+ .doSOAPRequest(new SOAPRequest(deviceService, "GetSecurityPort"));
if (!soapResponse.getSOAPBody().hasFault()) {
SOAPValueConverter soapValueConverter = new SOAPValueConverter(httpClient);
soapValueConverter.getStateFromSOAPValue(soapResponse, "NewSecurityPort", null)
"Could not get service definition for 'urn:DeviceInfo-com:serviceId:DeviceInfo1'"))
.getActionList().stream().filter(action -> action.getName().equals("GetInfo")).findFirst()
.orElseThrow(() -> new SCPDException("Action 'GetInfo' not found"));
- SOAPMessage soapResponse1 = soapConnector.doSOAPRequest(deviceService, getInfoAction.getName(),
- Collections.emptyMap());
+ SOAPMessage soapResponse1 = soapConnector
+ .doSOAPRequest(new SOAPRequest(deviceService, getInfoAction.getName()));
SOAPValueConverter soapValueConverter = new SOAPValueConverter(httpClient);
Map<String, String> properties = editProperties();
PROPERTY_ARGUMENTS.forEach(argumentName -> getInfoAction.getArgumentList().stream()
*/
public List<SCPDDeviceType> getAllSubDevices() {
final SCPDUtil scpdUtil = this.scpdUtil;
- return (scpdUtil == null) ? Collections.emptyList() : scpdUtil.getAllSubDevices();
+ return (scpdUtil == null) ? List.of() : scpdUtil.getAllSubDevices();
}
/**
.map(phonebookList -> Arrays.stream(phonebookList.toString().split(","))).orElse(Stream.empty())
.map(index -> {
try {
- SOAPMessage soapMessageURL = soapConnector.doSOAPRequest(scpdService, "GetPhonebook",
- Map.of("NewPhonebookID", index));
+ SOAPMessage soapMessageURL = soapConnector.doSOAPRequest(
+ new SOAPRequest(scpdService, "GetPhonebook", Map.of("NewPhonebookID", index)));
return soapValueConverter.getStateFromSOAPValue(soapMessageURL, "NewPhonebookURL", null)
.map(url -> (Phonebook) new Tr064PhonebookImpl(httpClient, url.toString()));
} catch (Tr064CommunicationException e) {
phonebooks = scpdService.map(service -> {
try {
- return processPhonebookList(
- soapConnector.doSOAPRequest(service, "GetPhonebookList", Collections.emptyMap()), service);
+ return processPhonebookList(soapConnector.doSOAPRequest(new SOAPRequest(service, "GetPhonebookList")),
+ service);
} catch (Tr064CommunicationException e) {
return Collections.<Phonebook> emptyList();
}
- }).orElse(Collections.emptyList());
+ }).orElse(List.of());
if (phonebooks.isEmpty()) {
logger.warn("Could not get phonebooks for thing {}", thing.getUID());
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
+import java.time.Duration;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
+import java.util.Objects;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
*/
@NonNullByDefault
public class SOAPConnector {
- private static final int SOAP_TIMEOUT = 2000; // in ms
+ private static final int SOAP_TIMEOUT = 5; // in
private final Logger logger = LoggerFactory.getLogger(SOAPConnector.class);
private final HttpClient httpClient;
private final String endpointBaseURL;
private final SOAPValueConverter soapValueConverter;
+ private final ExpiringCacheMap<SOAPRequest, SOAPMessage> soapMessageCache = new ExpiringCacheMap<>(
+ Duration.ofMillis(2000));
+
public SOAPConnector(HttpClient httpClient, String endpointBaseURL) {
this.httpClient = httpClient;
this.endpointBaseURL = endpointBaseURL;
/**
* prepare a SOAP request for an action request to a service
*
- * @param service the service
- * @param soapAction the action to send
- * @param arguments arguments to send along with the request
+ * @param soapRequest the request to be generated
* @return a jetty Request containing the full SOAP message
* @throws IOException if a problem while writing the SOAP message to the Request occurs
* @throws SOAPException if a problem with creating the SOAP message occurs
*/
- private Request prepareSOAPRequest(SCPDServiceType service, String soapAction, Map<String, String> arguments)
- throws IOException, SOAPException {
+ private Request prepareSOAPRequest(SOAPRequest soapRequest) throws IOException, SOAPException {
MessageFactory messageFactory = MessageFactory.newInstance();
SOAPMessage soapMessage = messageFactory.createMessage();
SOAPPart soapPart = soapMessage.getSOAPPart();
// SOAP body
SOAPBody soapBody = envelope.getBody();
- SOAPElement soapBodyElem = soapBody.addChildElement(soapAction, "u", service.getServiceType());
- arguments.entrySet().stream().sorted(Map.Entry.comparingByKey()).forEach(argument -> {
+ SOAPElement soapBodyElem = soapBody.addChildElement(soapRequest.soapAction, "u",
+ soapRequest.service.getServiceType());
+ soapRequest.arguments.entrySet().stream().sorted(Map.Entry.comparingByKey()).forEach(argument -> {
try {
soapBodyElem.addChildElement(argument.getKey()).setTextContent(argument.getValue());
} catch (SOAPException e) {
// SOAP headers
MimeHeaders headers = soapMessage.getMimeHeaders();
- headers.addHeader("SOAPAction", service.getServiceType() + "#" + soapAction);
+ headers.addHeader("SOAPAction", soapRequest.service.getServiceType() + "#" + soapRequest.soapAction);
soapMessage.saveChanges();
// create Request and add headers and content
- Request request = httpClient.newRequest(endpointBaseURL + service.getControlURL()).method(HttpMethod.POST);
+ Request request = httpClient.newRequest(endpointBaseURL + soapRequest.service.getControlURL())
+ .method(HttpMethod.POST);
((Iterator<MimeHeader>) soapMessage.getMimeHeaders().getAllHeaders())
.forEachRemaining(header -> request.header(header.getName(), header.getValue()));
try (final ByteArrayOutputStream os = new ByteArrayOutputStream()) {
}
/**
- * execute a SOAP request
+ * execute a SOAP request with cache
*
- * @param service the service to send the action to
- * @param soapAction the action itself
- * @param arguments arguments to send along with the request
+ * @param soapRequest the request itself
* @return the SOAPMessage answer from the remote host
* @throws Tr064CommunicationException if an error occurs during the request
*/
- public synchronized SOAPMessage doSOAPRequest(SCPDServiceType service, String soapAction,
- Map<String, String> arguments) throws Tr064CommunicationException {
+ public SOAPMessage doSOAPRequest(SOAPRequest soapRequest) throws Tr064CommunicationException {
try {
- Request request = prepareSOAPRequest(service, soapAction, arguments).timeout(SOAP_TIMEOUT,
- TimeUnit.MILLISECONDS);
+ SOAPMessage soapMessage = Objects.requireNonNull(soapMessageCache.putIfAbsentAndGet(soapRequest, () -> {
+ try {
+ SOAPMessage newValue = doSOAPRequestUncached(soapRequest);
+ logger.trace("Storing in cache: {}", newValue);
+ return newValue;
+ } catch (Tr064CommunicationException e) {
+ // wrap exception
+ throw new IllegalArgumentException(e);
+ }
+ }));
+ logger.trace("Returning from cache: {}", soapMessage);
+ return soapMessage;
+ } catch (IllegalArgumentException e) {
+ Throwable cause = e.getCause();
+ if (cause instanceof Tr064CommunicationException) {
+ throw (Tr064CommunicationException) cause;
+ } else {
+ throw e;
+ }
+ }
+ }
+
+ /**
+ * execute a SOAP request without cache
+ *
+ * @param soapRequest the request itself
+ * @return the SOAPMessage answer from the remote host
+ * @throws Tr064CommunicationException if an error occurs during the request
+ */
+ public synchronized SOAPMessage doSOAPRequestUncached(SOAPRequest soapRequest) throws Tr064CommunicationException {
+ try {
+ Request request = prepareSOAPRequest(soapRequest).timeout(SOAP_TIMEOUT, TimeUnit.SECONDS);
if (logger.isTraceEnabled()) {
request.getContent().forEach(buffer -> logger.trace("Request: {}", new String(buffer.array())));
}
// retry once if authentication expired
logger.trace("Re-Auth needed.");
httpClient.getAuthenticationStore().clearAuthenticationResults();
- request = prepareSOAPRequest(service, soapAction, arguments).timeout(SOAP_TIMEOUT,
- TimeUnit.MILLISECONDS);
+ request = prepareSOAPRequest(soapRequest).timeout(SOAP_TIMEOUT, TimeUnit.SECONDS);
response = request.send();
}
try (final ByteArrayInputStream is = new ByteArrayInputStream(response.getContent())) {
channelConfig.getChannelTypeDescription().getGetAction().getParameter().getName(),
parameter);
}
- doSOAPRequest(service, channelTypeDescription.getSetAction().getName(), arguments);
+ SOAPRequest soapRequest = new SOAPRequest(service,
+ channelTypeDescription.getSetAction().getName(), arguments);
+ doSOAPRequestUncached(soapRequest);
} catch (Tr064CommunicationException e) {
logger.warn("Could not send command {}: {}", command, e.getMessage());
}
if (parameter != null && !action.getParameter().isInternalOnly()) {
arguments.put(action.getParameter().getName(), parameter);
}
- SOAPMessage soapResponse = doSOAPRequest(channelConfig.getService(), getAction.getName(), arguments);
-
+ SOAPMessage soapResponse = doSOAPRequest(
+ new SOAPRequest(channelConfig.getService(), getAction.getName(), arguments));
String argumentName = channelConfig.getChannelTypeDescription().getGetAction().getArgument();
// find all other channels with the same action that are already in cache, so we can update them
Map<ChannelUID, Tr064ChannelConfig> channelsInRequest = channelConfigMap.entrySet().stream()
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.tr064.internal.soap;
+
+import java.util.Map;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.tr064.internal.dto.scpd.root.SCPDServiceType;
+
+/**
+ * The {@link SOAPRequest} is a wrapper for SOAP requests
+ *
+ * @author Jan N. Klug - Initial contribution
+ */
+@NonNullByDefault
+public class SOAPRequest {
+ public SCPDServiceType service;
+ public String soapAction;
+ public Map<String, String> arguments = Map.of();
+
+ public SOAPRequest(SCPDServiceType service, String soapAction) {
+ this.service = service;
+ this.soapAction = soapAction;
+ }
+
+ public SOAPRequest(SCPDServiceType service, String soapAction, Map<String, String> arguments) {
+ this.service = service;
+ this.soapAction = soapAction;
+ this.arguments = arguments;
+ }
+
+ @Override
+ public boolean equals(@Nullable Object o) {
+ if (this == o)
+ return true;
+ if (o == null || getClass() != o.getClass())
+ return false;
+
+ SOAPRequest that = (SOAPRequest) o;
+
+ if (!service.equals(that.service))
+ return false;
+ if (!soapAction.equals(that.soapAction))
+ return false;
+ return arguments.equals(that.arguments);
+ }
+
+ @Override
+ public int hashCode() {
+ int result = service.hashCode();
+ result = 31 * result + soapAction.hashCode();
+ result = 31 * result + arguments.hashCode();
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ return "SOAPRequest{" + "service=" + service + ", soapAction='" + soapAction + '\'' + ", arguments=" + arguments
+ + '}';
+ }
+}
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.lang.reflect.Field;
+import java.time.Duration;
import java.util.*;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import org.openhab.binding.tr064.internal.dto.config.ParameterType;
import org.openhab.binding.tr064.internal.dto.scpd.root.SCPDServiceType;
import org.openhab.binding.tr064.internal.dto.scpd.service.*;
+import org.openhab.core.cache.ExpiringCacheMap;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.binding.builder.ChannelBuilder;
@NonNullByDefault
public class Util {
private static final Logger LOGGER = LoggerFactory.getLogger(Util.class);
+ private static final int HTTP_REQUEST_TIMEOUT = 5; // in s
+ // cache XML content for 5s
+ private static final ExpiringCacheMap<String, Object> XML_OBJECT_CACHE = new ExpiringCacheMap<>(
+ Duration.ofMillis(3000));
/**
* read the channel config from the resource file (static initialization)
* @param clazz the class describing the XML file
* @return unmarshalling result
*/
+ @SuppressWarnings("unchecked")
public static <T> @Nullable T getAndUnmarshalXML(HttpClient httpClient, String uri, Class<T> clazz) {
try {
- ContentResponse contentResponse = httpClient.newRequest(uri).timeout(2, TimeUnit.SECONDS)
- .method(HttpMethod.GET).send();
- byte[] response = contentResponse.getContent();
- if (LOGGER.isTraceEnabled()) {
- LOGGER.trace("XML = {}", new String(response));
- }
- InputStream xml = new ByteArrayInputStream(response);
+ T returnValue = (T) XML_OBJECT_CACHE.putIfAbsentAndGet(uri, () -> {
+ try {
+ LOGGER.trace("Refreshing cache for '{}'", uri);
+ ContentResponse contentResponse = httpClient.newRequest(uri)
+ .timeout(HTTP_REQUEST_TIMEOUT, TimeUnit.SECONDS).method(HttpMethod.GET).send();
+ byte[] response = contentResponse.getContent();
+ if (LOGGER.isTraceEnabled()) {
+ LOGGER.trace("XML = {}", new String(response));
+ }
+ InputStream xml = new ByteArrayInputStream(response);
- JAXBContext context = JAXBContext.newInstance(clazz);
- Unmarshaller um = context.createUnmarshaller();
- return um.unmarshal(new StreamSource(xml), clazz).getValue();
- } catch (ExecutionException | InterruptedException | TimeoutException e) {
- LOGGER.debug("HTTP Failed to GET uri '{}': {}", uri, e.getMessage());
- } catch (JAXBException e) {
- LOGGER.debug("Unmarshalling failed: {}", e.getMessage());
+ JAXBContext context = JAXBContext.newInstance(clazz);
+ Unmarshaller um = context.createUnmarshaller();
+ T newValue = um.unmarshal(new StreamSource(xml), clazz).getValue();
+ LOGGER.trace("Storing in cache {}", newValue);
+ return newValue;
+ } catch (ExecutionException | InterruptedException | TimeoutException e) {
+ LOGGER.debug("HTTP Failed to GET uri '{}': {}", uri, e.getMessage());
+ throw new IllegalArgumentException();
+ } catch (JAXBException e) {
+ LOGGER.debug("Unmarshalling failed: {}", e.getMessage());
+ throw new IllegalArgumentException();
+ }
+ });
+ LOGGER.trace("Returning from cache: {}", returnValue);
+ return returnValue;
+ } catch (IllegalArgumentException e) {
+ // already logged
}
return null;
}