001/*
002 * JGrapes Event Driven Framework
003 * Copyright (C) 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 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 General Public License 
013 * for more details.
014 * 
015 * You should have received a copy of the GNU General Public License along 
016 * with this program; if not, see <http://www.gnu.org/licenses/>.
017 */
018
019package org.jgrapes.util;
020
021import java.util.Arrays;
022import java.util.Collection;
023import java.util.Collections;
024import java.util.HashMap;
025import java.util.LinkedList;
026import java.util.List;
027import java.util.Map;
028import java.util.Objects;
029import java.util.Optional;
030import java.util.Set;
031import java.util.function.Function;
032import java.util.stream.Collectors;
033import org.jgrapes.core.Channel;
034import org.jgrapes.core.Component;
035import org.jgrapes.core.ComponentFactory;
036import org.jgrapes.core.Components;
037import org.jgrapes.core.Event;
038import org.jgrapes.core.Manager;
039import org.jgrapes.core.annotation.Handler;
040import org.jgrapes.util.events.ConfigurationUpdate;
041
042/**
043 * Provides child components dynamically using {@link ComponentFactory}s.
044 * 
045 * An instance is configured with a collection of {@link ComponentFactory}s
046 * (see {@link #setFactories(ComponentFactory...)}) and component 
047 * configurations (see {@link #setPinned(List)} and 
048 * {@link #onConfigurationUpdate(ConfigurationUpdate)}). 
049 * 
050 * For each configuration that references a known factory, a component is 
051 * created and attached to this component provider as child.
052 * 
053 * @since 1.3
054 */
055@SuppressWarnings("PMD.DataflowAnomalyAnalysis")
056public class ComponentProvider extends Component {
057
058    /** The entry name for the component's type. */
059    public static final String COMPONENT_TYPE = "componentType";
060    /** The entry name for the component's name. */
061    public static final String COMPONENT_NAME = "name";
062
063    private String componentsEntry = "components";
064    private Map<String, ComponentFactory> factoryByType
065        = Collections.emptyMap();
066    private List<Map<Object, Object>> currentConfig = Collections.emptyList();
067    private List<Map<Object, Object>> pinnedConfigurations
068        = Collections.emptyList();
069
070    /**
071     * Creates a new component with its channel set to this object. 
072     */
073    public ComponentProvider() {
074        this(Channel.SELF);
075    }
076
077    /**
078     * Creates a new component with its channel set to the given 
079     * channel. 
080     *
081     * @param componentChannel the channel that the component's
082     * handlers listen on by default and that 
083     * {@link Manager#fire(Event, Channel...)} sends the event to
084     */
085    public ComponentProvider(Channel componentChannel) {
086        super(componentChannel);
087    }
088
089    /**
090     * Sets the name of the entry in this component's configuration
091     * information (as returned by 
092     * {@link #providerConfiguration(ConfigurationUpdate)})
093     * that holds the information about the components to be provided.
094     * Defaults to "components". 
095     * 
096     * If set to `null`, handling {@link ConfigurationUpdate} events 
097     * is effectively disabled (unless 
098     * {@link #componentConfigurations(ConfigurationUpdate)}
099     * is overridden by a method that ignores the setting).
100     *
101     * @param name the name of the entry
102     * @return the component provider for easy chaining
103     */
104    public ComponentProvider setComponentsEntry(String name) {
105        this.componentsEntry = name;
106        return this;
107    }
108
109    /**
110     * Sets the factories that this provider knows about. Only
111     * configurations with a component type that matches one
112     * of the factories are handled by this provider.
113     *
114     * @param factories the factories
115     * @return the component provider for easy chaining
116     */
117    public ComponentProvider setFactories(ComponentFactory... factories) {
118        factoryByType = Collections.unmodifiableMap(Arrays.stream(factories)
119            .collect(Collectors
120                .toMap(f -> f.componentType().getName(), Function.identity(),
121                    (a, b) -> b)));
122        synchronize(currentConfig);
123        return this;
124    }
125
126    /**
127     * Gets the factories as a map, indexed by component type.
128     *
129     * @return the factories
130     */
131    public Map<String, ComponentFactory> factories() {
132        return factoryByType;
133    }
134
135    /**
136     * Sets the pinned configurations. Components provided due to
137     * these configurations exist independent of any information passed by
138     * {@link ConfigurationUpdate} events.
139     *
140     * @param pinnedConfigurations the configurations to be pinned
141     * @return the component provider for easy chaining
142     */
143    @SuppressWarnings("unchecked")
144    public ComponentProvider setPinned(List<Map<?, ?>> pinnedConfigurations) {
145        this.pinnedConfigurations
146            = Collections.unmodifiableList(pinnedConfigurations.stream()
147                .map(c -> Collections
148                    .unmodifiableMap(new HashMap<>((Map<Object, Object>) c)))
149                .collect(Collectors.toList()));
150        synchronize(currentConfig);
151        return this;
152    }
153
154    /**
155     * Gets the pinned configurations.
156     *
157     * @return the pinned configurations
158     */
159    public List<Map<Object, Object>> pinned() {
160        return pinnedConfigurations;
161    }
162
163    /**
164     * Selects configuration information targeted at this component
165     * from the event. The default implementation invokes 
166     * {@link ConfigurationUpdate#structured(String)} with this
167     * component's path to obtain the information. Called by 
168     * {@link #componentConfigurations(ConfigurationUpdate)}.
169     *
170     * @param evt the event
171     * @return the configuration information as provided by
172     * {@link ConfigurationUpdate#structured(String)} if it exists
173     */
174    protected Optional<Map<String, Object>>
175            providerConfiguration(ConfigurationUpdate evt) {
176        return evt.structured(componentPath());
177    }
178
179    /**
180     * Retrieves the configurations for components to be provided
181     * from an entry in a {@link ConfigurationUpdate} event.
182     * Overriding this method enables derived classes to fully 
183     * control how this information is retrieved from the
184     * {@link ConfigurationUpdate} event.
185     * 
186     * This implementation of the method calls 
187     * {@link #providerConfiguration(ConfigurationUpdate)} to obtain
188     * all configuration information targeted at this component provider.
189     * It then uses the configured entry 
190     * (see {@link #setComponentsEntry(String)}) to retrieve the information
191     * about the components to be provided.
192     * 
193     * The method must ensure that the result is a collection
194     * of maps, where each map has at least entries with
195     * keys "componentType" and "name", each associated with a
196     * value of type {@link String}.
197     * 
198     * @param evt the event
199     * @return the collection
200     */
201    @SuppressWarnings("PMD.AvoidDuplicateLiterals")
202    protected List<Map<Object, Object>>
203            componentConfigurations(ConfigurationUpdate evt) {
204        if (componentsEntry == null) {
205            // Shortcut, avoids call to provider configuration.
206            return Collections.emptyList();
207        }
208        return providerConfiguration(evt)
209            .map(conf -> conf.get(componentsEntry))
210            .filter(Collection.class::isInstance).map(c -> (Collection<?>) c)
211            .orElse(Collections.emptyList()).stream()
212            .filter(Map.class::isInstance).map(c -> (Map<?, ?>) c)
213            .filter(c -> c.keySet()
214                .containsAll(Set.of(COMPONENT_TYPE, COMPONENT_NAME))
215                && String.class.isInstance(c.get(COMPONENT_TYPE))
216                && String.class.isInstance(c.get(COMPONENT_NAME)))
217            .map(c -> {
218                @SuppressWarnings("unchecked") // Checked for relevant entries
219                var casted = (Map<Object, Object>) c;
220                return casted;
221            })
222            .collect(Collectors.toList());
223    }
224
225    /**
226     * Uses the information from the event to configure the
227     * provided components.
228     * 
229     * @see #componentConfigurations(ConfigurationUpdate)
230     * @param evt the event
231     */
232    @Handler
233    public void onConfigurationUpdate(ConfigurationUpdate evt) {
234        synchronize(componentConfigurations(evt));
235    }
236
237    private void synchronize(List<Map<Object, Object>> requested) {
238        synchronized (this) {
239            // Calculate starters for to be added/to be removed
240            var toBeAdded = new LinkedList<>(requested);
241            toBeAdded.addAll(pinnedConfigurations);
242            var toBeRemoved = children().stream()
243                .map(c -> Components.manager(c))
244                .collect(Collectors.toCollection(LinkedList::new));
245
246            // Don't attempt to add something that we have no factory for.
247            toBeAdded = toBeAdded.stream()
248                .filter(c -> factoryByType.containsKey(c.get(COMPONENT_TYPE)))
249                .collect(Collectors.toCollection(LinkedList::new));
250
251            // Remove the intersection of "to be added" and "to be removed"
252            // from both, thus leaving what their names say.
253            for (var childIter = toBeRemoved.iterator(); childIter.hasNext();) {
254                var child = childIter.next();
255                @SuppressWarnings("PMD.DataflowAnomalyAnalysis")
256                var childComp = child.component().getClass().getName();
257                var childName = child.name();
258                for (var confIter = toBeAdded.iterator();
259                        confIter.hasNext();) {
260                    var config = confIter.next();
261                    var confComp = config.get(COMPONENT_TYPE);
262                    var confName = config.get(COMPONENT_NAME);
263                    if (confComp.equals(childComp)
264                        && Objects.equals(childName, confName)) {
265                        confIter.remove();
266                        childIter.remove();
267                    }
268                }
269            }
270
271            // Update children
272            for (var child : toBeRemoved) {
273                child.detach();
274            }
275            toBeAdded.stream().map(config -> {
276                return factoryByType.get(config.get(COMPONENT_TYPE))
277                    .create(channel(), config).map(
278                        c -> ComponentFactory.setStandardProperties(c, config))
279                    .stream();
280            }).flatMap(Function.identity())
281                .forEach(component -> attach(component));
282
283            // Save configuration as current
284            currentConfig = requested;
285        }
286    }
287
288}