/** * 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.internal; import java.security.AccessController; import java.security.PrivilegedAction; import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; import java.util.List; import java.util.Map.Entry; import java.util.Set; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CopyOnWriteArraySet; import org.eclipse.smarthome.config.discovery.DiscoveryListener; import org.eclipse.smarthome.config.discovery.DiscoveryResult; import org.eclipse.smarthome.config.discovery.DiscoveryResultFlag; import org.eclipse.smarthome.config.discovery.DiscoveryService; import org.eclipse.smarthome.config.discovery.DiscoveryServiceCallback; import org.eclipse.smarthome.config.discovery.DiscoveryServiceRegistry; import org.eclipse.smarthome.config.discovery.ExtendedDiscoveryService; import org.eclipse.smarthome.config.discovery.ScanListener; import org.eclipse.smarthome.config.discovery.inbox.Inbox; import org.eclipse.smarthome.config.discovery.inbox.InboxFilterCriteria; import org.eclipse.smarthome.core.common.SafeMethodCaller; import org.eclipse.smarthome.core.thing.Thing; import org.eclipse.smarthome.core.thing.ThingRegistry; import org.eclipse.smarthome.core.thing.ThingTypeUID; import org.eclipse.smarthome.core.thing.ThingUID; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.common.collect.HashMultimap; /** * The {@link DiscoveryServiceRegistryImpl} is a concrete implementation of the {@link DiscoveryServiceRegistry}. * <p> * This implementation tracks any existing {@link DiscoveryService} and registers itself as {@link DiscoveryListener} on * it. * <p> * This implementation does neither handle memory leaks (orphaned listener instances) nor blocked listeners. No * performance optimizations have been done (synchronization). * * @author Michael Grammling - Initial Contribution * @author Kai Kreuzer - Refactored API * @author Andre Fuechsel - Added removeOlderResults * @author Ivaylo Ivanov - Added getMaxScanTimeout * * @see DiscoveryServiceRegistry * @see DiscoveryListener */ public final class DiscoveryServiceRegistryImpl implements DiscoveryServiceRegistry, DiscoveryListener { private HashMultimap<DiscoveryService, DiscoveryResult> cachedResults = HashMultimap.create(); private final class AggregatingScanListener implements ScanListener { private final ScanListener listener; private int finishedDiscoveryServices = 0; private boolean errorOccurred = false; private int numberOfDiscoveryServices; private AggregatingScanListener(int numberOfDiscoveryServices, ScanListener listener) { this.numberOfDiscoveryServices = numberOfDiscoveryServices; this.listener = listener; } @Override public synchronized void onFinished() { synchronized (this) { finishedDiscoveryServices++; logger.debug("Finished {} of {} discovery services.", finishedDiscoveryServices, numberOfDiscoveryServices); if (!errorOccurred && finishedDiscoveryServices == numberOfDiscoveryServices) { if (listener != null) { listener.onFinished(); } } } } @Override public void onErrorOccurred(Exception exception) { synchronized (this) { if (!errorOccurred) { if (listener != null) { listener.onErrorOccurred(exception); } errorOccurred = true; } else { logger.warn("Error occurred while executing discovery service: " + exception.getMessage(), exception); } } } public void reduceNumberOfDiscoveryServices() { synchronized (this) { numberOfDiscoveryServices--; if (!errorOccurred && finishedDiscoveryServices == numberOfDiscoveryServices) { if (listener != null) { listener.onFinished(); } } } } } private List<DiscoveryService> discoveryServices = new CopyOnWriteArrayList<>(); private Set<DiscoveryListener> listeners = new CopyOnWriteArraySet<>(); private final Logger logger = LoggerFactory.getLogger(DiscoveryServiceRegistryImpl.class); private Inbox inbox; private ThingRegistry thingRegistry; private DiscoveryServiceCallback discoveryServiceCallback = new DiscoveryServiceCallback() { @Override public Thing getExistingThing(ThingUID thingUID) { ThingRegistry thingRegistryReference = thingRegistry; if (thingRegistryReference == null) { logger.warn("ThingRegistry not set"); return null; } return thingRegistryReference.get(thingUID); } @Override public DiscoveryResult getExistingDiscoveryResult(ThingUID thingUID) { Inbox inboxReference = inbox; if (inboxReference == null) { logger.warn("Inbox not set"); return null; } List<DiscoveryResult> ret = new ArrayList<>(); ret = inboxReference.get(new InboxFilterCriteria(thingUID, DiscoveryResultFlag.NEW)); if (ret.size() > 0) { return ret.get(0); } else { return null; } } }; @Override public boolean abortScan(ThingTypeUID thingTypeUID) throws IllegalStateException { Set<DiscoveryService> discoveryServicesForThingType = getDiscoveryServices(thingTypeUID); if (discoveryServicesForThingType.isEmpty()) { logger.warn("No discovery service for thing type '{}' found!", thingTypeUID); return false; } return abortScans(discoveryServicesForThingType); } @Override public boolean abortScan(String bindingId) throws IllegalStateException { Set<DiscoveryService> discoveryServicesForBinding = getDiscoveryServices(bindingId); if (discoveryServicesForBinding.isEmpty()) { logger.warn("No discovery service for binding '{}' found!", bindingId); return false; } return abortScans(discoveryServicesForBinding); } @Override public void addDiscoveryListener(DiscoveryListener listener) throws IllegalStateException { synchronized (cachedResults) { Set<Entry<DiscoveryService, DiscoveryResult>> entries = cachedResults.entries(); for (Entry<DiscoveryService, DiscoveryResult> entry : entries) { listener.thingDiscovered(entry.getKey(), entry.getValue()); } } if (listener != null) { this.listeners.add(listener); } } @Override public boolean startScan(ThingTypeUID thingTypeUID, ScanListener listener) throws IllegalStateException { Set<DiscoveryService> discoveryServicesForThingType = getDiscoveryServices(thingTypeUID); if (discoveryServicesForThingType.isEmpty()) { logger.warn("No discovery service for thing type '{}' found!", thingTypeUID); return false; } return startScans(discoveryServicesForThingType, listener); } @Override public boolean startScan(String bindingId, final ScanListener listener) throws IllegalStateException { final Set<DiscoveryService> discoveryServicesForBinding = getDiscoveryServices(bindingId); if (discoveryServicesForBinding.isEmpty()) { logger.warn("No discovery service for binding id '{}' found!", bindingId); return false; } return startScans(discoveryServicesForBinding, listener); } @Override public boolean supportsDiscovery(ThingTypeUID thingTypeUID) { return !getDiscoveryServices(thingTypeUID).isEmpty(); } @Override public boolean supportsDiscovery(String bindingId) { return !getDiscoveryServices(bindingId).isEmpty(); } @Override public List<ThingTypeUID> getSupportedThingTypes() { List<ThingTypeUID> thingTypeUIDs = new ArrayList<>(); for (DiscoveryService discoveryService : this.discoveryServices) { thingTypeUIDs.addAll(discoveryService.getSupportedThingTypes()); } return thingTypeUIDs; } @Override public List<String> getSupportedBindings() { List<String> bindings = new ArrayList<>(); for (DiscoveryService discoveryService : this.discoveryServices) { Collection<ThingTypeUID> supportedThingTypes = discoveryService.getSupportedThingTypes(); for (ThingTypeUID thingTypeUID : supportedThingTypes) { bindings.add(thingTypeUID.getBindingId()); } } return bindings; } @Override public synchronized void removeDiscoveryListener(DiscoveryListener listener) throws IllegalStateException { if (listener != null) { this.listeners.remove(listener); } } @Override public synchronized void thingDiscovered(final DiscoveryService source, final DiscoveryResult result) { synchronized (cachedResults) { cachedResults.remove(source, result); cachedResults.put(source, result); } for (final DiscoveryListener listener : this.listeners) { try { AccessController.doPrivileged(new PrivilegedAction<Void>() { @Override public Void run() { listener.thingDiscovered(source, result); return null; } }); } catch (Exception ex) { logger.error("Cannot notify the DiscoveryListener " + listener.getClass().getName() + " on Thing discovered event!", ex); } } } @Override public synchronized void thingRemoved(final DiscoveryService source, final ThingUID thingUID) { synchronized (cachedResults) { cachedResults.remove(source, thingUID); } for (final DiscoveryListener listener : this.listeners) { try { AccessController.doPrivileged(new PrivilegedAction<Void>() { @Override public Void run() { listener.thingRemoved(source, thingUID); return null; } }); } catch (Exception ex) { logger.error("Cannot notify the DiscoveryListener '" + listener.getClass().getName() + "' on Thing removed event!", ex); } } } @Override public Collection<ThingUID> removeOlderResults(final DiscoveryService source, final long timestamp, final Collection<ThingTypeUID> thingTypeUIDs) { HashSet<ThingUID> removedResults = new HashSet<>(); for (final DiscoveryListener listener : this.listeners) { try { Collection<ThingUID> olderResults = AccessController .doPrivileged(new PrivilegedAction<Collection<ThingUID>>() { @Override public Collection<ThingUID> run() { return listener.removeOlderResults(source, timestamp, thingTypeUIDs); } }); if (olderResults != null) { removedResults.addAll(olderResults); } } catch (Exception ex) { logger.error("Cannot notify the DiscoveryListener '" + listener.getClass().getName() + "' on all things removed event!", ex); } } return removedResults; } private boolean abortScans(Set<DiscoveryService> discoveryServices) { boolean allServicesAborted = true; for (DiscoveryService discoveryService : discoveryServices) { Collection<ThingTypeUID> supportedThingTypes = discoveryService.getSupportedThingTypes(); try { logger.debug("Abort scan for thing types '{}' on '{}'...", supportedThingTypes, discoveryService.getClass().getName()); discoveryService.abortScan(); logger.debug("Scan for thing types '{}' aborted on '{}'.", supportedThingTypes, discoveryService.getClass().getName()); } catch (Exception ex) { logger.error("Cannot abort scan for thing types '" + supportedThingTypes + "' on '" + discoveryService.getClass().getName() + "'!", ex); allServicesAborted = false; } } return allServicesAborted; } private boolean startScans(Set<DiscoveryService> discoveryServices, ScanListener listener) { boolean atLeastOneDiscoveryServiceHasBeenStarted = false; if (discoveryServices.size() > 1) { logger.debug("Trying to start {} scans with an aggregating listener.", discoveryServices.size()); AggregatingScanListener aggregatingScanListener = new AggregatingScanListener(discoveryServices.size(), listener); for (DiscoveryService discoveryService : discoveryServices) { if (startScan(discoveryService, aggregatingScanListener)) { atLeastOneDiscoveryServiceHasBeenStarted = true; } else { logger.debug( "Reducing number of discovery services in aggregating listener, because discovery service failed to start scan."); aggregatingScanListener.reduceNumberOfDiscoveryServices(); } } } else { if (startScan(discoveryServices.iterator().next(), listener)) { atLeastOneDiscoveryServiceHasBeenStarted = true; } } return atLeastOneDiscoveryServiceHasBeenStarted; } private boolean startScan(DiscoveryService discoveryService, ScanListener listener) { Collection<ThingTypeUID> supportedThingTypes = discoveryService.getSupportedThingTypes(); try { logger.debug("Triggering scan for thing types '{}' on '{}'...", supportedThingTypes, discoveryService.getClass().getSimpleName()); discoveryService.startScan(listener); return true; } catch (Exception ex) { logger.error("Cannot trigger scan for thing types '" + supportedThingTypes + "' on '" + discoveryService.getClass().getSimpleName() + "'!", ex); return false; } } private synchronized Set<DiscoveryService> getDiscoveryServices(ThingTypeUID thingTypeUID) throws IllegalStateException { Set<DiscoveryService> discoveryServices = new HashSet<>(); if (thingTypeUID != null) { for (DiscoveryService discoveryService : this.discoveryServices) { Collection<ThingTypeUID> discoveryThingTypes = discoveryService.getSupportedThingTypes(); if (discoveryThingTypes.contains(thingTypeUID)) { discoveryServices.add(discoveryService); } } } return discoveryServices; } private synchronized Set<DiscoveryService> getDiscoveryServices(String bindingId) throws IllegalStateException { Set<DiscoveryService> discoveryServices = new HashSet<>(); for (DiscoveryService discoveryService : this.discoveryServices) { Collection<ThingTypeUID> discoveryThingTypes = discoveryService.getSupportedThingTypes(); for (ThingTypeUID thingTypeUID : discoveryThingTypes) { if (thingTypeUID.getBindingId().equals(bindingId)) { discoveryServices.add(discoveryService); } } } return discoveryServices; } protected void addDiscoveryService(final DiscoveryService discoveryService) { discoveryService.addDiscoveryListener(this); if (discoveryService instanceof ExtendedDiscoveryService) { SafeMethodCaller.call(new SafeMethodCaller.Action<Void>() { @Override public Void call() throws Exception { ((ExtendedDiscoveryService) discoveryService).setDiscoveryServiceCallback(discoveryServiceCallback); return null; } }); } this.discoveryServices.add(discoveryService); } protected void removeDiscoveryService(DiscoveryService discoveryService) { this.discoveryServices.remove(discoveryService); discoveryService.removeDiscoveryListener(this); synchronized (cachedResults) { this.cachedResults.removeAll(discoveryService); } } protected void deactivate() { this.discoveryServices.clear(); this.listeners.clear(); this.cachedResults.clear(); } private int getMaxScanTimeout(Set<DiscoveryService> discoveryServices) { int result = 0; for (DiscoveryService discoveryService : discoveryServices) { if (discoveryService.getScanTimeout() > result) { result = discoveryService.getScanTimeout(); } } return result; } @Override public int getMaxScanTimeout(ThingTypeUID thingTypeUID) { return getMaxScanTimeout(getDiscoveryServices(thingTypeUID)); } @Override public int getMaxScanTimeout(String bindingId) { return getMaxScanTimeout(getDiscoveryServices(bindingId)); } protected void setInbox(Inbox inbox) { this.inbox = inbox; } protected void unsetInbox(Inbox inbox) { this.inbox = null; } protected void setThingRegistry(ThingRegistry thingRegistry) { this.thingRegistry = thingRegistry; } protected void unsetThingRegistry(ThingRegistry thingRegistry) { this.thingRegistry = null; } }