/** * 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.core.common.registry; import java.util.Collection; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArraySet; import java.util.stream.Collectors; import java.util.stream.Stream; import org.eclipse.smarthome.core.events.Event; import org.eclipse.smarthome.core.events.EventPublisher; import org.osgi.framework.BundleContext; import org.osgi.framework.ServiceReference; import org.osgi.util.tracker.ServiceTracker; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.common.collect.ImmutableList; import com.google.common.collect.Iterables; /** * The {@link AbstractRegistry} is an abstract implementation of the {@link Registry} interface, that can be used as * base class for {@link Registry} implementations. * * @author Dennis Nobel - Initial contribution * @author Stefan Bußweiler - Migration to new event mechanism * @author Victor Toni - provide elements as {@link Stream} * * @param <E> * type of the element */ public abstract class AbstractRegistry<E, K, P extends Provider<E>> implements ProviderChangeListener<E>, Registry<E, K> { private enum EventType { ADDED, REMOVED, UPDATED; } private final Logger logger = LoggerFactory.getLogger(AbstractRegistry.class); private Class<P> providerClazz; private ServiceTracker<P, P> providerTracker; protected Map<Provider<E>, Collection<E>> elementMap = new ConcurrentHashMap<Provider<E>, Collection<E>>(); protected Collection<RegistryChangeListener<E>> listeners = new CopyOnWriteArraySet<RegistryChangeListener<E>>(); protected ManagedProvider<E, K> managedProvider; protected EventPublisher eventPublisher; /** * Constructor. * * @param providerClazz the class of the providers (see e.g. {@link AbstractRegistry#addProvider(Provider)}), null * if no providers should be tracked automatically after activation */ protected AbstractRegistry(final Class<P> providerClazz) { this.providerClazz = providerClazz; } protected void activate(final BundleContext context) { if (providerClazz != null) { /* * The handlers for 'add' and 'remove' the services implementing the provider class (cardinality is * multiple) rely on an active component. * To grant that the add and remove functions are called only for an active component, we use a provider * tracker. */ providerTracker = new ProviderTracker(context, providerClazz); providerTracker.open(); } } protected void deactivate() { if (providerTracker != null) { providerTracker.close(); providerTracker = null; } } private final class ProviderTracker extends ServiceTracker<P, P> { private final BundleContext context; /** * Constructor. * * @param context the bundle context to lookup services * @param providerClazz the class that implementing services should be tracked */ public ProviderTracker(final BundleContext context, final Class<P> providerClazz) { super(context, providerClazz.getName(), null); this.context = context; } @Override public P addingService(ServiceReference<P> reference) { final P service = context.getService(reference); addProvider(service); return service; } @Override public void removedService(ServiceReference<P> reference, P service) { removeProvider(service); } } @Override public void added(Provider<E> provider, E element) { Collection<E> elements = elementMap.get(provider); if (elements != null) { try { onAddElement(element); elements.add(element); notifyListenersAboutAddedElement(element); } catch (Exception ex) { logger.warn("Could not add element: " + ex.getMessage(), ex); } } } @Override public void addRegistryChangeListener(RegistryChangeListener<E> listener) { listeners.add(listener); } @Override public Collection<E> getAll() { return stream().collect(Collectors.toList()); } @Override public Stream<E> stream() { return elementMap.values() // gets a Collection<Collection<E>> .stream() // creates a Stream<Collection<E>> .flatMap(collection -> collection.stream()); // flattens the stream to Stream<E> } @Override public void removed(Provider<E> provider, E element) { Collection<E> elements = elementMap.get(provider); if (elements != null) { try { onRemoveElement(element); elements.remove(element); notifyListenersAboutRemovedElement(element); } catch (Exception ex) { logger.warn("Could not remove element: " + ex.getMessage(), ex); } } } @Override public void removeRegistryChangeListener(RegistryChangeListener<E> listener) { listeners.remove(listener); } @Override public void updated(Provider<E> provider, E oldElement, E element) { Collection<E> elements = elementMap.get(provider); if (elements != null) { try { onUpdateElement(oldElement, element); elements.remove(oldElement); elements.add(element); notifyListenersAboutUpdatedElement(oldElement, element); } catch (Exception ex) { logger.warn("Could not update element: " + ex.getMessage(), ex); } } } @Override public E add(E element) { if (this.managedProvider != null) { this.managedProvider.add(element); return element; } else { throw new IllegalStateException("ManagedProvider is not available"); } } @Override public E update(E element) { if (this.managedProvider != null) { return this.managedProvider.update(element); } else { throw new IllegalStateException("ManagedProvider is not available"); } } @Override public E remove(K key) { if (this.managedProvider != null) { return this.managedProvider.remove(key); } else { throw new IllegalStateException("ManagedProvider is not available"); } } protected void notifyListeners(E oldElement, E element, EventType eventType) { for (RegistryChangeListener<E> listener : this.listeners) { try { switch (eventType) { case ADDED: listener.added(element); break; case REMOVED: listener.removed(element); break; case UPDATED: listener.updated(oldElement, element); break; default: break; } } catch (Throwable throwable) { logger.error("Could not inform the listener '" + listener + "' about the '" + eventType.name() + "' event!: " + throwable.getMessage(), throwable); } } } protected void notifyListeners(E element, EventType eventType) { notifyListeners(null, element, eventType); } protected void notifyListenersAboutAddedElement(E element) { notifyListeners(element, EventType.ADDED); } protected void notifyListenersAboutRemovedElement(E element) { notifyListeners(element, EventType.REMOVED); } protected void notifyListenersAboutUpdatedElement(E oldElement, E element) { notifyListeners(oldElement, element, EventType.UPDATED); } protected void addProvider(Provider<E> provider) { // only add this provider if it does not already exist if (!elementMap.containsKey(provider)) { Collection<E> elementsOfProvider = provider.getAll(); Collection<E> elements = new CopyOnWriteArraySet<E>(); provider.addProviderChangeListener(this); elementMap.put(provider, elements); for (E element : elementsOfProvider) { try { onAddElement(element); elements.add(element); notifyListenersAboutAddedElement(element); } catch (Exception ex) { logger.warn("Could not add element: " + ex.getMessage(), ex); } } logger.debug("Provider '{}' has been added.", provider.getClass().getName()); } } protected void setManagedProvider(ManagedProvider<E, K> provider) { managedProvider = provider; } /** * This method is called before an element is added. The implementing class * can override this method to perform initialization logic or check the * validity of the element. * * <p> * If the method throws an {@link IllegalArgumentException} the element will not be added. * <p> * * @param element * element to be added * @throws IllegalArgumentException * if the element is invalid and should not be added */ protected void onAddElement(E element) throws IllegalArgumentException { // can be overridden by sub classes } /** * This method is called before an element is removed. The implementing * class can override this method to perform specific logic. * * @param element * element to be removed */ protected void onRemoveElement(E element) { // can be overridden by sub classes } /** * This method is called before an element is updated. The implementing * class can override this method to perform specific logic or check the * validity of the updated element. * * @param oldElement * old element (before update) * @param element * updated element (after update) * * <p> * If the method throws an {@link IllegalArgumentException} the element will not be updated. * <p> * * @throws IllegalArgumentException * if the updated element is invalid and should not be updated */ protected void onUpdateElement(E oldElement, E element) throws IllegalArgumentException { // can be overridden by sub classes } protected void removeProvider(Provider<E> provider) { if (elementMap.containsKey(provider)) { for (E element : elementMap.get(provider)) { try { onRemoveElement(element); notifyListenersAboutRemovedElement(element); } catch (Exception ex) { logger.warn("Could not remove element: " + ex.getMessage(), ex); } } elementMap.remove(provider); provider.removeProviderChangeListener(this); logger.debug("Provider '{}' has been removed.", provider.getClass().getSimpleName()); } } protected void removeManagedProvider(ManagedProvider<E, K> managedProvider) { this.managedProvider = null; } protected void setEventPublisher(EventPublisher eventPublisher) { this.eventPublisher = eventPublisher; } protected void unsetEventPublisher(EventPublisher eventPublisher) { this.eventPublisher = null; } /** * This method can be used in a subclass in order to post events through the Eclipse SmartHome events bus. A common * use case is to notify event subscribers about an element which has been added/removed/updated to the registry. * * @param event the event */ protected void postEvent(Event event) { if (eventPublisher != null) { try { eventPublisher.post(event); } catch (Exception ex) { logger.error("Could not post event of type '" + event.getType() + "'.", ex); } } } }