]> git.basschouten.com Git - openhab-addons.git/commitdiff
[feed] Minor improvements for Feed Binding (#8824)
authorChristoph Weitkamp <github@christophweitkamp.de>
Wed, 21 Oct 2020 15:53:46 +0000 (17:53 +0200)
committerGitHub <noreply@github.com>
Wed, 21 Oct 2020 15:53:46 +0000 (08:53 -0700)
Signed-off-by: Christoph Weitkamp <github@christophweitkamp.de>
bundles/org.openhab.binding.feed/README.md
bundles/org.openhab.binding.feed/src/main/java/org/openhab/binding/feed/internal/FeedBindingConstants.java
bundles/org.openhab.binding.feed/src/main/java/org/openhab/binding/feed/internal/FeedHandlerFactory.java
bundles/org.openhab.binding.feed/src/main/java/org/openhab/binding/feed/internal/handler/FeedHandler.java
bundles/org.openhab.binding.feed/src/main/resources/OH-INF/thing/thing-types.xml
itests/org.openhab.binding.feed.tests/src/main/java/org/openhab/binding/feed/test/FeedHandlerTest.java
itests/org.openhab.binding.feed.tests/src/main/java/org/openhab/binding/feed/test/SlowTests.java

index 982d52d0eb09bb137de7622f5a2bb287cae816a3..cec19d5472dc5730ac4be0f7f1c88289e6cccc88 100644 (file)
@@ -4,9 +4,7 @@ This binding allows you to integrate feeds in the openHAB environment.
 The Feed binding downloads the content, tracks for changes, and displays information like feed author, feed title and description, number of entries, last update date.
 
 It can be used in combination with openHAB rules to trigger events on feed change.
-It uses the [ROME library](https://rometools.github.io/rome/index.html) for parsing
-and supports a wide range of popular feed formats - RSS 2.00, RSS 1.00, RSS 0.94, RSS 0.93, RSS 0.92, RSS 0.91 UserLand,
-RSS 0.91 Netscape, RSS 0.90, Atom 1.0, Atom 0.3.
+It uses the [ROME library](https://rometools.github.io/rome/index.html) for parsing and supports a wide range of popular feed formats - RSS 2.00, RSS 1.00, RSS 0.94, RSS 0.93, RSS 0.92, RSS 0.91 UserLand, RSS 0.91 Netscape, RSS 0.90, Atom 1.0, Atom 0.3.
 
 ## Supported Things
 
@@ -24,11 +22,11 @@ No binding configuration required.
 
 Required configuration:
 
-- **URL** - the URL of the feed (e.g <http://example.com/path/file>). The binding uses this URL to download data
+- **URL** - the URL of the feed (e.g <http://example.com/path/file>). The binding uses this URL to download data.
 
 Optional configuration:
 
-- **refresh** - a refresh interval defines after how many minutes the binding will check, if new content is available. Default value is 20 minutes
+- **refresh** - a refresh interval defines after how many minutes the binding will check, if new content is available. Default value is 20 minutes.
 
 ## Channels
 
@@ -39,18 +37,18 @@ The binding supports following channels
 | latest-title       | String    | Contains the title of the last feed entry.          |
 | latest-description | String    | Contains the description of last feed entry.        |
 | latest-date        | DateTime  | Contains the published date of the last feed entry. |
-| author             | String    | The name of the feed author, if author is present   |
-| title              | String    | The title of the feed                               |
-| description        | String    | Description of the feed                             |
-| last-update        | DateTime  | The last update date of the feed                    |
-| number-of-entries  | Number    | Number of entries in the feed                       |
+| author             | String    | The name of the feed author, if author is present.  |
+| title              | String    | The title of the feed.                              |
+| description        | String    | Description of the feed.                            |
+| last-update        | DateTime  | The last update date of the feed.                   |
+| number-of-entries  | Number    | Number of entries in the feed.                      |
 
 ## Example
 
 Things:
 
 ```java
-feed:feed:bbc  [ URL="http://feeds.bbci.co.uk/news/video_and_audio/news_front_page/rss.xml?edition=uk"]
+feed:feed:bbc [ URL="http://feeds.bbci.co.uk/news/video_and_audio/news_front_page/rss.xml?edition=uk"]
 feed:feed:techCrunch [ URL="http://feeds.feedburner.com/TechCrunch/", refresh=60]
 ```
 
index cdc78866acca5f2b3434739d11180a67111b8db6..366f131a7f8209b44c941e00adc2bcd7dc57f898 100644 (file)
@@ -12,8 +12,6 @@
  */
 package org.openhab.binding.feed.internal;
 
-import java.math.BigDecimal;
-
 import org.eclipse.jdt.annotation.NonNullByDefault;
 import org.openhab.core.thing.ThingTypeUID;
 
@@ -86,11 +84,11 @@ public class FeedBindingConstants {
     /**
      * The default auto refresh time in minutes.
      */
-    public static final BigDecimal DEFAULT_REFRESH_TIME = new BigDecimal(20);
+    public static final long DEFAULT_REFRESH_TIME = 20;
 
     /**
      * The minimum refresh time in milliseconds. Any REFRESH command send to a Thing, before this time has expired, will
-     * not trigger an attempt to dowload new data form the server.
+     * not trigger an attempt to download new data from the server.
      **/
     public static final int MINIMUM_REFRESH_TIME = 3000;
 }
index b090fb95bf9c9315657e0ce1036538e347927a93..c39546c9745d53ed7ee6aefa5c219968785499e6 100644 (file)
@@ -14,9 +14,10 @@ package org.openhab.binding.feed.internal;
 
 import static org.openhab.binding.feed.internal.FeedBindingConstants.FEED_THING_TYPE_UID;
 
-import java.util.Collections;
 import java.util.Set;
 
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
 import org.openhab.binding.feed.internal.handler.FeedHandler;
 import org.openhab.core.thing.Thing;
 import org.openhab.core.thing.ThingTypeUID;
@@ -32,9 +33,10 @@ import org.osgi.service.component.annotations.Component;
  * @author Svilen Valkanov - Initial contribution
  */
 @Component(service = ThingHandlerFactory.class, configurationPid = "binding.feed")
+@NonNullByDefault
 public class FeedHandlerFactory extends BaseThingHandlerFactory {
 
-    private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Collections.singleton(FEED_THING_TYPE_UID);
+    private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Set.of(FEED_THING_TYPE_UID);
 
     @Override
     public boolean supportsThingType(ThingTypeUID thingTypeUID) {
@@ -42,7 +44,7 @@ public class FeedHandlerFactory extends BaseThingHandlerFactory {
     }
 
     @Override
-    protected ThingHandler createHandler(Thing thing) {
+    protected @Nullable ThingHandler createHandler(Thing thing) {
         ThingTypeUID thingTypeUID = thing.getThingTypeUID();
 
         if (thingTypeUID.equals(FEED_THING_TYPE_UID)) {
index 9d828965157855c7566f51b1a5f047bb4c344bd5..874654c09fd7512b82f33942762e313d65378dab 100644 (file)
@@ -29,11 +29,12 @@ import java.util.concurrent.ScheduledFuture;
 import java.util.concurrent.TimeUnit;
 import java.util.zip.GZIPInputStream;
 
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
 import org.openhab.core.config.core.Configuration;
 import org.openhab.core.library.types.DateTimeType;
 import org.openhab.core.library.types.DecimalType;
 import org.openhab.core.library.types.StringType;
-import org.openhab.core.thing.Channel;
 import org.openhab.core.thing.ChannelUID;
 import org.openhab.core.thing.Thing;
 import org.openhab.core.thing.ThingStatus;
@@ -57,73 +58,80 @@ import com.rometools.rome.io.SyndFeedInput;
  *
  * @author Svilen Valkanov - Initial contribution
  */
+@NonNullByDefault
 public class FeedHandler extends BaseThingHandler {
 
-    private Logger logger = LoggerFactory.getLogger(FeedHandler.class);
+    private final Logger logger = LoggerFactory.getLogger(FeedHandler.class);
 
-    private String urlString;
-    private BigDecimal refreshTime;
-    private ScheduledFuture<?> refreshTask;
-    private SyndFeed currentFeedState;
+    private @Nullable URL url;
+    private long refreshTime;
+    private @Nullable ScheduledFuture<?> refreshTask;
+    private @Nullable SyndFeed currentFeedState;
     private long lastRefreshTime;
 
     public FeedHandler(Thing thing) {
         super(thing);
-        currentFeedState = null;
     }
 
     @Override
     public void initialize() {
-        checkConfiguration();
-        updateStatus(ThingStatus.UNKNOWN);
-        startAutomaticRefresh();
+        if (checkConfiguration()) {
+            updateStatus(ThingStatus.UNKNOWN);
+            startAutomaticRefresh();
+        }
     }
 
     /**
      * This method checks if the provided configuration is valid.
      * When invalid parameter is found, default value is assigned.
      */
-    private void checkConfiguration() {
+    private boolean checkConfiguration() {
         logger.debug("Start reading Feed Thing configuration.");
         Configuration configuration = getConfig();
 
         // It is not necessary to check if the URL is valid, this will be done in fetchFeedData() method
-        urlString = (String) configuration.get(URL);
+        String urlString = (String) configuration.get(URL);
+        try {
+            url = new URL(urlString);
+        } catch (MalformedURLException e) {
+            logger.warn("Url '{}' is not valid: ", urlString, e);
+            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, e.getMessage());
+            return false;
+        }
 
+        BigDecimal localRefreshTime = null;
         try {
-            refreshTime = (BigDecimal) configuration.get(REFRESH_TIME);
-            if (refreshTime.intValue() <= 0) {
+            localRefreshTime = (BigDecimal) configuration.get(REFRESH_TIME);
+            if (localRefreshTime.intValue() <= 0) {
                 throw new IllegalArgumentException("Refresh time must be positive number!");
             }
+            refreshTime = localRefreshTime.longValue();
         } catch (Exception e) {
-            logger.warn("Refresh time [{}] is not valid. Falling back to default value: {}. {}", refreshTime,
+            logger.warn("Refresh time [{}] is not valid. Falling back to default value: {}. {}", localRefreshTime,
                     DEFAULT_REFRESH_TIME, e.getMessage());
             refreshTime = DEFAULT_REFRESH_TIME;
         }
+        return true;
     }
 
     private void startAutomaticRefresh() {
-        refreshTask = scheduler.scheduleWithFixedDelay(this::refreshFeedState, 0, refreshTime.intValue(),
-                TimeUnit.MINUTES);
-        logger.debug("Start automatic refresh at {} minutes", refreshTime.intValue());
+        refreshTask = scheduler.scheduleWithFixedDelay(this::refreshFeedState, 0, refreshTime, TimeUnit.MINUTES);
+        logger.debug("Start automatic refresh at {} minutes!", refreshTime);
     }
 
     private void refreshFeedState() {
-        SyndFeed feed = fetchFeedData(urlString);
+        SyndFeed feed = fetchFeedData();
         boolean feedUpdated = updateFeedIfChanged(feed);
-
         if (feedUpdated) {
-            List<Channel> channels = getThing().getChannels();
-            for (Channel channel : channels) {
-                publishChannelIfLinked(channel.getUID());
-            }
+            getThing().getChannels().forEach(channel -> publishChannelIfLinked(channel.getUID()));
         }
     }
 
     private void publishChannelIfLinked(ChannelUID channelUID) {
         String channelID = channelUID.getId();
 
-        if (currentFeedState == null) {
+        SyndFeed feedState = currentFeedState;
+        if (feedState == null) {
             // This will happen if the binding could not download data from the server
             logger.trace("Cannot update channel with ID {}; no data has been downloaded from the server!", channelID);
             return;
@@ -135,7 +143,7 @@ public class FeedHandler extends BaseThingHandler {
         }
 
         State state = null;
-        SyndEntry latestEntry = getLatestEntry(currentFeedState);
+        SyndEntry latestEntry = getLatestEntry(feedState);
 
         switch (channelID) {
             case CHANNEL_LATEST_TITLE:
@@ -166,19 +174,19 @@ public class FeedHandler extends BaseThingHandler {
                 }
                 break;
             case CHANNEL_AUTHOR:
-                String author = currentFeedState.getAuthor();
+                String author = feedState.getAuthor();
                 state = new StringType(getValueSafely(author));
                 break;
             case CHANNEL_DESCRIPTION:
-                String channelDescription = currentFeedState.getDescription();
+                String channelDescription = feedState.getDescription();
                 state = new StringType(getValueSafely(channelDescription));
                 break;
             case CHANNEL_TITLE:
-                String channelTitle = currentFeedState.getTitle();
+                String channelTitle = feedState.getTitle();
                 state = new StringType(getValueSafely(channelTitle));
                 break;
             case CHANNEL_NUMBER_OF_ENTRIES:
-                int numberOfEntries = currentFeedState.getEntries().size();
+                int numberOfEntries = feedState.getEntries().size();
                 state = new DecimalType(numberOfEntries);
                 break;
             default:
@@ -200,7 +208,7 @@ public class FeedHandler extends BaseThingHandler {
      * @return <code>true</code> if new content is available on the server since the last update or <code>false</code>
      *         otherwise
      */
-    private synchronized boolean updateFeedIfChanged(SyndFeed newFeedState) {
+    private synchronized boolean updateFeedIfChanged(@Nullable SyndFeed newFeedState) {
         // SyndFeed class has implementation of equals ()
         if (newFeedState != null && !newFeedState.equals(currentFeedState)) {
             currentFeedState = newFeedState;
@@ -218,16 +226,18 @@ public class FeedHandler extends BaseThingHandler {
      * {@link ThingStatusDetail#CONFIGURATION_ERROR} or
      * {@link ThingStatusDetail#COMMUNICATION_ERROR} and adequate message.
      *
-     * @param urlString URL of the Feed
      * @return {@link SyndFeed} instance with the feed data, if the connection attempt was successful and
      *         <code>null</code> otherwise
      */
-    private SyndFeed fetchFeedData(String urlString) {
-        SyndFeed feed = null;
-        try {
-            URL url = new URL(urlString);
+    private @Nullable SyndFeed fetchFeedData() {
+        URL localUrl = url;
+        if (localUrl == null) {
+            logger.trace("Url '{}' is not valid: ", localUrl);
+            return null;
+        }
 
-            URLConnection connection = url.openConnection();
+        try {
+            URLConnection connection = localUrl.openConnection();
             connection.setRequestProperty("Accept-Encoding", "gzip");
 
             BufferedReader in = null;
@@ -238,50 +248,45 @@ public class FeedHandler extends BaseThingHandler {
             }
 
             SyndFeedInput input = new SyndFeedInput();
-            feed = input.build(in);
+            SyndFeed feed = input.build(in);
             in.close();
 
             if (this.thing.getStatus() != ThingStatus.ONLINE) {
                 updateStatus(ThingStatus.ONLINE);
             }
-        } catch (MalformedURLException e) {
-            logger.warn("Url '{}' is not valid: ", urlString, e);
-            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, e.getMessage());
-            return null;
+
+            return feed;
         } catch (IOException e) {
-            logger.warn("Error accessing feed: {}", urlString, e);
+            logger.warn("Error accessing feed: {}", localUrl, e);
             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, e.getMessage());
             return null;
         } catch (IllegalArgumentException e) {
-            logger.warn("Feed URL is null ", e);
+            logger.warn("Feed URL is null: {} ", localUrl, e);
             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, e.getMessage());
             return null;
         } catch (FeedException e) {
-            logger.warn("Feed content is not valid: {} ", urlString, e);
+            logger.warn("Feed content is not valid: {} ", localUrl, e);
             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, e.getMessage());
             return null;
         }
-
-        return feed;
     }
 
     /**
      * Returns the most recent entry or null, if no entries are found.
      */
-    private SyndEntry getLatestEntry(SyndFeed feed) {
+    private @Nullable SyndEntry getLatestEntry(SyndFeed feed) {
         List<SyndEntry> allEntries = feed.getEntries();
-        SyndEntry lastEntry = null;
         if (!allEntries.isEmpty()) {
             /*
              * The entries are stored in the SyndFeed object in the following order -
              * the newest entry has index 0. The order is determined from the time the entry was posted, not the
              * published time of the entry.
              */
-            lastEntry = allEntries.get(0);
+            return allEntries.get(0);
         } else {
             logger.debug("No entries found");
         }
-        return lastEntry;
+        return null;
     }
 
     @Override
@@ -289,7 +294,7 @@ public class FeedHandler extends BaseThingHandler {
         if (command instanceof RefreshType) {
             // safeguard for multiple REFRESH commands for different channels in a row
             if (isMinimumRefreshTimeExceeded()) {
-                SyndFeed feed = fetchFeedData(urlString);
+                SyndFeed feed = fetchFeedData();
                 updateFeedIfChanged(feed);
             }
             publishChannelIfLinked(channelUID);
@@ -317,7 +322,7 @@ public class FeedHandler extends BaseThingHandler {
         return true;
     }
 
-    public String getValueSafely(String value) {
-        return value == null ? new String() : value;
+    public String getValueSafely(@Nullable String value) {
+        return value == null ? "" : value;
     }
 }
index dd6275891c589770cd1cffaf52c17adcba624456..25834727440f65b87403f47ce1abf6614f1fe813 100644 (file)
@@ -6,8 +6,9 @@
 
        <!-- DEFINITIONS of terms used in the binding: Feed is a XML document used for providing users with frequently updated content.
                The most popular feed formats are RSS and Atom. Entry is a single element in the Feed, that contains reference (link),
-               short description and other information like images, comments and etc. Entry in this binding is abstraction for RSS item
-               element and Atom entry element. -->
+               short
+               description and other information like images, comments and etc. Entry in this binding is abstraction for RSS item element
+               and Atom entry element. -->
 
        <!-- Feed Thing Type -->
        <thing-type id="feed">
                <config-description>
                        <parameter name="URL" type="text" required="true">
                                <label>Feed URL</label>
-                               <description>The URL of the feed</description>
+                               <description>The URL of the feed.</description>
                        </parameter>
 
-                       <!--After the refresh time interval expires, the bindings checks for updates in the Feed, and if the information is not
+                       <!-- After the refresh time interval expires, the bindings checks for updates in the Feed, and if the information is not
                                up to date, updates the feed content stored in the channel -->
-
                        <parameter name="refresh" type="integer">
                                <label>Refresh Time Interval</label>
                                <description>Refresh time interval in minutes.</description>
                                <default>20</default>
                        </parameter>
-
                </config-description>
        </thing-type>
 
        <channel-type id="author" advanced="true">
                <item-type>String</item-type>
                <label>Author</label>
-               <description>The name of the feed author, if author is present</description>
+               <description>The name of the feed author, if author is present.</description>
                <state readOnly="true" pattern="%s"/>
        </channel-type>
 
        <channel-type id="title" advanced="true">
                <item-type>String</item-type>
                <label>Title</label>
-               <description>The title of the feed</description>
+               <description>The title of the feed.</description>
                <state readOnly="true" pattern="%s"/>
        </channel-type>
 
        <channel-type id="description" advanced="true">
                <item-type>String</item-type>
                <label>Description</label>
-               <description>Description of the feed</description>
+               <description>Description of the feed.</description>
                <state readOnly="true" pattern="%s"/>
        </channel-type>
 
        <channel-type id="last-update" advanced="true">
                <item-type>DateTime</item-type>
                <label>Last Update</label>
-               <description>The last update date of the feed</description>
+               <description>The last update date of the feed.</description>
                <state readOnly="true" pattern="%tc %n"/>
        </channel-type>
 
        <channel-type id="number-of-entries" advanced="true">
                <item-type>Number</item-type>
                <label>Number of Entries</label>
-               <description>Number of entries in the feed</description>
+               <description>Number of entries in the feed.</description>
                <state readOnly="true" pattern="%d"/>
        </channel-type>
 
index a34c38b9716c57f32dff8b24c27d063eea7acd6b..fea0dba789ef8bf4fbb5dd338f7ecb368225a64a 100644 (file)
@@ -93,7 +93,7 @@ public class FeedHandlerTest extends JavaOSGiTest {
     /**
      * It is updated from mocked {@link StateChangeListener#stateUpdated() }
      */
-    private StringType currentItemState = null;
+    private StringType currentItemState;
 
     // Required services for the test
     private ManagedThingProvider managedThingProvider;
index 4aca7103dfcd07aae376869113f9e512ae4a34fa..4e858916df35c510ef98dba6be5bacc7b9ff9438 100644 (file)
  */
 package org.openhab.binding.feed.test;
 
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
 /**
  * This interface is used to mark tests that take too much time
  *
- * @author Svilen Valkanov
+ * @author Svilen Valkanov - Initial contribution
  */
+@NonNullByDefault
 public interface SlowTests {
-
 }