/** * 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.net.URI; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CopyOnWriteArraySet; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; import org.eclipse.smarthome.config.core.ConfigDescription; import org.eclipse.smarthome.config.core.ConfigDescriptionParameter; import org.eclipse.smarthome.config.core.ConfigDescriptionRegistry; import org.eclipse.smarthome.config.core.Configuration; 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.DiscoveryServiceRegistry; import org.eclipse.smarthome.config.discovery.inbox.Inbox; import org.eclipse.smarthome.config.discovery.inbox.InboxFilterCriteria; import org.eclipse.smarthome.config.discovery.inbox.InboxListener; import org.eclipse.smarthome.config.discovery.inbox.events.InboxEventFactory; import org.eclipse.smarthome.core.common.ThreadPoolManager; import org.eclipse.smarthome.core.events.EventPublisher; import org.eclipse.smarthome.core.storage.Storage; import org.eclipse.smarthome.core.storage.StorageService; import org.eclipse.smarthome.core.thing.Bridge; import org.eclipse.smarthome.core.thing.ManagedThingProvider; import org.eclipse.smarthome.core.thing.Thing; import org.eclipse.smarthome.core.thing.ThingRegistry; import org.eclipse.smarthome.core.thing.ThingRegistryChangeListener; import org.eclipse.smarthome.core.thing.ThingTypeUID; import org.eclipse.smarthome.core.thing.ThingUID; import org.eclipse.smarthome.core.thing.binding.ThingFactory; import org.eclipse.smarthome.core.thing.binding.ThingHandlerFactory; import org.eclipse.smarthome.core.thing.type.ThingType; import org.eclipse.smarthome.core.thing.type.ThingTypeRegistry; import org.osgi.service.component.ComponentContext; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * The {@link PersistentInbox} class is a concrete implementation of the {@link Inbox}. * <p> * This implementation uses the {@link DiscoveryServiceRegistry} to register itself as {@link DiscoveryListener} to * receive {@link DiscoveryResult} objects automatically from {@link DiscoveryService}s. * <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 Dennis Nobel - Added automated removing of entries * @author Michael Grammling - Added dynamic configuration updates * @author Dennis Nobel - Added persistence support * @author Andre Fuechsel - Added removeOlderResults * @author Christoph Knauf - Added removeThingsForBridge and getPropsAndConfigParams * */ public final class PersistentInbox implements Inbox, DiscoveryListener, ThingRegistryChangeListener { /** * Internal enumeration to identify the correct type of the event to be fired. */ private enum EventType { added, removed, updated } private class TimeToLiveCheckingThread implements Runnable { private PersistentInbox inbox; public TimeToLiveCheckingThread(PersistentInbox inbox) { this.inbox = inbox; } @Override public void run() { long now = new Date().getTime(); for (DiscoveryResult result : inbox.getAll()) { if (isResultExpired(result, now)) { logger.debug("Inbox entry for thing {} is expired and will be removed", result.getThingUID()); remove(result.getThingUID()); } } } private boolean isResultExpired(DiscoveryResult result, long now) { if (result.getTimeToLive() == DiscoveryResult.TTL_UNLIMITED) { return false; } return (result.getTimestamp() + result.getTimeToLive() * 1000 < now); } } private final Logger logger = LoggerFactory.getLogger(PersistentInbox.class); private Set<InboxListener> listeners = new CopyOnWriteArraySet<>(); private DiscoveryServiceRegistry discoveryServiceRegistry; private ThingRegistry thingRegistry; private ManagedThingProvider managedThingProvider; private ThingTypeRegistry thingTypeRegistry; private ConfigDescriptionRegistry configDescRegistry; private Storage<DiscoveryResult> discoveryResultStorage; private Map<DiscoveryResult, Class<?>> resultDiscovererMap = new ConcurrentHashMap<>(); private ScheduledFuture<?> timeToLiveChecker; private EventPublisher eventPublisher; private List<ThingHandlerFactory> thingHandlerFactories = new CopyOnWriteArrayList<>(); @Override public Thing approve(ThingUID thingUID, String label) { if (thingUID == null) { throw new IllegalArgumentException("Thing UID must not be null"); } List<DiscoveryResult> results = get(new InboxFilterCriteria(thingUID, null)); if (results.isEmpty()) { throw new IllegalArgumentException("No Thing with UID " + thingUID.getAsString() + " in inbox"); } DiscoveryResult result = results.get(0); final Map<String, String> properties = new HashMap<>(); final Map<String, Object> configParams = new HashMap<>(); getPropsAndConfigParams(result, properties, configParams); final Configuration config = new Configuration(configParams); ThingTypeUID thingTypeUID = result.getThingTypeUID(); Thing newThing = ThingFactory.createThing(thingUID, config, properties, result.getBridgeUID(), thingTypeUID, this.thingHandlerFactories); if (newThing == null) { logger.warn("Cannot create thing. No binding found that supports creating a thing" + " of type {}.", thingTypeUID); return null; } if (label != null && !label.isEmpty()) { newThing.setLabel(label); } else { newThing.setLabel(result.getLabel()); } addThingSafely(newThing); return newThing; } @Override public synchronized boolean add(DiscoveryResult result) throws IllegalStateException { if (result != null) { ThingUID thingUID = result.getThingUID(); Thing thing = this.thingRegistry.get(thingUID); if (thing == null) { DiscoveryResult inboxResult = get(thingUID); if (inboxResult == null) { discoveryResultStorage.put(result.getThingUID().toString(), result); notifyListeners(result, EventType.added); logger.info("Added new thing '{}' to inbox.", thingUID); return true; } else { if (inboxResult instanceof DiscoveryResultImpl) { DiscoveryResultImpl resultImpl = (DiscoveryResultImpl) inboxResult; resultImpl.synchronize(result); discoveryResultStorage.put(result.getThingUID().toString(), resultImpl); notifyListeners(resultImpl, EventType.updated); logger.debug("Updated discovery result for '{}'.", thingUID); return true; } else { logger.warn("Cannot synchronize result with implementation class '{}'.", inboxResult.getClass().getName()); } } } else { logger.debug("Discovery result with thing '{}' not added as inbox entry." + " It is already present as thing in the ThingRegistry.", thingUID); boolean updated = synchronizeConfiguration(result.getProperties(), thing.getConfiguration()); if (updated) { logger.debug("The configuration for thing '{}' is updated...", thingUID); this.managedThingProvider.update(thing); } } } return false; } private boolean synchronizeConfiguration(Map<String, Object> properties, Configuration config) { boolean configUpdated = false; final Set<Map.Entry<String, Object>> propertySet = properties.entrySet(); for (Map.Entry<String, Object> propertyEntry : propertySet) { final String propertyKey = propertyEntry.getKey(); final Object propertyValue = propertyEntry.getValue(); // Check if the key is present in the configuration. if (!config.containsKey(propertyKey)) { continue; } // If the value is equal to the one of the configuration, there is nothing to do. if (Objects.equals(propertyValue, config.get(propertyKey))) { continue; } // - the given key is part of the configuration // - the values differ // update value config.put(propertyKey, propertyValue); configUpdated = true; } return configUpdated; } @Override public void addInboxListener(InboxListener listener) throws IllegalStateException { if (listener != null) { this.listeners.add(listener); } } @Override public List<DiscoveryResult> get(InboxFilterCriteria criteria) throws IllegalStateException { List<DiscoveryResult> filteredEntries = new ArrayList<>(); for (DiscoveryResult discoveryResult : this.discoveryResultStorage.getValues()) { if (matchFilter(discoveryResult, criteria)) { filteredEntries.add(discoveryResult); } } return filteredEntries; } @Override public List<DiscoveryResult> getAll() { return get((InboxFilterCriteria) null); } @Override public synchronized boolean remove(ThingUID thingUID) throws IllegalStateException { if (thingUID != null) { DiscoveryResult discoveryResult = get(thingUID); if (discoveryResult != null) { if (!isInRegistry(thingUID)) { removeResultsForBridge(thingUID); } resultDiscovererMap.remove(discoveryResult); this.discoveryResultStorage.remove(thingUID.toString()); notifyListeners(discoveryResult, EventType.removed); return true; } } return false; } @Override public void removeInboxListener(InboxListener listener) throws IllegalStateException { if (listener != null) { this.listeners.remove(listener); } } @Override public void thingDiscovered(DiscoveryService source, DiscoveryResult result) { if (add(result)) { resultDiscovererMap.put(result, source.getClass()); } } @Override public void thingRemoved(DiscoveryService source, ThingUID thingUID) { remove(thingUID); } @Override public Collection<ThingUID> removeOlderResults(DiscoveryService source, long timestamp, Collection<ThingTypeUID> thingTypeUIDs) { HashSet<ThingUID> removedThings = new HashSet<>(); for (DiscoveryResult discoveryResult : getAll()) { Class<?> discoverer = resultDiscovererMap.get(discoveryResult); if (thingTypeUIDs.contains(discoveryResult.getThingTypeUID()) && discoveryResult.getTimestamp() < timestamp && (discoverer == null || source.getClass() == discoverer)) { ThingUID thingUID = discoveryResult.getThingUID(); removedThings.add(thingUID); remove(thingUID); logger.debug("Removed {} from inbox because it was older than {}", thingUID, new Date(timestamp)); } } return removedThings; } @Override public void added(Thing thing) { if (remove(thing.getUID())) { logger.debug( "Discovery result removed from inbox, because it was added as a Thing" + " to the ThingRegistry."); } } @Override public void removed(Thing thing) { if (thing instanceof Bridge) { removeResultsForBridge(thing.getUID()); } } @Override public void updated(Thing oldThing, Thing thing) { // Attention: Do NOT fire an event back to the ThingRegistry otherwise circular // events are fired! This event was triggered by the 'add(DiscoveryResult)' // method within this class. -> NOTHING TO DO HERE } @Override public void setFlag(ThingUID thingUID, DiscoveryResultFlag flag) { DiscoveryResult result = get(thingUID); if (result instanceof DiscoveryResultImpl) { DiscoveryResultImpl resultImpl = (DiscoveryResultImpl) result; resultImpl.setFlag((flag == null) ? DiscoveryResultFlag.NEW : flag); discoveryResultStorage.put(resultImpl.getThingUID().toString(), resultImpl); notifyListeners(resultImpl, EventType.updated); } else { logger.warn("Cannot set flag for result of instance type '{}'", result.getClass().getName()); } } /** * Returns the {@link DiscoveryResult} in this {@link Inbox} associated with * the specified {@code Thing} ID, or {@code null}, if no {@link DiscoveryResult} could be found. * * @param thingId * the Thing ID to which the discovery result should be returned * * @return the discovery result associated with the specified Thing ID, or * null, if no discovery result could be found */ private DiscoveryResult get(ThingUID thingUID) { if (thingUID != null) { return discoveryResultStorage.get(thingUID.toString()); } return null; } private boolean matchFilter(DiscoveryResult discoveryResult, InboxFilterCriteria criteria) { if (criteria != null) { String bindingId = criteria.getBindingId(); if ((bindingId != null) && (!bindingId.isEmpty())) { if (!discoveryResult.getBindingId().equals(bindingId)) { return false; } } ThingTypeUID thingTypeUID = criteria.getThingTypeUID(); if (thingTypeUID != null) { if (!discoveryResult.getThingTypeUID().equals(thingTypeUID)) { return false; } } ThingUID thingUID = criteria.getThingUID(); if (thingUID != null) { if (!discoveryResult.getThingUID().equals(thingUID)) { return false; } } DiscoveryResultFlag flag = criteria.getFlag(); if (flag != null) { if (discoveryResult.getFlag() != flag) { return false; } } } return true; } private void notifyListeners(DiscoveryResult result, EventType type) { for (InboxListener listener : this.listeners) { try { switch (type) { case added: listener.thingAdded(this, result); break; case removed: listener.thingRemoved(this, result); break; case updated: listener.thingUpdated(this, result); break; } } catch (Exception ex) { String errorMessage = String.format("Cannot notify the InboxListener '%s' about a Thing %s event!", listener.getClass().getName(), type.name()); logger.error(errorMessage, ex); } } postEvent(result, type); } private void postEvent(DiscoveryResult result, EventType eventType) { if (eventPublisher != null) { try { switch (eventType) { case added: eventPublisher.post(InboxEventFactory.createAddedEvent(result)); break; case removed: eventPublisher.post(InboxEventFactory.createRemovedEvent(result)); break; case updated: eventPublisher.post(InboxEventFactory.createUpdatedEvent(result)); break; default: break; } } catch (Exception ex) { logger.error("Could not post event of type '" + eventType.name() + "'.", ex); } } } private boolean isInRegistry(ThingUID thingUID) { if (thingRegistry.get(thingUID) == null) { return false; } return true; } private void removeResultsForBridge(ThingUID bridgeUID) { for (ThingUID thingUID : getResultsForBridge(bridgeUID)) { DiscoveryResult discoveryResult = get(thingUID); if (discoveryResult != null) { this.discoveryResultStorage.remove(thingUID.toString()); notifyListeners(discoveryResult, EventType.removed); } } } private List<ThingUID> getResultsForBridge(ThingUID bridgeUID) { List<ThingUID> thingsForBridge = new ArrayList<>(); for (DiscoveryResult result : discoveryResultStorage.getValues()) { if (bridgeUID.equals(result.getBridgeUID())) { thingsForBridge.add(result.getThingUID()); } } return thingsForBridge; } /** * Get the properties and configuration parameters for the thing with the given {@link DiscoveryResult}. * * @param discoveryResult the DiscoveryResult * @param props the location the properties should be stored to. * @param configParams the location the configuration parameters should be stored to. */ private void getPropsAndConfigParams(final DiscoveryResult discoveryResult, final Map<String, String> props, final Map<String, Object> configParams) { final Set<String> paramNames = getConfigDescParamNames(discoveryResult); final Map<String, Object> resultProps = discoveryResult.getProperties(); for (String resultKey : resultProps.keySet()) { if (paramNames.contains(resultKey)) { configParams.put(resultKey, resultProps.get(resultKey)); } else { props.put(resultKey, String.valueOf(resultProps.get(resultKey))); } } } private Set<String> getConfigDescParamNames(DiscoveryResult discoveryResult) { List<ConfigDescriptionParameter> confDescParams = getConfigDescParams(discoveryResult); Set<String> paramNames = new HashSet<>(); for (ConfigDescriptionParameter param : confDescParams) { paramNames.add(param.getName()); } return paramNames; } private List<ConfigDescriptionParameter> getConfigDescParams(DiscoveryResult discoveryResult) { ThingType type = thingTypeRegistry.getThingType(discoveryResult.getThingTypeUID()); if (type != null && type.getConfigDescriptionURI() != null) { URI descURI = type.getConfigDescriptionURI(); ConfigDescription desc = configDescRegistry.getConfigDescription(descURI); if (desc != null) { return desc.getParameters(); } } return Collections.emptyList(); } private void addThingSafely(Thing thing) { ThingUID thingUID = thing.getUID(); if (thingRegistry.get(thingUID) != null) { thingRegistry.remove(thingUID); } thingRegistry.add(thing); } protected void activate(ComponentContext componentContext) { this.timeToLiveChecker = ThreadPoolManager.getScheduledPool("discovery") .scheduleWithFixedDelay(new TimeToLiveCheckingThread(this), 0, 30, TimeUnit.SECONDS); this.discoveryServiceRegistry.addDiscoveryListener(this); } void setTimeToLiveCheckingInterval(int interval) { this.timeToLiveChecker.cancel(true); this.timeToLiveChecker = ThreadPoolManager.getScheduledPool("discovery") .scheduleWithFixedDelay(new TimeToLiveCheckingThread(this), 0, interval, TimeUnit.SECONDS); } protected void deactivate(ComponentContext componentContext) { this.discoveryServiceRegistry.removeDiscoveryListener(this); this.listeners.clear(); this.timeToLiveChecker.cancel(true); } protected void setDiscoveryServiceRegistry(DiscoveryServiceRegistry discoveryServiceRegistry) { this.discoveryServiceRegistry = discoveryServiceRegistry; } protected void setThingRegistry(ThingRegistry thingRegistry) { this.thingRegistry = thingRegistry; this.thingRegistry.addRegistryChangeListener(this); } protected void setManagedThingProvider(ManagedThingProvider thingProvider) { this.managedThingProvider = thingProvider; } protected void unsetDiscoveryServiceRegistry(DiscoveryServiceRegistry discoveryServiceRegistry) { this.discoveryServiceRegistry = null; } protected void unsetThingRegistry(ThingRegistry thingRegistry) { this.thingRegistry.removeRegistryChangeListener(this); this.thingRegistry = null; } protected void unsetManagedThingProvider(ManagedThingProvider thingProvider) { this.managedThingProvider = null; } protected void setStorageService(StorageService storageService) { this.discoveryResultStorage = storageService.getStorage(DiscoveryResult.class.getName(), this.getClass().getClassLoader()); } protected void unsetStorageService(StorageService storageService) { this.discoveryResultStorage = null; } protected void setEventPublisher(EventPublisher eventPublisher) { this.eventPublisher = eventPublisher; } protected void unsetEventPublisher(EventPublisher eventPublisher) { this.eventPublisher = null; } protected void setThingTypeRegistry(ThingTypeRegistry thingTypeRegistry) { this.thingTypeRegistry = thingTypeRegistry; } protected void unsetThingTypeRegistry(ThingTypeRegistry thingTypeRegistry) { this.thingTypeRegistry = null; } protected void setConfigDescriptionRegistry(ConfigDescriptionRegistry configDescriptionRegistry) { this.configDescRegistry = configDescriptionRegistry; } protected void unsetConfigDescriptionRegistry(ConfigDescriptionRegistry configDescriptionRegistry) { this.configDescRegistry = null; } protected void addThingHandlerFactory(ThingHandlerFactory thingHandlerFactory) { this.thingHandlerFactories.add(thingHandlerFactory); } protected void removeThingHandlerFactory(ThingHandlerFactory thingHandlerFactory) { this.thingHandlerFactories.remove(thingHandlerFactory); } }