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