/** * Copyright (c) 2014-2017 by the respective copyright holders. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html */ package org.eclipse.smarthome.config.discovery; import java.util.Collection; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.Map; import java.util.Set; import java.util.concurrent.CancellationException; import java.util.concurrent.CopyOnWriteArraySet; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; import org.eclipse.smarthome.core.common.ThreadPoolManager; import org.eclipse.smarthome.core.thing.ThingTypeUID; import org.eclipse.smarthome.core.thing.ThingUID; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * The {@link AbstractDiscoveryService} provides methods which handle the {@link DiscoveryListener}s. * * Subclasses do not have to care about adding and removing those listeners. * They can use the protected methods {@link #thingDiscovered(DiscoveryResult)} and {@link #thingRemoved(String)} in * order to notify the registered {@link DiscoveryListener}s. * * @author Oliver Libutzki - Initial contribution * @author Kai Kreuzer - Refactored API * @author Dennis Nobel - Added background discovery configuration through Configuration Admin * @author Andre Fuechsel - Added removeOlderResults */ public abstract class AbstractDiscoveryService implements DiscoveryService { private static final String DISCOVERY_THREADPOOL_NAME = "discovery"; private final Logger logger = LoggerFactory.getLogger(AbstractDiscoveryService.class); static protected final ScheduledExecutorService scheduler = ThreadPoolManager.getScheduledPool(DISCOVERY_THREADPOOL_NAME); private Set<DiscoveryListener> discoveryListeners = new CopyOnWriteArraySet<>(); protected ScanListener scanListener = null; private boolean backgroundDiscoveryEnabled; private Map<ThingUID, DiscoveryResult> cachedResults = new HashMap<>(); final private Set<ThingTypeUID> supportedThingTypes; final private int timeout; private long timestampOfLastScan = 0L; private ScheduledFuture<?> scheduledStop; /** * Creates a new instance of this class with the specified parameters. * * @param supportedThingTypes * the list of Thing types which are supported (can be null) * * @param timeout * the discovery timeout in seconds after which the discovery * service automatically stops its forced discovery process (>= * 0). * * @param backgroundDiscoveryEnabledByDefault * defines, whether the default for this discovery service is to * enable background discovery or not. * * @throws IllegalArgumentException * if the timeout < 0 */ public AbstractDiscoveryService(Set<ThingTypeUID> supportedThingTypes, int timeout, boolean backgroundDiscoveryEnabledByDefault) throws IllegalArgumentException { if (supportedThingTypes == null) { this.supportedThingTypes = Collections.emptySet(); } else { this.supportedThingTypes = supportedThingTypes; } if (timeout < 0) { throw new IllegalArgumentException("The timeout must be >= 0!"); } this.timeout = timeout; this.backgroundDiscoveryEnabled = backgroundDiscoveryEnabledByDefault; } /** * Creates a new instance of this class with the specified parameters. * * @param supportedThingTypes the list of Thing types which are supported (can be null) * * @param timeout the discovery timeout in seconds after which the discovery service * automatically stops its forced discovery process (>= 0). * * @throws IllegalArgumentException if the timeout < 0 */ public AbstractDiscoveryService(Set<ThingTypeUID> supportedThingTypes, int timeout) throws IllegalArgumentException { this(supportedThingTypes, timeout, true); } /** * Creates a new instance of this class with the specified parameters. * * @param timeout the discovery timeout in seconds after which the discovery service * automatically stops its forced discovery process (>= 0). * * @throws IllegalArgumentException if the timeout < 0 */ public AbstractDiscoveryService(int timeout) throws IllegalArgumentException { this(null, timeout); } /** * Returns the list of {@code Thing} types which are supported by the {@link DiscoveryService}. * * @return the list of Thing types which are supported by the discovery service * (not null, could be empty) */ @Override public Set<ThingTypeUID> getSupportedThingTypes() { return this.supportedThingTypes; } /** * Returns the amount of time in seconds after which the discovery service automatically * stops its forced discovery process. * * @return the discovery timeout in seconds (>= 0). */ @Override public int getScanTimeout() { return this.timeout; } @Override public boolean isBackgroundDiscoveryEnabled() { return backgroundDiscoveryEnabled; } @Override public void addDiscoveryListener(DiscoveryListener listener) { synchronized (cachedResults) { for (DiscoveryResult cachedResult : cachedResults.values()) { listener.thingDiscovered(this, cachedResult); } } discoveryListeners.add(listener); } @Override public void removeDiscoveryListener(DiscoveryListener listener) { discoveryListeners.remove(listener); } @Override public synchronized void startScan(ScanListener listener) { synchronized (this) { // we first stop any currently running scan and its scheduled stop // call stopScan(); if (scheduledStop != null) { scheduledStop.cancel(false); scheduledStop = null; } this.scanListener = listener; // schedule an automatic call of stopScan when timeout is reached if (getScanTimeout() > 0) { Runnable runnable = new Runnable() { @Override public void run() { try { stopScan(); } catch (Exception e) { logger.debug("Exception occurred during execution: {}", e.getMessage(), e); } } }; scheduledStop = scheduler.schedule(runnable, getScanTimeout(), TimeUnit.SECONDS); } this.timestampOfLastScan = new Date().getTime(); try { startScan(); } catch (Exception ex) { if (scheduledStop != null) { scheduledStop.cancel(false); scheduledStop = null; } scanListener = null; throw ex; } } } @Override public synchronized void abortScan() { synchronized (this) { if (scheduledStop != null) { scheduledStop.cancel(false); scheduledStop = null; } if (scanListener != null) { Exception e = new CancellationException("Scan has been aborted."); scanListener.onErrorOccurred(e); scanListener = null; } } } /** * This method is called by the {@link #startScan(ScanListener))} implementation of the * {@link AbstractDiscoveryService}. * The abstract class schedules a call of {@link #stopScan()} after {@link #getScanTimeout()} seconds. If this * behavior is not appropriate, the {@link #startScan(ScanListener))} method should be overridden. */ abstract protected void startScan(); /** * This method cleans up after a scan, i.e. it removes listeners and other required operations. */ protected synchronized void stopScan() { if (scanListener != null) { scanListener.onFinished(); scanListener = null; } } /** * Notifies the registered {@link DiscoveryListener}s about a discovered device. * * @param discoveryResult * Holds the information needed to identify the discovered device. */ protected void thingDiscovered(DiscoveryResult discoveryResult) { for (DiscoveryListener discoveryListener : discoveryListeners) { try { discoveryListener.thingDiscovered(this, discoveryResult); } catch (Exception e) { logger.error("An error occurred while calling the discovery listener " + discoveryListener.getClass().getName() + ".", e); } } synchronized (cachedResults) { cachedResults.put(discoveryResult.getThingUID(), discoveryResult); } } /** * Notifies the registered {@link DiscoveryListener}s about a removed device. * * @param thingUID * The UID of the removed thing. */ protected void thingRemoved(ThingUID thingUID) { for (DiscoveryListener discoveryListener : discoveryListeners) { try { discoveryListener.thingRemoved(this, thingUID); } catch (Exception e) { logger.error("An error occurred while calling the discovery listener " + discoveryListener.getClass().getName() + ".", e); } } synchronized (cachedResults) { cachedResults.remove(thingUID); } } /** * Call to remove all results of all {@link #supportedThingTypes} that are * older than the given timestamp. To remove all left over results after a * full scan, this method could be called {@link #getTimestampOfLastScan()} * as timestamp. * * @param timestamp * timestamp, older results will be removed */ protected void removeOlderResults(long timestamp) { removeOlderResults(timestamp, null); } /** * Call to remove all results of the given types that are older than the * given timestamp. To remove all left over results after a full scan, this * method could be called {@link #getTimestampOfLastScan()} as timestamp. * * @param timestamp * timestamp, older results will be removed * @param thingTypeUIDs * collection of {@code ThingType}s, only results of these * {@code ThingType}s will be removed; if {@code null} then * {@link DiscoveryService#getSupportedThingTypes()} will be used * instead */ protected void removeOlderResults(long timestamp, Collection<ThingTypeUID> thingTypeUIDs) { Collection<ThingUID> removedThings = null; if (thingTypeUIDs == null) { thingTypeUIDs = getSupportedThingTypes(); } for (DiscoveryListener discoveryListener : discoveryListeners) { try { removedThings = discoveryListener.removeOlderResults(this, timestamp, thingTypeUIDs); } catch (Exception e) { logger.error("An error occurred while calling the discovery listener " + discoveryListener.getClass().getName() + ".", e); } } if (removedThings != null) { synchronized (cachedResults) { for (ThingUID uid : removedThings) { cachedResults.remove(uid); } } } } /** * Called on component activation, if the implementation of this class is an * OSGi declarative service and does not override the method. The method * implementation calls {@link AbstractDiscoveryService#startBackgroundDiscovery()} if background * discovery is enabled by default and not overridden by the configuration. * * @param configProperties configuration properties */ protected void activate(Map<String, Object> configProperties) { if (configProperties != null) { Object property = configProperties.get(DiscoveryService.CONFIG_PROPERTY_BACKGROUND_DISCOVERY_ENABLED); if (property != null) { this.backgroundDiscoveryEnabled = getAutoDiscoveryEnabled(property); } } if (this.backgroundDiscoveryEnabled) { startBackgroundDiscovery(); logger.debug("Background discovery for discovery service '{}' enabled.", this.getClass().getName()); } } /** * Called when the configuration for the discovery service is changed. If * background discovery should be enabled and is currently disabled, the * method {@link AbstractDiscoveryService#startBackgroundDiscovery()} is * called. If background discovery should be disabled and is currently * enabled, the method {@link AbstractDiscoveryService#stopBackgroundDiscovery()} is called. In * all other cases, nothing happens. * * @param configProperties * configuration properties */ protected void modified(Map<String, Object> configProperties) { if (configProperties != null) { Object property = configProperties.get(DiscoveryService.CONFIG_PROPERTY_BACKGROUND_DISCOVERY_ENABLED); if (property != null) { boolean enabled = getAutoDiscoveryEnabled(property); if (this.backgroundDiscoveryEnabled && !enabled) { stopBackgroundDiscovery(); logger.debug("Background discovery for discovery service '{}' disabled.", this.getClass().getName()); } else if (!this.backgroundDiscoveryEnabled && enabled) { startBackgroundDiscovery(); logger.debug("Background discovery for discovery service '{}' enabled.", this.getClass().getName()); } this.backgroundDiscoveryEnabled = enabled; } } } /** * Called on component deactivation, if the implementation of this class is * an OSGi declarative service and does not override the method. The method * implementation calls {@link AbstractDiscoveryService#stopBackgroundDiscovery()} if background * discovery is enabled at the time of component deactivation. */ protected void deactivate() { if (this.backgroundDiscoveryEnabled) { stopBackgroundDiscovery(); } } /** * Can be overridden to start background discovery logic. This method is * called when {@link AbstractDiscoveryService#setBackgroundDiscoveryEnabled(boolean)} is called with true as * parameter and when the component is being * activated (see {@link AbstractDiscoveryService#activate()}. */ protected void startBackgroundDiscovery() { // can be overridden } /** * Can be overridden to stop background discovery logic. This method is * called when {@link AbstractDiscoveryService#setBackgroundDiscoveryEnabled(boolean)} is called with false as * parameter and when the component is being * deactivated (see {@link AbstractDiscoveryService#deactivate()}. */ protected void stopBackgroundDiscovery() { // can be overridden } /** * Get the timestamp of the last call of {@link #startScan()}. * * @return timestamp as long */ protected long getTimestampOfLastScan() { return timestampOfLastScan; } private boolean getAutoDiscoveryEnabled(Object autoDiscoveryEnabled) { if (autoDiscoveryEnabled instanceof String) { return Boolean.valueOf((String) autoDiscoveryEnabled); } else if (autoDiscoveryEnabled == Boolean.TRUE) { return true; } else { return false; } } }