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<String, ComponentFactory>());
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    }
244
245    /**
246     * Properly dispose of the Child handlers.
247     */
248    public void dispose()
249    {
250        synchronized (this)
251        {
252            _disposed = true;
253            
254            for (String role : _components.keySet())
255            {
256                T component = _components.get(role);
257
258                try
259                {
260                    if (component instanceof Startable)
261                    {
262                        ((Startable) component).stop();
263                    }
264    
265                    if (component instanceof Disposable)
266                    {
267                        ((Disposable) component).dispose();
268                    }
269                }
270                catch (Exception e)
271                {
272                    getLogger().error("Caught an exception trying to dispose the component " + role, e);
273                }
274            }
275
276            _components.clear();
277        }
278    }
279
280    /**
281     * Add a new component to the manager.
282     * @param pluginName the plugin containing the component
283     * @param featureName the feature containing the component
284     * @param role the role name for the new component.
285     * @param component the class of this component.
286     * @param configuration the configuration for this component.
287     */
288    public void addComponent(String pluginName, String featureName, String role, Class<? extends T> component, Configuration configuration)
289    {
290        if (Poolable.class.isAssignableFrom(component) || SingleThreaded.class.isAssignableFrom(component))
291        {
292            throw new IllegalArgumentException("The class " + component.getName() + " implements SingleThreaded, or Poolable, which is not allowed by this ComponentManager");
293        }
294
295        // get the factory to use to create the instance of the Component.
296        ComponentFactory factory = getComponentFactory(pluginName, featureName, role, component, configuration);
297        
298        _addComponent(role, factory);
299    }
300    
301    void _addComponent(String role, ComponentFactory factory)
302    {
303        if (_initialized)
304        {
305            throw new IllegalStateException("Cannot add components to an initialized ComponentManager");
306        }
307        
308        if (_componentFactories.containsKey(role))
309        {
310            throw new IllegalArgumentException("A component for the role '" + role + "' is already registered on this ComponentManager.");
311        }
312
313        getLogger().debug("Registering factory for role [{}]", role);
314
315        _componentFactories.put(role, factory);
316    }
317    
318    ComponentFactory getComponentFactory(String pluginName, String featureName, String role, Class<? extends T> componentClass, Configuration configuration)
319    {
320        return new ComponentFactory(pluginName, featureName, role, componentClass, configuration, _manager, getLogger());
321    }
322    
323    class ComponentFactory
324    {
325        String _pluginName;
326        String _featureName;
327        String _role;
328        Class<? extends T> _componentClass;
329        Configuration _configuration;
330        ServiceManager _serviceManager;
331        Logger _logger;
332
333        ComponentFactory(String pluginName, String featureName, String role, Class<? extends T> componentClass, Configuration configuration, ServiceManager serviceManager, Logger logger)
334        {
335            _pluginName = pluginName;
336            _featureName = featureName;
337            _componentClass = componentClass;
338            _configuration = configuration;
339            _role = role;
340            _serviceManager = serviceManager;
341            _logger = logger;
342        }
343        
344        T instanciate() throws Exception
345        {
346            T component = _componentClass.getDeclaredConstructor().newInstance();
347
348            if (_logger.isDebugEnabled())
349            {
350                _logger.debug("ComponentFactory creating new instance of " + _componentClass.getName() + ".");
351            }
352            
353            String logger = _configuration == null ? null : _configuration.getAttribute("logger", null);
354            logger = logger != null ? logger : _componentClass.getName();
355            
356            if (component instanceof LogEnabled)
357            {
358                ((LogEnabled) component).setLogger(LoggerFactory.getLogger(logger));
359            }
360
361            if (component instanceof org.apache.avalon.framework.logger.LogEnabled)
362            {
363                ContainerUtil.enableLogging(component, new SLF4JLoggerAdapter(LoggerFactory.getLogger(logger)));
364            }
365
366            if (component instanceof Contextualizable)
367            {
368                ContainerUtil.contextualize(component, _context);
369            }
370
371            if (component instanceof PluginAware)
372            {
373                ((PluginAware) component).setPluginInfo(_pluginName, _featureName, _role);
374            }
375            
376            return component;
377        }
378        
379        void configureAndStart(T component) throws Exception
380        {
381            ContainerUtil.service(component, _serviceManager);
382            
383            ContainerUtil.configure(component, _configuration);
384
385            if (component instanceof Parameterizable)
386            {
387                final Parameters parameters = Parameters.fromConfiguration(_configuration);
388                ContainerUtil.parameterize(component, parameters);
389            }
390
391            ContainerUtil.initialize(component);
392
393            ContainerUtil.start(component);
394        }
395        
396        T newInstance() throws Exception
397        {
398            T component = instanciate();
399            
400            _componentsInitializing.put(_role, component);
401            
402            configureAndStart(component);
403
404            _componentsInitializing.remove(_role);
405            
406            return component;
407        }
408    }
409}