001/*
002 *  Copyright 2016 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.core.user.directory;
017
018import java.util.HashMap;
019import java.util.LinkedHashMap;
020import java.util.Map;
021import java.util.Set;
022
023import org.apache.avalon.framework.activity.Disposable;
024import org.apache.avalon.framework.activity.Initializable;
025import org.apache.avalon.framework.component.Component;
026import org.apache.avalon.framework.configuration.Configuration;
027import org.apache.avalon.framework.configuration.ConfigurationException;
028import org.apache.avalon.framework.context.Context;
029import org.apache.avalon.framework.context.ContextException;
030import org.apache.avalon.framework.context.Contextualizable;
031import org.apache.avalon.framework.service.ServiceException;
032import org.apache.avalon.framework.service.ServiceManager;
033import org.apache.avalon.framework.service.Serviceable;
034import org.apache.avalon.framework.thread.ThreadSafe;
035import org.apache.cocoon.components.LifecycleHelper;
036import org.apache.cocoon.util.log.SLF4JLoggerAdapter;
037import org.slf4j.Logger;
038import org.slf4j.LoggerFactory;
039
040import org.ametys.runtime.i18n.I18nizableText;
041import org.ametys.runtime.parameter.AbstractParameterParser;
042import org.ametys.runtime.parameter.Enumerator;
043import org.ametys.runtime.parameter.Parameter;
044import org.ametys.runtime.parameter.ParameterChecker;
045import org.ametys.runtime.parameter.ParameterCheckerDescriptor;
046import org.ametys.runtime.parameter.ParameterCheckerParser;
047import org.ametys.runtime.parameter.ParameterHelper;
048import org.ametys.runtime.parameter.ParameterHelper.ParameterType;
049import org.ametys.runtime.parameter.Validator;
050import org.ametys.runtime.plugin.ExtensionPoint;
051import org.ametys.runtime.plugin.component.AbstractLogEnabled;
052import org.ametys.runtime.plugin.component.LogEnabled;
053import org.ametys.runtime.plugin.component.ThreadSafeComponentManager;
054
055/**
056 * This extension point handles a list of {@link UserDirectoryModel} handled by the plugins.
057 */
058public class UserDirectoryFactory extends AbstractLogEnabled implements ExtensionPoint<UserDirectoryModel>, Initializable, ThreadSafe, Component, Serviceable, Contextualizable, Disposable
059{
060    /** The avalon role */
061    public static final String ROLE = UserDirectoryFactory.class.getName();
062    
063    private Map<String, UserDirectoryModel> _udModels;
064
065    private ServiceManager _smanager;
066
067    private Context _context;
068    
069    @Override
070    public void initialize() throws Exception
071    {
072        _udModels = new HashMap<>();
073    }
074    
075    @Override
076    public void dispose()
077    {
078        _udModels.clear();
079    }
080    
081    @Override
082    public void service(ServiceManager smanager) throws ServiceException
083    {
084        _smanager = smanager;
085    }
086    
087    @Override
088    public void contextualize(Context context) throws ContextException
089    {
090        _context = context;
091    }
092    
093    /**
094     * Creates a instance of {@link UserDirectory}
095     * @param modelId The id of the user directory model
096     * @param id A non-null and non-empty unique identifier
097     * @param paramsValues The parameters's values
098     * @param populationId The id of the population the created user directory belongs to.
099     * @param label The directory optional label
100     * @return a user's directory
101     */
102    public UserDirectory createUserDirectory(String id, String modelId, Map<String, Object> paramsValues, String populationId, String label)
103    {
104        if (_udModels.containsKey(modelId))
105        {
106            UserDirectoryModel userDirectoryModel = _udModels.get(modelId);
107            
108            UserDirectory ud = null;
109            Class<UserDirectory> udClass = userDirectoryModel.getUserDirectoryClass();
110            try
111            {
112                ud = userDirectoryModel.getUserDirectoryClass().newInstance();
113            }
114            catch (InstantiationException | IllegalAccessException  e)
115            {
116                throw new IllegalArgumentException("Cannot instanciate the class " + udClass.getCanonicalName() + ". Check that there is a public constructor with no arguments.");
117            }
118            
119            Logger logger = LoggerFactory.getLogger(udClass);
120            try
121            {
122                if (ud instanceof LogEnabled)
123                {
124                    ((LogEnabled) ud).setLogger(logger);
125                }
126                
127                LifecycleHelper.setupComponent(ud, new SLF4JLoggerAdapter(logger), _context, _smanager, userDirectoryModel.getUserDirectoryConfiguration());
128            }
129            catch (Exception e)
130            {
131                getLogger().warn("An exception occured during the setup of the component " + modelId, e);
132            }
133            
134            ud.setPopulationId(populationId);
135            try
136            {
137                ud.init(id, modelId, paramsValues, label);
138            }
139            catch (Exception e)
140            {
141                throw new IllegalStateException("An error occured during the initialization of the UserDirectory '" + modelId + "' of the UserPopulation '" + populationId + "'", e);
142            }
143            
144            return ud;
145        }
146        
147        return null;
148    }
149
150    @Override
151    public void addExtension(String id, String pluginName, String featureName, Configuration configuration) throws ConfigurationException
152    {
153        getLogger().debug("Adding user directory model from plugin {}/{}", pluginName, featureName);
154
155        try
156        {
157            addUserDirectoryModel(pluginName, configuration);
158        }
159        catch (ConfigurationException e)
160        {
161            if (getLogger().isWarnEnabled())
162            {
163                getLogger().warn("The plugin '" + pluginName + "." + featureName + "' has a user directory model extension but has an incorrect configuration", e);
164            }
165        }
166    }
167    
168    /**
169     * Add a user directory model
170     * @param pluginName The plugin name
171     * @param configuration The configuration
172     * @throws ConfigurationException when a configuration problem occurs
173     */
174    protected void addUserDirectoryModel (String pluginName, Configuration configuration) throws ConfigurationException
175    {
176        String id = configuration.getAttribute("id");
177        I18nizableText label = I18nizableText.parseI18nizableText(configuration.getChild("label"), "plugin." + pluginName);
178        I18nizableText description = I18nizableText.parseI18nizableText(configuration.getChild("description"), "plugin." + pluginName);
179        
180        String className = null;
181        Class<?> udClass = null;
182        Configuration classConfig = null;
183        try
184        {
185            className = configuration.getChild("class").getAttribute("name");
186            udClass = Class.forName(className);
187            classConfig = configuration.getChild("class");
188        }
189        catch (ClassNotFoundException | ConfigurationException e)
190        {
191            throw new ConfigurationException("User directory model with id '" + id + "' has an invalid configuration for class name " + (className != null ? className + " <class not found>" : "<missing tag <class>") + "'", e);
192        }
193        
194        if (!UserDirectory.class.isAssignableFrom(udClass))
195        {
196            throw new ConfigurationException("User directory model with id '" + id + "' has an invalid configuration: '" + className + "' is not an instance of UserDirectory");
197        }
198        
199        // Parse parameter
200        Map<String, Parameter<ParameterType>> parameters = new LinkedHashMap<>();
201        
202        ThreadSafeComponentManager<Validator> validatorManager = new ThreadSafeComponentManager<>();
203        validatorManager.setLogger(getLogger());
204        validatorManager.contextualize(_context);
205        validatorManager.service(_smanager);
206        
207        ThreadSafeComponentManager<Enumerator> enumeratorManager = new ThreadSafeComponentManager<>();
208        enumeratorManager.setLogger(getLogger());
209        enumeratorManager.contextualize(_context);
210        enumeratorManager.service(_smanager);
211        
212        UserDirectoryModelParameterParser udParser = new UserDirectoryModelParameterParser(enumeratorManager, validatorManager);
213        
214        Configuration[] paramsConfiguration = configuration.getChild("parameters").getChildren("param");
215        for (Configuration paramConfiguration : paramsConfiguration)
216        {
217            configureParameters(udParser, paramConfiguration, pluginName, parameters);
218        }
219        
220        // Parse parameter checkers
221        Map<String, ParameterCheckerDescriptor> parameterCheckers = new LinkedHashMap<>();
222        
223        ThreadSafeComponentManager<ParameterChecker> parameterCheckerManager = new ThreadSafeComponentManager<>();
224        parameterCheckerManager.setLogger(getLogger());
225        parameterCheckerManager.contextualize(_context);
226        parameterCheckerManager.service(_smanager);
227        
228        ParameterCheckerParser parameterCheckerParser = new ParameterCheckerParser(parameterCheckerManager);
229        
230        Configuration[] paramCheckersConfiguration = configuration.getChild("parameters").getChildren("param-checker");
231        for (Configuration paramCheckerConfiguration : paramCheckersConfiguration)
232        {
233            configureParamChecker(parameterCheckerParser, paramCheckerConfiguration, pluginName, parameterCheckers);
234        }
235        
236        try
237        {
238            udParser.lookupComponents();
239            parameterCheckerParser.lookupComponents();
240        }
241        catch (Exception e)
242        {
243            throw new ConfigurationException("Unable to lookup parameter local components", configuration, e);
244        }
245        
246        // Create and reference the model
247        @SuppressWarnings("unchecked")
248        UserDirectoryModel udModel = new DefaultUserDirectoryModel(id, (Class<UserDirectory>) udClass, classConfig, label, description, parameters, parameterCheckers, pluginName);
249        if (_udModels.containsKey(id))
250        {
251            UserDirectoryModel oldUDModel = _udModels.get(id);
252            throw new IllegalArgumentException("User directory model with id '" + id + "' is already declared in plugin '" + oldUDModel.getPluginName() + "'. This second declaration is ignored.");
253        }
254        
255        _udModels.put(id, udModel);
256    }
257    
258    /**
259     * Configure a parameter to access the user directory
260     * @param paramParser the parameter parser.
261     * @param configuration The parameter configuration.
262     * @param pluginName The plugin name
263     * @param parameters The model's parameters
264     * @throws ConfigurationException if configuration is incomplete or invalid.
265     */
266    protected void configureParameters(UserDirectoryModelParameterParser paramParser, Configuration configuration, String pluginName, Map<String, Parameter<ParameterType>> parameters) throws ConfigurationException
267    {
268        Parameter<ParameterType> parameter = paramParser.parseParameter(_smanager, pluginName, configuration);
269        String id = parameter.getId();
270        
271        if (parameters.containsKey(id))
272        {
273            throw new ConfigurationException("The parameter '" + id + "' is already declared. IDs must be unique.", configuration);
274        }
275        
276        parameters.put(id, parameter);
277    }
278    
279    /**
280     * Configure a parameter checker of a user directory
281     * @param parser the parameter checker parser.
282     * @param configuration The parameter checker configuration.
283     * @param pluginName The plugin name
284     * @param parameterCheckers The model's parameter checkers
285     * @throws ConfigurationException if configuration is incomplete or invalid.
286     */
287    protected void configureParamChecker(ParameterCheckerParser parser, Configuration configuration, String pluginName, Map<String, ParameterCheckerDescriptor> parameterCheckers) throws ConfigurationException
288    {
289        ParameterCheckerDescriptor parameterChecker = parser.parseParameterChecker(pluginName, configuration);
290        String id = parameterChecker.getId();
291        
292        if (parameterCheckers.containsKey(id))
293        {
294            throw new ConfigurationException("The parameter checker '" + id + "' is already declared. IDs must be unique.", configuration);
295        }
296        
297        parameterCheckers.put(id, parameterChecker);
298    }
299    
300    @Override
301    public void initializeExtensions() throws Exception
302    {
303        // Nothing to do
304    }
305
306    @Override
307    public boolean hasExtension(String id)
308    {
309        return _udModels.containsKey(id);
310    }
311
312    @Override
313    public UserDirectoryModel getExtension(String id)
314    {
315        return _udModels.get(id);
316    }
317
318    @Override
319    public Set<String> getExtensionsIds()
320    {
321        return _udModels.keySet();
322    }
323    
324    /**
325     * Class for parsing parameters of a {@link UserDirectoryModel}
326     */
327    public class UserDirectoryModelParameterParser extends AbstractParameterParser<Parameter<ParameterType>, ParameterType>
328    {
329        /**
330         * Constructor
331         * @param enumeratorManager The manager for enumeration
332         * @param validatorManager The manager for validation
333         */
334        public UserDirectoryModelParameterParser(ThreadSafeComponentManager<Enumerator> enumeratorManager, ThreadSafeComponentManager<Validator> validatorManager)
335        {
336            super(enumeratorManager, validatorManager);
337        }
338        
339        @Override
340        protected Parameter<ParameterType> _createParameter(Configuration parameterConfig) throws ConfigurationException
341        {
342            return new Parameter<>();
343        }
344        
345        @Override
346        protected String _parseId(Configuration parameterConfig) throws ConfigurationException
347        {
348            return parameterConfig.getAttribute("id");
349        }
350        
351        @Override
352        protected ParameterType _parseType(Configuration parameterConfig) throws ConfigurationException
353        {
354            try
355            {
356                return ParameterType.valueOf(parameterConfig.getAttribute("type").toUpperCase());
357            }
358            catch (IllegalArgumentException e)
359            {
360                throw new ConfigurationException("Invalid parameter type", parameterConfig, e);
361            }
362        }
363        
364        @Override
365        protected Object _parseDefaultValue(Configuration parameterConfig, Parameter<ParameterType> parameter) throws ConfigurationException
366        {
367            String defaultValue = parameterConfig.getChild("default-value").getValue(null);
368            return ParameterHelper.castValue(defaultValue, parameter.getType());
369        }
370        
371        @Override
372        protected void _additionalParsing(ServiceManager manager, String pluginName, Configuration parameterConfig, String parameterId, Parameter<ParameterType> parameter)
373                throws ConfigurationException
374        {
375            super._additionalParsing(manager, pluginName, parameterConfig, parameterId, parameter);
376            parameter.setId(parameterId);
377        }
378    }
379}