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}