001/*
002 *  Copyright 2012 Anyware Services
003 *
004 *  Licensed under the Apache License, Version 2.0 (the "License");
005 *  you may not use this file except in compliance with the License.
006 *  You may obtain a copy of the License at
007 *
008 *      http://www.apache.org/licenses/LICENSE-2.0
009 *
010 *  Unless required by applicable law or agreed to in writing, software
011 *  distributed under the License is distributed on an "AS IS" BASIS,
012 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 *  See the License for the specific language governing permissions and
014 *  limitations under the License.
015 */
016package org.ametys.runtime.plugin.component;
017
018import java.util.Collections;
019import java.util.HashMap;
020import java.util.LinkedHashMap;
021import java.util.Map;
022
023import org.apache.avalon.excalibur.pool.Poolable;
024import org.apache.avalon.framework.activity.Disposable;
025import org.apache.avalon.framework.activity.Initializable;
026import org.apache.avalon.framework.activity.Startable;
027import org.apache.avalon.framework.component.ComponentException;
028import org.apache.avalon.framework.configuration.Configuration;
029import org.apache.avalon.framework.container.ContainerUtil;
030import org.apache.avalon.framework.context.Context;
031import org.apache.avalon.framework.context.Contextualizable;
032import org.apache.avalon.framework.parameters.Parameterizable;
033import org.apache.avalon.framework.parameters.Parameters;
034import org.apache.avalon.framework.service.ServiceManager;
035import org.apache.avalon.framework.service.Serviceable;
036import org.apache.avalon.framework.thread.SingleThreaded;
037import org.apache.cocoon.util.log.SLF4JLoggerAdapter;
038import org.slf4j.Logger;
039import org.slf4j.LoggerFactory;
040
041// Le code est initialement inspiré du ExcaliburComponentManager
042/**
043 * Special ServiceManager able to handle extensions.<br>
044 * It does not actually implements the Avalon ServiceManager interface, to be able to use the Java 5 generic parameter.<br>
045 * This class is also intended to be used "locally" : its lookup(role) method does NOT propagate to the parent manager if the component does not exist.<br>
046 * To use the real ServiceManager, use the PluginsServiceManager subclass instead.<br>
047 * <br>
048 * It handles the "PluginAware" interface, implemented by extensions.<br>
049 * It handles the following Avalon interfaces :
050 * <ul>
051 * <li>LogEnabled
052 * <li>Contextualizable
053 * <li>Serviceable
054 * <li>Configurable
055 * <li>Parameterizable
056 * <li>Initializable
057 * <li>Startable
058 * <li>Disposable
059 * </ul>
060 * All handled components are considered ThreadSafe, ie only one instance is created at initialization time and returned at each lookup()
061 * @param <T> The type of handled extensions
062 */
063public class ThreadSafeComponentManager<T> extends AbstractLogEnabled implements Contextualizable, Initializable, Disposable, Serviceable
064{
065    /** The Cocoon ServiceManager */
066    protected ServiceManager _manager;
067    
068    /** The application context for components */
069    Context _context;
070
071    // Map<role, component>
072    Map<String, T> _componentsInitializing = new HashMap<>();
073    
074    // Map<role, component>
075    private Map<String, T> _components = new LinkedHashMap<>();
076    
077    /** Used to map roles to ComponentFactories */
078    private Map<String, ComponentFactory> _componentFactories = Collections.synchronizedMap(new LinkedHashMap<>());
079
080    /** Is the Manager disposed or not? */
081    private boolean _disposed;
082
083    /** Is the Manager initialized? */
084    private boolean _initialized;
085    
086    private boolean _allowCircularDependencies;
087    
088    /**
089     * Default constructor. Allow circular dependencies.
090     */
091    public ThreadSafeComponentManager()
092    {
093        this(true);
094    }
095    
096    /**
097     * Constructor.
098     * @param allowCircularDependencies if this manager should allow circular dependencies during initialization
099     */
100    public ThreadSafeComponentManager(boolean allowCircularDependencies)
101    {
102        _allowCircularDependencies = allowCircularDependencies;
103    }
104    
105    public void service(ServiceManager manager)
106    {
107        _manager = manager;
108    }
109
110    /**
111     * Set up the Component Context.
112     */
113    public void contextualize(final Context context)
114    {
115        _context = context;
116    }
117
118    /**
119     * Return an instance of a component based on a Role. The Role is usually the Interface Fully Qualified Name(FQN).
120     * @param role The key name of the <code>Component</code> to retrieve.
121     * @return the desired component or null if no component exist for the given role
122     * @throws ComponentException if an error occurs
123     */
124    public T lookup(String role) throws ComponentException
125    {
126        if (!_initialized)
127        {
128            getLogger().debug("Looking up component on an uninitialized ThreadSafeComponentManager '{}'", role);
129        }
130
131        if (_disposed)
132        {
133            throw new IllegalStateException("You cannot lookup components on a disposed ThreadSafeComponentManager");
134        }
135
136        if (role == null)
137        {
138            throw new ComponentException(role, "ThreadSafeComponentManager attempted to retrieve component with null role.");
139        }
140
141        T component = _components.get(role);
142        
143        if (component == null)
144        {
145            if (_componentsInitializing.containsKey(role))
146            {
147                if (_allowCircularDependencies)
148                {
149                    getLogger().debug("Trying to lookup the component '{}' during its own initialization", role);
150                    return _componentsInitializing.get(role);
151                }
152                else
153                {
154                    throw new IllegalArgumentException("Trying to lookup the component '" + role + "' during its own initialization");
155                }
156            }
157            else if (_componentFactories.containsKey(role))
158            {
159                try
160                {
161                    getLogger().debug("Trying to lookup an uninitializing component '{}'. It will be initialized right now.", role);
162                    component = _componentFactories.get(role).newInstance();
163                }
164                catch (Exception e)
165                {
166                    throw new ComponentException(role, "Unable to initialize component " + role, e);
167                }
168
169                _components.put(role, component);
170            }
171        }
172
173        return component;
174    }
175
176    /**
177     * Tests for existence of a component for a given role.
178     * @param role a string identifying the key to check.
179     * @return true if there is a component for the given role, false if not.
180     */
181    public boolean hasRole(String role)
182    {
183        if (!_initialized || _disposed)
184        {
185            return false;
186        }
187
188        return _components.containsKey(role) || _componentsInitializing.containsKey(role) || _componentFactories.containsKey(role);
189    }
190
191    /**
192     * Tests for existence of a component.
193     * @param component to component to check
194     * @return true if the component exists, false if it does not.
195     */
196    public boolean hasComponent(T component)
197    {
198        if (!_initialized || _disposed)
199        {
200            return false;
201        }
202
203        return _components.containsValue(component) || _componentsInitializing.containsValue(component) || _componentFactories.containsValue(component);
204    }
205
206    /**
207     * Properly initialize of the Child handlers.
208     */
209    public void initialize() throws Exception
210    {
211        if (_initialized)
212        {
213            throw new IllegalStateException("ComponentManager has already been initialized");
214        }
215        
216        synchronized (this)
217        {
218            _initialized = true;
219            
220            for (String role : _componentFactories.keySet())
221            {
222                if (!_components.containsKey(role)) 
223                {
224                    ComponentFactory factory = _componentFactories.get(role);
225
226                    try
227                    {
228                        getLogger().debug("Initializing component for role {}", role);
229                        
230                        T component = factory.newInstance();
231                        _components.put(role, component);
232                    }
233                    catch (Exception e)
234                    {
235                        // Rethrow the exception
236                        throw new Exception("Caught an exception trying to initialize the component " + role, e);
237                    }
238                }
239            }
240
241            _componentFactories.clear();
242            
243            // at this point, all components are initialized
244            // finally, we allow components to be wired after initialization
245            // so that they may avoid circular dependencies issues
246            for (String id : _components.keySet())
247            {
248                Object component = lookup(id);
249                
250                if (component instanceof DeferredServiceable serviceableComponent)
251                {
252                    serviceableComponent.deferredService(_manager);
253                }
254            }
255        }
256    }
257
258    /**
259     * Properly dispose of the Child handlers.
260     */
261    public void dispose()
262    {
263        synchronized (this)
264        {
265            _disposed = true;
266            
267            for (String role : _components.keySet())
268            {
269                T component = _components.get(role);
270
271                try
272                {
273                    if (component instanceof Startable)
274                    {
275                        ((Startable) component).stop();
276                    }
277    
278                    if (component instanceof Disposable)
279                    {
280                        ((Disposable) component).dispose();
281                    }
282                }
283                catch (Exception e)
284                {
285                    getLogger().error("Caught an exception trying to dispose the component " + role, e);
286                }
287            }
288
289            _components.clear();
290        }
291    }
292
293    /**
294     * Add a new component to the manager.
295     * @param pluginName the plugin containing the component
296     * @param featureName the feature containing the component
297     * @param role the role name for the new component.
298     * @param component the class of this component.
299     * @param configuration the configuration for this component.
300     */
301    public void addComponent(String pluginName, String featureName, String role, Class<? extends T> component, Configuration configuration)
302    {
303        if (Poolable.class.isAssignableFrom(component) || SingleThreaded.class.isAssignableFrom(component))
304        {
305            throw new IllegalArgumentException("The class " + component.getName() + " implements SingleThreaded, or Poolable, which is not allowed by this ComponentManager");
306        }
307
308        // get the factory to use to create the instance of the Component.
309        ComponentFactory factory = getComponentFactory(pluginName, featureName, role, component, configuration);
310        
311        _addComponent(role, factory);
312    }
313    
314    void _addComponent(String role, ComponentFactory factory)
315    {
316        if (_initialized)
317        {
318            throw new IllegalStateException("Cannot add components to an initialized ComponentManager");
319        }
320        
321        if (_componentFactories.containsKey(role))
322        {
323            throw new IllegalArgumentException("A component for the role '" + role + "' is already registered on this ComponentManager.");
324        }
325
326        getLogger().debug("Registering factory for role [{}]", role);
327
328        _componentFactories.put(role, factory);
329    }
330    
331    ComponentFactory getComponentFactory(String pluginName, String featureName, String role, Class<? extends T> componentClass, Configuration configuration)
332    {
333        return new ComponentFactory(pluginName, featureName, role, componentClass, configuration, _manager, getLogger());
334    }
335    
336    class ComponentFactory
337    {
338        String _pluginName;
339        String _featureName;
340        String _role;
341        Class<? extends T> _componentClass;
342        Configuration _configuration;
343        ServiceManager _serviceManager;
344        Logger _logger;
345
346        ComponentFactory(String pluginName, String featureName, String role, Class<? extends T> componentClass, Configuration configuration, ServiceManager serviceManager, Logger logger)
347        {
348            _pluginName = pluginName;
349            _featureName = featureName;
350            _componentClass = componentClass;
351            _configuration = configuration;
352            _role = role;
353            _serviceManager = serviceManager;
354            _logger = logger;
355        }
356        
357        T instanciate() throws Exception
358        {
359            T component = _componentClass.getDeclaredConstructor().newInstance();
360
361            if (_logger.isDebugEnabled())
362            {
363                _logger.debug("ComponentFactory creating new instance of " + _componentClass.getName() + ".");
364            }
365            
366            String logger = _configuration == null ? null : _configuration.getAttribute("logger", null);
367            logger = logger != null ? logger : _componentClass.getName();
368            
369            if (component instanceof LogEnabled)
370            {
371                ((LogEnabled) component).setLogger(LoggerFactory.getLogger(logger));
372            }
373
374            if (component instanceof org.apache.avalon.framework.logger.LogEnabled)
375            {
376                ContainerUtil.enableLogging(component, new SLF4JLoggerAdapter(LoggerFactory.getLogger(logger)));
377            }
378
379            if (component instanceof Contextualizable)
380            {
381                ContainerUtil.contextualize(component, _context);
382            }
383
384            if (component instanceof PluginAware)
385            {
386                ((PluginAware) component).setPluginInfo(_pluginName, _featureName, _role);
387            }
388            
389            return component;
390        }
391        
392        void configureAndStart(T component) throws Exception
393        {
394            ContainerUtil.service(component, _serviceManager);
395            
396            ContainerUtil.configure(component, _configuration);
397
398            if (component instanceof Parameterizable)
399            {
400                final Parameters parameters = Parameters.fromConfiguration(_configuration);
401                ContainerUtil.parameterize(component, parameters);
402            }
403
404            ContainerUtil.initialize(component);
405
406            ContainerUtil.start(component);
407        }
408        
409        T newInstance() throws Exception
410        {
411            T component = instanciate();
412            
413            _componentsInitializing.put(_role, component);
414            
415            configureAndStart(component);
416
417            _componentsInitializing.remove(_role);
418            
419            return component;
420        }
421    }
422}