]> git.basschouten.com Git - openhab-addons.git/commitdiff
[tr064] reduce network load and improve XML handling (#9693)
authorJ-N-K <J-N-K@users.noreply.github.com>
Sat, 9 Jan 2021 09:41:22 +0000 (10:41 +0100)
committerGitHub <noreply@github.com>
Sat, 9 Jan 2021 09:41:22 +0000 (10:41 +0100)
* reduce network load
* adjust soap timeout

Signed-off-by: Jan N. Klug <jan.n.klug@rub.de>
bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/Tr064RootHandler.java
bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/soap/SOAPConnector.java
bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/soap/SOAPRequest.java [new file with mode: 0644]
bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/util/Util.java

index b63f93c4f28b02ae21d8a4b2ce53c3f49b275b49..ff9df9b23bc375ffc36fedb43810081bd57fcf15 100644 (file)
@@ -17,6 +17,7 @@ import static org.openhab.binding.tr064.internal.Tr064BindingConstants.THING_TYP
 
 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;
@@ -42,6 +43,7 @@ import org.openhab.binding.tr064.internal.phonebook.PhonebookActions;
 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;
@@ -81,8 +83,8 @@ public class Tr064RootHandler extends BaseBridgeHandler implements PhonebookProv
 
     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;
@@ -222,8 +224,8 @@ public class Tr064RootHandler extends BaseBridgeHandler implements PhonebookProv
                 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)
@@ -248,8 +250,8 @@ public class Tr064RootHandler extends BaseBridgeHandler implements PhonebookProv
                                 "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()
@@ -276,7 +278,7 @@ public class Tr064RootHandler extends BaseBridgeHandler implements PhonebookProv
      */
     public List<SCPDDeviceType> getAllSubDevices() {
         final SCPDUtil scpdUtil = this.scpdUtil;
-        return (scpdUtil == null) ? Collections.emptyList() : scpdUtil.getAllSubDevices();
+        return (scpdUtil == null) ? List.of() : scpdUtil.getAllSubDevices();
     }
 
     /**
@@ -334,8 +336,8 @@ public class Tr064RootHandler extends BaseBridgeHandler implements PhonebookProv
                 .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) {
@@ -357,12 +359,12 @@ public class Tr064RootHandler extends BaseBridgeHandler implements PhonebookProv
 
         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());
index 80db04fc4405ba4b1044e78bb0b939f98d0ce2ab..15d7de7468bb51adf9041a5adfbbfcbef4614ff9 100644 (file)
@@ -17,9 +17,11 @@ import static org.openhab.binding.tr064.internal.util.Util.getSOAPElement;
 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;
@@ -57,12 +59,15 @@ import org.slf4j.LoggerFactory;
  */
 @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;
@@ -72,15 +77,12 @@ public class SOAPConnector {
     /**
      * 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();
@@ -89,8 +91,9 @@ public class SOAPConnector {
 
         // 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) {
@@ -101,11 +104,12 @@ public class SOAPConnector {
 
         // 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()) {
@@ -118,19 +122,46 @@ public class SOAPConnector {
     }
 
     /**
-     * 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())));
             }
@@ -140,8 +171,7 @@ public class SOAPConnector {
                 // 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())) {
@@ -186,7 +216,9 @@ public class SOAPConnector {
                                     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());
                     }
@@ -224,8 +256,8 @@ public class SOAPConnector {
             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()
diff --git a/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/soap/SOAPRequest.java b/bundles/org.openhab.binding.tr064/src/main/java/org/openhab/binding/tr064/internal/soap/SOAPRequest.java
new file mode 100644 (file)
index 0000000..c8d1bef
--- /dev/null
@@ -0,0 +1,72 @@
+/**
+ * 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
+                + '}';
+    }
+}
index 6aa65a13aac45a4a02b4f2c0689bd47f49ce2d78..495e8cc846f932d498ebd63ab858b8266654635b 100644 (file)
@@ -17,6 +17,7 @@ import static org.openhab.binding.tr064.internal.Tr064BindingConstants.*;
 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;
@@ -49,6 +50,7 @@ import org.openhab.binding.tr064.internal.dto.config.ChannelTypeDescriptions;
 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;
@@ -67,6 +69,10 @@ import org.w3c.dom.NodeList;
 @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)
@@ -317,23 +323,37 @@ public class Util {
      * @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;
     }