001/*
002 * JGrapes Event Driven Framework
003 * Copyright (C) 2016, 2022  Michael N. Lipp
004 *
005 * This program is free software; you can redistribute it and/or modify it 
006 * under the terms of the GNU Affero General Public License as published by 
007 * the Free Software Foundation; either version 3 of the License, or 
008 * (at your option) any later version.
009 *
010 * This program is distributed in the hope that it will be useful, but 
011 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
012 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License 
013 * for more details.
014 *
015 * You should have received a copy of the GNU Affero General Public License along 
016 * with this program; if not, see <http://www.gnu.org/licenses/>.
017 */
018
019package org.jgrapes.osgi.core;
020
021import java.util.Arrays;
022import java.util.Collections;
023import java.util.HashMap;
024import java.util.List;
025import java.util.Map;
026import java.util.function.Function;
027import static java.util.function.Predicate.not;
028import java.util.stream.Collectors;
029import java.util.stream.Stream;
030import org.jgrapes.core.Channel;
031import org.jgrapes.core.Component;
032import org.jgrapes.core.ComponentFactory;
033import org.jgrapes.core.events.Stop;
034import org.jgrapes.util.ComponentProvider;
035import org.jgrapes.util.events.ConfigurationUpdate;
036import org.osgi.framework.BundleContext;
037import org.osgi.framework.ServiceReference;
038import org.osgi.util.tracker.ServiceTracker;
039import org.osgi.util.tracker.ServiceTrackerCustomizer;
040
041/**
042 * A component that collects all services from the OSGi service registry
043 * which implement the sub-interface of {@link ComponentFactory} 
044 * specified when creating the collector. The collector uses each service 
045 * found to create one or more {@link Component}s that are then attached 
046 * to the component collector instance.
047 * 
048 * Effectively, the component collector leverages OSGi's service layer
049 * to modify the component tree at run-time.
050 * 
051 * This class uses {@link ComponentProvider#setFactories(ComponentFactory...)} 
052 * and {@link ComponentProvider#setPinned(List)} for its implementation.
053 * As it inherits from {@link ComponentProvider}, it automatically
054 * supports the provisioning of additional components through
055 * {@link ConfigurationUpdate} events. If this is not desired, invoke
056 * {@link ComponentProvider#setComponentsEntry(String)} with `null` as
057 * argument. 
058 * 
059 * @param <F> the component factory type
060 */
061@SuppressWarnings({ "PMD.DataflowAnomalyAnalysis",
062    "PMD.DataflowAnomalyAnalysis" })
063public class ComponentCollector<F extends ComponentFactory>
064        extends ComponentProvider implements ServiceTrackerCustomizer<F, F> {
065
066    private static final List<Map<Object, Object>> SINGLE_DEFAULT
067        = Arrays.asList(Collections.emptyMap());
068
069    private BundleContext context;
070    @SuppressWarnings("PMD.SingularField")
071    private ServiceTracker<F, F> serviceTracker;
072    private Function<String, List<Map<Object, Object>>> configurator;
073
074    /**
075     * Creates a collector component that uses a {@link ServiceTracker} 
076     * to monitor the addition and removal of component factories.
077     * 
078     * @see #addingService(ServiceReference)
079     * @see #removedService(ServiceReference, ComponentFactory)
080     *
081     * @param componentChannel this component's channel
082     * @param context the OSGi {@link BundleContext}
083     * @param factoryCls the factory class
084     * @param configurator the function that provides the pinned configurations
085     */
086    public ComponentCollector(
087            Channel componentChannel, BundleContext context,
088            Class<F> factoryCls,
089            Function<String, List<Map<Object, Object>>> configurator) {
090        super(componentChannel);
091        this.context = context;
092        this.configurator = configurator;
093        serviceTracker = new ServiceTracker<>(context, factoryCls, this);
094        serviceTracker.open();
095    }
096
097    /**
098     * Utility constructor that uses each factory to create a single instance,
099     * using an empty map as properties.
100     *
101     * @param componentChannel this component's channel
102     * @param context the bundle context
103     * @param factoryClass the factory class
104     */
105    public ComponentCollector(Channel componentChannel, BundleContext context,
106            Class<F> factoryClass) {
107        this(componentChannel, context, factoryClass, type -> SINGLE_DEFAULT);
108    }
109
110    /**
111     * Whenever a new factory is added, it is used to create component 
112     * instances with this component's channel. First, the `configurator`
113     * passed to the constructor is invoked with the name of the class 
114     * of the component to be created as argument. The list of maps 
115     * returned is then added to the pinned components 
116     * (see {@link ComponentProvider#setPinned(List)}). 
117     * 
118     * The map return from the `configurator` is automatically augmented
119     * with an entry with key {@link BundleContext}.class and the bundle 
120     * context passed to the constructor.
121     *
122     * @see ServiceTrackerCustomizer#addingService(ServiceReference)
123     */
124    @Override
125    public F addingService(ServiceReference<F> reference) {
126        F factory = context.getService(reference);
127        if (factories().containsKey(factory.componentType().getName())) {
128            // Factory for the new component type is already known.
129            return factory;
130        }
131
132        // Add configuration to provider before adding new factory
133        List<Map<?, ?>> newConfigs = Stream.concat(
134            // filter existing configs to avoid duplicates
135            pinned().stream().filter(not(c -> factory.componentType().getName()
136                .equals(c.get(COMPONENT_TYPE)))),
137            // add new configs
138            configurator.apply(
139                factory.componentType().getName()).stream().map(c -> {
140                    // Received may be immutable
141                    var newMap = new HashMap<>(c);
142                    newMap.put(BundleContext.class, context);
143                    if (!c.containsKey(COMPONENT_TYPE)) {
144                        newMap.put(COMPONENT_TYPE,
145                            factory.componentType().getName());
146                    }
147                    return newMap;
148                }))
149            .collect(Collectors.toList());
150        setPinned(newConfigs);
151
152        // Now add factory
153        ComponentFactory[] updatedFactories
154            = Stream.concat(factories().values().stream(), Stream.of(factory))
155                .toArray(s -> new ComponentFactory[s]);
156        setFactories(updatedFactories);
157        return factory;
158    }
159
160    /*
161     * (non-Javadoc)
162     * 
163     * @see org.osgi.util.tracker.ServiceTrackerCustomizer#modifiedService
164     */
165    @Override
166    public void modifiedService(ServiceReference<F> reference, F service) {
167        // Do nothing.
168    }
169
170    /**
171     * Deletes all child components with the type produced by the factory that 
172     * is removed. A (possibly) final {@link Stop} event is send to the
173     * detached subtrees which may be used to deactivate bundles.
174     * 
175     * @see ServiceTrackerCustomizer#removedService(ServiceReference, Object)
176     */
177    @Override
178    @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops")
179    public void removedService(ServiceReference<F> reference, F service) {
180        // Remove factory.
181        ComponentFactory[] updatedFactories = factories().values().stream()
182            .filter(f -> !f.componentType().getName()
183                .equals(service.componentType().getName()))
184            .toArray(s -> new ComponentFactory[s]);
185        setFactories(updatedFactories);
186
187        // Remove configuration
188        List<Map<?, ?>> updatedConfigs = pinned().stream()
189            .filter(not(c -> service.componentType().getName()
190                .equals(c.get(COMPONENT_TYPE))))
191            .collect(Collectors.toList());
192        setPinned(updatedConfigs);
193    }
194}