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.population;
017
018import java.io.File;
019import java.io.FileOutputStream;
020import java.io.IOException;
021import java.io.OutputStream;
022import java.nio.file.Files;
023import java.nio.file.StandardCopyOption;
024import java.sql.Connection;
025import java.time.Instant;
026import java.time.temporal.ChronoUnit;
027import java.util.ArrayList;
028import java.util.Arrays;
029import java.util.Collections;
030import java.util.HashMap;
031import java.util.HashSet;
032import java.util.LinkedHashMap;
033import java.util.List;
034import java.util.Map;
035import java.util.Properties;
036import java.util.Set;
037import java.util.regex.Pattern;
038import java.util.stream.Collectors;
039
040import javax.xml.transform.OutputKeys;
041import javax.xml.transform.TransformerConfigurationException;
042import javax.xml.transform.TransformerFactory;
043import javax.xml.transform.TransformerFactoryConfigurationError;
044import javax.xml.transform.sax.SAXTransformerFactory;
045import javax.xml.transform.sax.TransformerHandler;
046import javax.xml.transform.stream.StreamResult;
047
048import org.apache.avalon.framework.activity.Disposable;
049import org.apache.avalon.framework.activity.Initializable;
050import org.apache.avalon.framework.component.Component;
051import org.apache.avalon.framework.configuration.Configuration;
052import org.apache.avalon.framework.configuration.ConfigurationException;
053import org.apache.avalon.framework.configuration.DefaultConfigurationBuilder;
054import org.apache.avalon.framework.service.ServiceException;
055import org.apache.avalon.framework.service.ServiceManager;
056import org.apache.avalon.framework.service.Serviceable;
057import org.apache.cocoon.xml.AttributesImpl;
058import org.apache.cocoon.xml.XMLUtils;
059import org.apache.commons.lang3.StringUtils;
060import org.apache.xml.serializer.OutputPropertiesFactory;
061import org.xml.sax.SAXException;
062
063import org.ametys.core.ObservationConstants;
064import org.ametys.core.authentication.CredentialProvider;
065import org.ametys.core.authentication.CredentialProviderFactory;
066import org.ametys.core.authentication.CredentialProviderModel;
067import org.ametys.core.datasource.ConnectionHelper;
068import org.ametys.core.datasource.SQLDataSourceManager;
069import org.ametys.core.observation.Event;
070import org.ametys.core.observation.ObservationManager;
071import org.ametys.core.script.SQLScriptHelper;
072import org.ametys.core.ui.Callable;
073import org.ametys.core.user.CurrentUserProvider;
074import org.ametys.core.user.InvalidModificationException;
075import org.ametys.core.user.UserIdentity;
076import org.ametys.core.user.directory.ModifiableUserDirectory;
077import org.ametys.core.user.directory.UserDirectory;
078import org.ametys.core.user.directory.UserDirectoryFactory;
079import org.ametys.core.user.directory.UserDirectoryModel;
080import org.ametys.core.util.I18nUtils;
081import org.ametys.plugins.core.impl.user.directory.StaticUserDirectory;
082import org.ametys.runtime.i18n.I18nizableText;
083import org.ametys.runtime.model.DefinitionContext;
084import org.ametys.runtime.model.ElementDefinition;
085import org.ametys.runtime.model.checker.ItemCheckerDescriptor;
086import org.ametys.runtime.model.type.DataContext;
087import org.ametys.runtime.model.type.ElementType;
088import org.ametys.runtime.model.type.ModelItemTypeConstants;
089import org.ametys.runtime.model.type.xml.XMLElementType;
090import org.ametys.runtime.plugin.PluginsManager;
091import org.ametys.runtime.plugin.PluginsManager.Status;
092import org.ametys.runtime.plugin.component.AbstractLogEnabled;
093import org.ametys.runtime.util.AmetysHomeHelper;
094
095/**
096 * DAO for accessing {@link UserPopulation}
097 */
098public class UserPopulationDAO extends AbstractLogEnabled implements Component, Serviceable, Initializable, Disposable
099{
100    /** Avalon Role */
101    public static final String ROLE = UserPopulationDAO.class.getName();
102    
103    /** The id of the "admin" population */
104    public static final String ADMIN_POPULATION_ID = "admin_population";
105    
106    /** The id of the "user system" login */
107    public static final String SYSTEM_USER_LOGIN = "system-user";
108    /** The id of the "user system" population */
109    public static final UserIdentity SYSTEM_USER_IDENTITY = new UserIdentity(SYSTEM_USER_LOGIN, ADMIN_POPULATION_ID);
110    
111    /** The sql table for admin users */
112    private static final String __ADMIN_TABLENAME = "AdminUsers";
113
114    /** The path of the XML file containing the user populations */
115    private static File __USER_POPULATIONS_FILE;
116    
117    /** The regular expression for an id of a user population */
118    private static final String __ID_REGEX = "^[a-z][a-z0-9_-]*";
119    
120    /** The date (as a long) of the last time the {@link #__USER_POPULATIONS_FILE Populations file} was read (last update) */
121    private long _lastFileReading;
122    
123    /** The whole user populations of the application */
124    private Map<String, UserPopulation> _userPopulations;
125    /** The misconfigured user populations */
126    private Set<String> _misconfiguredUserPopulations;
127    
128    /** The list of population ids which are declared in the user population file but were not instanciated since their configuration led to an error */
129    private Set<String> _ignoredPopulations;
130    
131    /** The population admin */
132    private UserPopulation _adminUserPopulation;
133    
134    /** The user directories factory  */
135    private UserDirectoryFactory _userDirectoryFactory;
136
137    /** The credential providers factory  */
138    private CredentialProviderFactory _credentialProviderFactory;
139    
140    /** The extension point for population consumers */
141    private PopulationConsumerExtensionPoint _populationConsumerEP;
142
143    private ObservationManager _observationManager;
144
145    private CurrentUserProvider _currentUserProvider;
146
147    private I18nUtils _i18nutils;
148    
149    private ServiceManager _manager;
150
151    @Override
152    public void initialize()
153    {
154        __USER_POPULATIONS_FILE = new File(AmetysHomeHelper.getAmetysHome(), "config" + File.separator + "user-populations.xml");
155        _userPopulations = new LinkedHashMap<>();
156        _misconfiguredUserPopulations = new HashSet<>();
157        _ignoredPopulations = new HashSet<>();
158        _lastFileReading = 0;
159    }
160    
161    private PopulationConsumerExtensionPoint _getPopulationConsumerExtensionPoint()
162    {
163        if (_populationConsumerEP == null)
164        {
165            try
166            {
167                _populationConsumerEP = (PopulationConsumerExtensionPoint) _manager.lookup(PopulationConsumerExtensionPoint.ROLE);
168            }
169            catch (ServiceException e)
170            {
171                throw new RuntimeException("Failed to retrieve PopulationConsumerExtensionPoint", e);
172            }
173        }
174        return _populationConsumerEP;
175    }
176    
177    private ObservationManager _getObservationManager()
178    {
179        if (_observationManager == null)
180        {
181            try
182            {
183                _observationManager = (ObservationManager) _manager.lookup(ObservationManager.ROLE);
184            }
185            catch (ServiceException e)
186            {
187                // Not a safe component... ignore it
188                return null;
189            }
190        }
191        return _observationManager;
192    }
193    
194    private UserDirectoryFactory _getUserDirectoryFactory()
195    {
196        if (_userDirectoryFactory == null)
197        {
198            try
199            {
200                _userDirectoryFactory = (UserDirectoryFactory) _manager.lookup(UserDirectoryFactory.ROLE);
201            }
202            catch (ServiceException e)
203            {
204                throw new RuntimeException("Failed to retrieve UserDirectoryFactory", e);
205            }
206        }
207        return _userDirectoryFactory;
208    }
209    
210    private CredentialProviderFactory _getCredentialProviderFactory()
211    {
212        if (_credentialProviderFactory == null)
213        {
214            try
215            {
216                _credentialProviderFactory = (CredentialProviderFactory) _manager.lookup(CredentialProviderFactory.ROLE);
217            }
218            catch (ServiceException e)
219            {
220                throw new RuntimeException("Failed to retrieve CredentialProviderFactory", e);
221            }
222        }
223        return _credentialProviderFactory;
224    }
225    
226    private CurrentUserProvider _getCurrentUserProvider()
227    {
228        if (_currentUserProvider == null)
229        {
230            try
231            {
232                _currentUserProvider = (CurrentUserProvider) _manager.lookup(CurrentUserProvider.ROLE);
233            }
234            catch (ServiceException e)
235            {
236                throw new RuntimeException("Failed to retrieve CurrentUserProvider", e);
237            }
238        }
239        return _currentUserProvider;
240    }
241    
242    private I18nUtils _getI18nUtils()
243    {
244        if (_i18nutils == null)
245        {
246            try
247            {
248                _i18nutils = (I18nUtils) _manager.lookup(I18nUtils.ROLE);
249            }
250            catch (ServiceException e)
251            {
252                throw new RuntimeException("Failed to retrieve I18nUtils", e);
253            }
254        }
255        return _i18nutils;
256    }
257    
258    @Override
259    public void service(ServiceManager manager) throws ServiceException
260    {
261        _manager =  manager;
262    }
263    
264    /**
265     * Gets all the populations to JSON format
266     * @param withAdmin True to include the "admin" population
267     * @return A list of object representing the {@link UserPopulation}s
268     */
269    public List<Object> getUserPopulationsAsJson(boolean withAdmin)
270    {
271        return getUserPopulations(withAdmin).stream().map(this::getUserPopulationAsJson).collect(Collectors.toList());
272    }
273    
274    /**
275     * Gets a population to JSON format
276     * @param userPopulation The user population to get
277     * @return An object representing a {@link UserPopulation}
278     */
279    public Map<String, Object> getUserPopulationAsJson(UserPopulation userPopulation)
280    {
281        Map<String, Object> result = new LinkedHashMap<>();
282        result.put("id", userPopulation.getId());
283        result.put("label", userPopulation.getLabel());
284        result.put("enabled", userPopulation.isEnabled());
285        result.put("valid", isValid(userPopulation.getId()));
286        result.put("isInUse", _getPopulationConsumerExtensionPoint().isInUse(userPopulation.getId()));
287        
288        List<Object> userDirectories = new ArrayList<>();
289        for (UserDirectory ud : userPopulation.getUserDirectories())
290        {
291            String udModelId = ud.getUserDirectoryModelId();
292            UserDirectoryModel udModel = _getUserDirectoryFactory().getExtension(udModelId);
293            
294            Map<String, Object> directory = new HashMap<>();
295            directory.put("id", ud.getId());
296            
297            if (StringUtils.isNotBlank(ud.getLabel()))
298            {
299                directory.put("label", _getI18nUtils().translate(udModel.getLabel()) + " (" + ud.getLabel() + ")");
300            }
301            else
302            {
303                directory.put("label", udModel.getLabel());
304            }
305            directory.put("modifiable", ud instanceof ModifiableUserDirectory);
306            userDirectories.add(directory);
307        }
308        result.put("userDirectories", userDirectories);
309        
310        List<Object> credentialProviders = new ArrayList<>();
311        for (CredentialProvider cp : userPopulation.getCredentialProviders())
312        {
313            String cpModelId = cp.getCredentialProviderModelId();
314            CredentialProviderModel cpModel = _getCredentialProviderFactory().getExtension(cpModelId);
315            
316            Map<String, Object> credentialProvider = new HashMap<>();
317            credentialProvider.put("id", cp.getId());
318            
319            if (StringUtils.isNotBlank(cp.getLabel()))
320            {
321                credentialProvider.put("label", _getI18nUtils().translate(cpModel.getLabel()) + " (" + cp.getLabel() + ")");
322            }
323            else
324            {
325                credentialProvider.put("label", cpModel.getLabel());
326            }
327            credentialProviders.add(credentialProvider);
328        }
329        result.put("credentialProviders", credentialProviders);
330        
331        return result;
332    }
333    
334    /**
335     * Gets all the populations of this application
336     * @param includeAdminPopulation True to include the "admin" population
337     * @return A list of {@link UserPopulation}
338     */
339    public List<UserPopulation> getUserPopulations(boolean includeAdminPopulation)
340    {
341        List<UserPopulation> result = new ArrayList<>();
342        if (includeAdminPopulation)
343        {
344            result.add(getAdminPopulation());
345        }
346        
347        // Don't read in safe mode, we know that only the admin population is needed in this case and we want to prevent some warnings in the logs for non-safe features not found
348        if (Status.OK.equals(PluginsManager.getInstance().getStatus()))
349        {
350            _readPopulations(false);
351            result.addAll(_userPopulations.values());
352        }
353        
354        return result;
355    }
356    
357    /**
358     * Gets all the enabled populations of this application
359     * @param withAdmin True to include the "admin" population
360     * @return A list of enabled {@link UserPopulation}
361     */
362    public List<UserPopulation> getEnabledUserPopulations(boolean withAdmin)
363    {
364        return getUserPopulations(withAdmin).stream().filter(UserPopulation::isEnabled).collect(Collectors.toList());
365    }
366    
367    /**
368     * Gets a population with its id.
369     * @param id The id of the population
370     * @return The {@link UserPopulation}, or null if not found
371     */
372    public UserPopulation getUserPopulation(String id)
373    {
374        if (ADMIN_POPULATION_ID.equals(id))
375        {
376            return getAdminPopulation();
377        }
378        
379        _readPopulations(false);
380        return _userPopulations.get(id);
381    }
382    
383    /**
384     * Gets the list of the ids of all the population of the application
385     * @return The list of the ids of all the populations
386     */
387    @Callable
388    public List<String> getUserPopulationsIds()
389    {
390        _readPopulations(false);
391        return new ArrayList<>(_userPopulations.keySet());
392    }
393    
394    /**
395     * Returns the id of population which have a fatal invalid configuration.
396     * These populations can NOT be used by application.
397     * @return The ignored populations
398     */
399    public Set<String> getIgnoredPopulations()
400    {
401        _readPopulations(false);
402        return _ignoredPopulations;
403    }
404    
405    /**
406     * Returns the id of population which have at least one user directory or one credential provider misconfigured
407     * These populations can be used by application.
408     * @return The misconfigured populations.
409     */
410    public Set<String> getMisconfiguredPopulations()
411    {
412        _readPopulations(false);
413        return _misconfiguredUserPopulations;
414    }
415    
416    /**
417     * Return the configuration file.
418     * @return the configuration file.
419     */
420    public File getConfigurationFile()
421    {
422        return __USER_POPULATIONS_FILE;
423    }
424    
425    /**
426     * Gets the configuration for creating/editing a user population.
427     * @return A map containing information about what is needed to create/edit a user population
428     * @throws Exception If an error occurs.
429     */
430    @Callable
431    public Map<String, Object> getEditionConfiguration() throws Exception
432    {
433        Map<String, Object> result = new LinkedHashMap<>();
434        DefinitionContext context = DefinitionContext.newInstance().withEdition(true);
435        
436        List<Object> userDirectoryModels = new ArrayList<>();
437        for (String extensionId : _getUserDirectoryFactory().getExtensionsIds())
438        {
439            UserDirectoryModel udModel = _getUserDirectoryFactory().getExtension(extensionId);
440            if (udModel == null)
441            {
442                throw new IllegalStateException("The user population configuration refers to an unexisting extension for the user directory '" + extensionId + "'");
443            }
444            
445            Map<String, Object> udMap = new LinkedHashMap<>();
446            udMap.put("id", extensionId);
447            udMap.put("label", udModel.getLabel());
448            udMap.put("description", udModel.getDescription());
449            
450            Map<String, Object> params = new LinkedHashMap<>();
451            for (String paramId : udModel.getParameters().keySet())
452            {
453                // prefix in case of two parameters from two different models have the same id which can lead to some errorsin client-side
454                params.put(extensionId + "$" + paramId, udModel.getParameters().get(paramId).toJSON(context));
455            }
456            udMap.put("parameters", params);
457            
458            Map<String, Object> paramCheckers = new LinkedHashMap<>();
459            for (String paramCheckerId : udModel.getParameterCheckers().keySet())
460            {
461                ItemCheckerDescriptor paramChecker = udModel.getParameterCheckers().get(paramCheckerId);
462                paramCheckers.put(extensionId + "$" + paramCheckerId, paramChecker.toJSON());
463            }
464            udMap.put("parameterCheckers", paramCheckers);
465            
466            userDirectoryModels.add(udMap);
467        }
468        result.put("userDirectoryModels", userDirectoryModels);
469        
470        List<Object> credentialProviderModels = new ArrayList<>();
471        for (String extensionId : _getCredentialProviderFactory().getExtensionsIds())
472        {
473            CredentialProviderModel cpModel = _getCredentialProviderFactory().getExtension(extensionId);
474            if (cpModel == null)
475            {
476                throw new IllegalStateException("The user population configuration refers to an unexisting extension for the credential provider '" + extensionId + "'");
477            }
478
479            Map<String, Object> cpMap = new LinkedHashMap<>();
480            cpMap.put("id", extensionId);
481            cpMap.put("label", cpModel.getLabel());
482            cpMap.put("description", cpModel.getDescription());
483            
484            Map<String, Object> params = new LinkedHashMap<>();
485            for (String paramId : cpModel.getParameters().keySet())
486            {
487                // prefix in case of two parameters from two different models have the same id which can lead to some errors in client-side
488                params.put(extensionId + "$" + paramId, cpModel.getParameters().get(paramId).toJSON(context));
489            }
490            cpMap.put("parameters", params);
491            
492            Map<String, Object> paramCheckers = new LinkedHashMap<>();
493            for (String paramCheckerId : cpModel.getParameterCheckers().keySet())
494            {
495                ItemCheckerDescriptor paramChecker = cpModel.getParameterCheckers().get(paramCheckerId);
496                paramCheckers.put(extensionId + "$" + paramCheckerId, paramChecker.toJSON());
497            }
498            cpMap.put("parameterCheckers", paramCheckers);
499            
500            credentialProviderModels.add(cpMap);
501        }
502        result.put("credentialProviderModels", credentialProviderModels);
503        
504        return result;
505    }
506    
507    /**
508     * Gets the values of the parameters of the given population
509     * @param id The id of the population
510     * @return The values of the parameters
511     */
512    @Callable
513    public Map<String, Object> getPopulationParameterValues(String id)
514    {
515        Map<String, Object> result = new LinkedHashMap<>();
516        
517        _readPopulations(false);
518        UserPopulation up = _userPopulations.get(id);
519        
520        if (up == null)
521        {
522            getLogger().error("The UserPopulation of id '{}' does not exists.", id);
523            result.put("error", "unknown");
524            return result;
525        }
526        
527        // Population Label
528        result.put("label", up.getLabel());
529        result.put("id", up.getId());
530        
531        // User Directories
532        List<Map<String, Object>> userDirectories = new ArrayList<>();
533        
534        for (UserDirectory ud : up.getUserDirectories())
535        {
536            Map<String, Object> ud2json = new HashMap<>();
537            String udModelId = ud.getUserDirectoryModelId();
538            UserDirectoryModel model = _getUserDirectoryFactory().getExtension(udModelId);
539            if (model == null)
540            {
541                throw new IllegalStateException("The user population configuration refers to an unexisting extension for the user directory '" + udModelId + "'");
542            }
543
544            ud2json.put("id", ud.getId());
545            ud2json.put("udModelId", udModelId);
546            ud2json.put("label", ud.getLabel());
547            Map<String, Object> params = new HashMap<>();
548            for (String key : ud.getParameterValues().keySet())
549            {
550                ElementDefinition parameter = model.getParameters().get(key);
551                params.put(udModelId + "$" + key, ModelItemTypeConstants.PASSWORD_ELEMENT_TYPE_ID.equals(parameter.getType().getId()) ? "PASSWORD" : ud.getParameterValues().get(key));
552            }
553            ud2json.put("params", params);
554            userDirectories.add(ud2json);
555        }
556        result.put("userDirectories", userDirectories);
557        
558        // Credential Providers
559        List<Map<String, Object>> credentialProviders = new ArrayList<>();
560        
561        for (CredentialProvider cp : up.getCredentialProviders())
562        {
563            Map<String, Object> cp2json = new HashMap<>();
564            String cpModelId = cp.getCredentialProviderModelId();
565            CredentialProviderModel model = _getCredentialProviderFactory().getExtension(cpModelId);
566            if (model == null)
567            {
568                throw new IllegalStateException("The user population configuration refers to an unexisting extension for the credential provider '" + cpModelId + "'");
569            }
570
571            cp2json.put("id", cp.getId());
572            cp2json.put("cpModelId", cpModelId);
573            cp2json.put("label", cp.getLabel());
574            Map<String, Object> params = new HashMap<>();
575            for (String key : cp.getParameterValues().keySet())
576            {
577                ElementDefinition parameter = model.getParameters().get(key);
578                params.put(cpModelId + "$" + key, ModelItemTypeConstants.PASSWORD_ELEMENT_TYPE_ID.equals(parameter.getType().getId()) ? "PASSWORD" : cp.getParameterValues().get(key));
579            }
580            cp2json.put("params", params);
581            credentialProviders.add(cp2json);
582        }
583        result.put("credentialProviders", credentialProviders);
584        
585        return result;
586    }
587    
588    /**
589     * Gets the "admin" population
590     * @return The "admin" population
591     */
592    public synchronized UserPopulation getAdminPopulation()
593    {
594        if (_adminUserPopulation != null)
595        {
596            return _adminUserPopulation;
597        }
598        
599        _adminUserPopulation = new UserPopulation();
600        _adminUserPopulation.setId(ADMIN_POPULATION_ID);
601        
602        Map<String, String> userDirectory1 = new HashMap<>();
603        userDirectory1.put("udModelId", "org.ametys.plugins.core.user.directory.Static");
604        userDirectory1.put("id", "static");
605        userDirectory1.put("org.ametys.plugins.core.user.directory.Static$runtime.users.static.users", SYSTEM_USER_LOGIN + ":System:User:");
606        
607        Map<String, String> userDirectory2 = new HashMap<>();
608        userDirectory2.put("udModelId", "org.ametys.plugins.core.user.directory.Jdbc");
609        userDirectory2.put("org.ametys.plugins.core.user.directory.Jdbc$runtime.users.jdbc.datasource", SQLDataSourceManager.AMETYS_INTERNAL_DATASOURCE_ID);
610        userDirectory2.put("org.ametys.plugins.core.user.directory.Jdbc$runtime.users.jdbc.table", __ADMIN_TABLENAME);
611        
612        Map<String, String> credentialProvider = new HashMap<>();
613        String cpModelId = "org.ametys.core.authentication.FormBased";
614        credentialProvider.put("cpModelId", cpModelId);
615        credentialProvider.put(cpModelId + "$" + "runtime.authentication.form.cookies", "false");
616        credentialProvider.put(cpModelId + "$" + "runtime.authentication.form.captcha", "true");
617        credentialProvider.put(cpModelId + "$" + "runtime.authentication.form.login-by-email", "false");
618        credentialProvider.put(cpModelId + "$" + "runtime.authentication.form.security.storage", SQLDataSourceManager.AMETYS_INTERNAL_DATASOURCE_ID);
619
620        // We may need to create the admin user
621        boolean wasExisting = false;
622        try (Connection connection = ConnectionHelper.getInternalSQLDataSourceConnection())
623        {
624            wasExisting = SQLScriptHelper.tableExists(connection, __ADMIN_TABLENAME);
625        }
626        catch (Exception e)
627        {
628            throw new RuntimeException("Cannot test if " + __ADMIN_TABLENAME + " table exists in internal database", e);
629        }
630        
631        _fillUserPopulation(_adminUserPopulation, new I18nizableText("plugin.core", "PLUGINS_CORE_USER_POPULATION_ADMIN_LABEL"), Arrays.asList(userDirectory1, userDirectory2), Collections.singletonList(credentialProvider), Collections.emptyMap(), Collections.emptyMap(), Collections.emptyMap(), Collections.emptyMap());
632        
633        ((StaticUserDirectory) _adminUserPopulation.getUserDirectory("static")).setGrantAllCredentials(false);
634        
635        if (!wasExisting)
636        {
637            Map<String, String> adminUserInformations = new HashMap<>();
638            adminUserInformations.put("login", "admin");
639            adminUserInformations.put("password", "admin");
640            adminUserInformations.put("firstname", "User");
641            adminUserInformations.put("lastname", "Administrator");
642            adminUserInformations.put("email", "");
643            
644            ModifiableUserDirectory adminJdbcUserDirectoy = (ModifiableUserDirectory) _adminUserPopulation.getUserDirectories().get(1);
645            try
646            {
647                adminJdbcUserDirectoy.add(adminUserInformations);
648            }
649            catch (InvalidModificationException e)
650            {
651                throw new RuntimeException("Cannot create the 'admin' user", e);
652            }
653        }
654        
655        return _adminUserPopulation;
656    }
657    
658    /**
659     * Adds a new population
660     * @param id The unique id of the population
661     * @param label The label of the population
662     * @param userDirectories A list of user directory parameters
663     * @param credentialProviders A list of credential provider parameters
664     * @return A map containing the id of the created population, or the kind of error that occured
665     */
666    @Callable
667    public Map<String, Object> add(String id, String label, List<Map<String, String>> userDirectories, List<Map<String, String>> credentialProviders)
668    {
669        _readPopulations(false);
670        
671        Map<String, Object> result = new LinkedHashMap<>();
672        
673        if (!_isCorrectId(id))
674        {
675            return null;
676        }
677        
678        UserPopulation up = new UserPopulation();
679        up.setId(id);
680        
681        _fillUserPopulation(up, new I18nizableText(label), userDirectories, credentialProviders, Collections.emptyMap(), Collections.emptyMap(), Collections.emptyMap(), Collections.emptyMap());
682        
683        _userPopulations.put(id, up);
684        if (_writePopulations())
685        {
686            getLogger().error("An error occured when writing the configuration file which contains the user populations.", id);
687            result.put("error", "server");
688            return result;
689        }
690        
691        if (_getObservationManager() != null)
692        {
693            Map<String, Object> eventParams = new HashMap<>();
694            eventParams.put(ObservationConstants.ARGS_USERPOPULATION_ID, id);
695            _getObservationManager().notify(new Event(ObservationConstants.EVENT_USERPOPULATION_ADDED, _getCurrentUserProvider().getUser(), eventParams));
696        }
697        
698        result.put("id", id);
699        return result;
700    }
701    
702    private boolean _isCorrectId(String id)
703    {
704        if (_userPopulations.get(id) != null || ADMIN_POPULATION_ID.equals(id))
705        {
706            getLogger().error("The id '{}' is already used for a population.", id);
707            return false;
708        }
709        
710        if (!Pattern.matches(__ID_REGEX, id))
711        {
712            getLogger().error("The id '{}' is not a correct id for a user population.", id);
713            return false;
714        }
715        
716        return true;
717    }
718    
719    /**
720     * Edits the given population.
721     * @param id The id of the population to edit
722     * @param label The label of the population
723     * @param userDirectories A list of user directory parameters
724     * @param credentialProviders A list of credential provider parameters
725     * @return A map containing the id of the edited population, or the kind of error that occured
726     */
727    @Callable
728    public Map<String, Object> edit(String id, String label, List<Map<String, String>> userDirectories, List<Map<String, String>> credentialProviders)
729    {
730        _readPopulations(false);
731        
732        Map<String, Object> result = new LinkedHashMap<>();
733        
734        UserPopulation up = _userPopulations.get(id);
735        if (up == null)
736        {
737            getLogger().error("The UserPopulation with id '{}' does not exist, it cannot be edited.", id);
738            result.put("error", "unknown");
739            return result;
740        }
741        
742        Map<String, Map<String, Object>> udParameters = up.getUserDirectories().stream()
743                                                                               .collect(Collectors.toMap(UserDirectory::getId, UserDirectory::getParameterValues));
744        
745        Map<String, String> udModels = up.getUserDirectories().stream().collect(Collectors.toMap(UserDirectory::getId, UserDirectory::getUserDirectoryModelId));
746        
747        Map<String, Map<String, Object>> cpParameters = up.getCredentialProviders().stream()
748                                                                                   .collect(Collectors.toMap(CredentialProvider::getId, CredentialProvider::getParameterValues));
749
750        Map<String, String> cpModels = up.getCredentialProviders().stream().collect(Collectors.toMap(CredentialProvider::getId, CredentialProvider::getCredentialProviderModelId));
751        
752        up.dispose();
753        _fillUserPopulation(up, new I18nizableText(label), userDirectories, credentialProviders, udParameters, cpParameters, udModels, cpModels);
754        
755        if (_writePopulations())
756        {
757            getLogger().error("An error occured when writing the configuration file which contains the user populations.", id);
758            result.put("error", "server");
759            return result;
760        }
761        
762        if (_getObservationManager() != null)
763        {
764            Map<String, Object> eventParams = new HashMap<>();
765            eventParams.put(ObservationConstants.ARGS_USERPOPULATION_ID, id);
766            _getObservationManager().notify(new Event(ObservationConstants.EVENT_USERPOPULATION_UPDATED, _getCurrentUserProvider().getUser(), eventParams));
767        }
768        
769        result.put("id", id);
770        return result;
771    }
772    
773    private void _fillUserPopulation(UserPopulation up, I18nizableText label, List<Map<String, String>> userDirectories, List<Map<String, String>> credentialProviders, Map<String, Map<String, Object>> existingUdParameters, Map<String, Map<String, Object>> existingCpParameters, Map<String, String> udModels, Map<String, String> cpModels)
774    {
775        up.setLabel(label);
776        
777        // Create the user directories
778        List<UserDirectory> uds = new ArrayList<>();
779        for (Map<String, String> userDirectoryParameters : userDirectories)
780        {
781            String id = userDirectoryParameters.remove("id");
782            String modelId = userDirectoryParameters.remove("udModelId");
783            String additionnalLabel = userDirectoryParameters.remove("label");
784            Map<String, Object> typedParamValues = _getTypedUDParameters(userDirectoryParameters, modelId);
785            
786            if (StringUtils.isBlank(id))
787            {
788                id = org.ametys.core.util.StringUtils.generateKey();
789            }
790            else
791            {
792                _keepExistingUserDirectoryPassword(modelId, typedParamValues, existingUdParameters.getOrDefault(id, Collections.emptyMap()), udModels.get(id));
793            }
794            
795            uds.add(_getUserDirectoryFactory().createUserDirectory(id, modelId, typedParamValues, up.getId(), additionnalLabel));
796        }
797        up.setUserDirectories(uds);
798        
799        // Create the credential providers
800        List<CredentialProvider> cps = new ArrayList<>();
801        for (Map<String, String> credentialProviderParameters : credentialProviders)
802        {
803            String id = credentialProviderParameters.remove("id");
804            String modelId = credentialProviderParameters.remove("cpModelId");
805            String additionnalLabel = credentialProviderParameters.remove("label");
806            Map<String, Object> typedParamValues = _getTypedCPParameters(credentialProviderParameters, modelId);
807            
808            if (StringUtils.isBlank(id))
809            {
810                id = org.ametys.core.util.StringUtils.generateKey();
811            }
812            else
813            {
814                _keepExistingCredentialProviderPassword(modelId, typedParamValues, existingCpParameters.getOrDefault(id, Collections.emptyMap()), cpModels.get(id));
815            }
816            
817            CredentialProvider credentialProvider = _getCredentialProviderFactory().createCredentialProvider(id, modelId, typedParamValues, additionnalLabel);
818            if (credentialProvider != null)
819            {
820                cps.add(credentialProvider);
821            }
822        }
823        up.setCredentialProviders(cps);
824    }
825
826    private void _keepExistingUserDirectoryPassword(String modelId, Map<String, Object> typedParamValues, Map<String, Object> existingUdParameters, String existingModelId)
827    {
828        // An existing id means an existing user directory
829        if (StringUtils.equals(modelId, existingModelId))
830        {
831            // The model did not changed, we want to keep the unmodified passwords
832            UserDirectoryModel userDirectoryModel = _getUserDirectoryFactory().getExtension(modelId);
833            for (Map.Entry<String, ? extends ElementDefinition> parameterEntry : userDirectoryModel.getParameters().entrySet())
834            {
835                ElementDefinition parameter = parameterEntry.getValue();
836                // If the parameter is a password AND has no value
837                if (ModelItemTypeConstants.PASSWORD_ELEMENT_TYPE_ID.equals(parameter.getType().getId()) && typedParamValues.get(parameterEntry.getKey()) == null)
838                {
839                    // Update the submitted data with the existing password
840                    typedParamValues.put(parameterEntry.getKey(), existingUdParameters.get(parameterEntry.getKey()));
841                }
842            }
843        }
844    }
845
846    private void _keepExistingCredentialProviderPassword(String modelId, Map<String, Object> typedParamValues, Map<String, Object> existingCpParameters, String existingModelId)
847    {
848        // An existing id means an existing credential provider
849        if (StringUtils.equals(modelId, existingModelId))
850        {
851            // The model did not changed, we want to keep the unmodified passwords
852            CredentialProviderModel credentialProviderModel = _getCredentialProviderFactory().getExtension(modelId);
853            for (Map.Entry<String, ? extends ElementDefinition> parameterEntry : credentialProviderModel.getParameters().entrySet())
854            {
855                ElementDefinition parameter = parameterEntry.getValue();
856                // If the parameter is a password AND has no value
857                if (ModelItemTypeConstants.PASSWORD_ELEMENT_TYPE_ID.equals(parameter.getType().getId()) && typedParamValues.get(parameterEntry.getKey()) == null)
858                {
859                    // Update the submitted data with the existing password
860                    typedParamValues.put(parameterEntry.getKey(), existingCpParameters.get(parameterEntry.getKey()));
861                }
862            }
863        }
864    }
865    
866    private Map<String, Object> _getTypedUDParameters(Map<String, String> parameters, String modelId)
867    {
868        Map<String, Object> resultParameters = new LinkedHashMap<>();
869        
870        UserDirectoryModel model = _getUserDirectoryFactory().getExtension(modelId);
871        if (model == null)
872        {
873            throw new IllegalStateException("The user population configuration refers to an unexisting extension for the user directory '" + modelId + "'");
874        }
875
876        Map<String, ? extends ElementDefinition> declaredParameters = model.getParameters();
877        for (String paramNameWithPrefix : parameters.keySet())
878        {
879            String[] splitStr = paramNameWithPrefix.split("\\$", 2);
880            String prefix = splitStr[0];
881            String paramName = splitStr[1];
882            if (prefix.equals(modelId) && declaredParameters.containsKey(paramName))
883            {
884                String originalValue = parameters.get(paramNameWithPrefix);
885                
886                ElementDefinition parameter = declaredParameters.get(paramName);
887                ElementType type = parameter.getType();
888                
889                Object typedValue = type.castValue(originalValue);
890                resultParameters.put(paramName, typedValue);
891            }
892            else if (prefix.equals(modelId))
893            {
894                getLogger().warn("The parameter {} is not declared in extension {}. It will be ignored", paramName, modelId);
895            }
896        }
897        
898        return resultParameters;
899    }
900    
901    private Map<String, Object> _getTypedCPParameters(Map<String, String> parameters, String modelId)
902    {
903        Map<String, Object> resultParameters = new LinkedHashMap<>();
904        
905        CredentialProviderModel model = _getCredentialProviderFactory().getExtension(modelId);
906        if (model == null)
907        {
908            throw new IllegalStateException("The user population configuration refers to an unexisting extension for the credential provider '" + modelId + "'");
909        }
910        
911        Map<String, ? extends ElementDefinition> declaredParameters = model.getParameters();
912        for (String paramNameWithPrefix : parameters.keySet())
913        {
914            String[] splitStr = paramNameWithPrefix.split("\\$", 2);
915            String prefix = splitStr[0];
916            String paramName = splitStr[1];
917            if (prefix.equals(modelId) && declaredParameters.containsKey(paramName))
918            {
919                String originalValue = parameters.get(paramNameWithPrefix);
920                
921                ElementDefinition parameter = declaredParameters.get(paramName);
922                ElementType type = parameter.getType();
923                
924                Object typedValue = type.castValue(originalValue);
925                resultParameters.put(paramName, typedValue);
926            }
927            else if (prefix.equals(modelId))
928            {
929                getLogger().warn("The parameter {} is not declared in extension {}. It will be ignored", paramName, modelId);
930            }
931        }
932        
933        return resultParameters;
934    }
935
936    /**
937     * Removes the given population.
938     * @param id The id of the population to remove
939     * @return A map containing the id of the removed population
940     */
941    @Callable
942    public Map<String, Object> remove(String id)
943    {
944        return remove(id, false);
945    }
946    
947    /**
948     * Removes the given population.
949     * @param id The id of the population to remove
950     * @param forceDeletion Delete the population even if it is still in use
951     * @return A map containing the id of the removed population
952     */
953    public Map<String, Object> remove(String id, boolean forceDeletion)
954    {
955        Map<String, Object> result = new LinkedHashMap<>();
956        
957        // Check if the population is not the admin population
958        if (ADMIN_POPULATION_ID.equals("id"))
959        {
960            return null;
961        }
962        
963        // Check if the population is used
964        if (!forceDeletion && _getPopulationConsumerExtensionPoint().isInUse(id))
965        {
966            getLogger().error("The UserPopulation with id '{}' is used, it cannot be removed.", id);
967            result.put("error", "used");
968            return result;
969        }
970        
971        _readPopulations(false);
972        
973        UserPopulation up = _userPopulations.remove(id);
974        
975        if (up == null)
976        {
977            getLogger().error("The UserPopulation with id '{}' does not exist, it cannot be removed.", id);
978            result.put("error", "unknown");
979            return result;
980        }
981        
982        up.dispose();
983        
984        if (_writePopulations())
985        {
986            result.put("error", "server");
987            return result;
988        }
989        
990        if (_getObservationManager() != null)
991        {
992            Map<String, Object> eventParams = new HashMap<>();
993            eventParams.put(ObservationConstants.ARGS_USERPOPULATION_ID, id);
994            _getObservationManager().notify(new Event(ObservationConstants.EVENT_USERPOPULATION_DELETED, _getCurrentUserProvider().getUser(), eventParams));
995        }
996        
997        result.put("id", id);
998        return result;
999    }
1000    
1001    /**
1002     * Enables/Disables the given population
1003     * @param populationId The id of the population to enable/disable
1004     * @param enabled True to enable the population, false to disable it.
1005     * @return A map containing the id of the enabled/disabled population, or with an error.
1006     */
1007    @Callable
1008    public Map<String, Object> enable(String populationId, boolean enabled)
1009    {
1010        Map<String, Object> result = new LinkedHashMap<>();
1011        
1012        UserPopulation population = getUserPopulation(populationId);
1013        if (population != null)
1014        {
1015            population.enable(enabled);
1016            result.put("id", populationId);
1017        }
1018        else
1019        {
1020            getLogger().error("The UserPopulation with id '{}' does not exist, it cannot be enabled/disabled.", populationId);
1021            result.put("error", "unknown");
1022        }
1023        
1024        if (_writePopulations())
1025        {
1026            result.put("error", "server");
1027            return result;
1028        }
1029        
1030        return result;
1031    }
1032    
1033    /**
1034     * Determines if a population has a valid configuration
1035     * @param populationId The id of the population to retrieve state
1036     * @return A map, with the response as a boolean, or an error.
1037     */
1038    @Callable
1039    public boolean isValid(String populationId)
1040    {
1041        return !_misconfiguredUserPopulations.contains(populationId);
1042    }
1043    
1044    /**
1045     * Returns the enabled state of the given population
1046     * @param populationId The id of the population to retrieve state
1047     * @return A map, with the response as a booolean, or an error.
1048     */
1049    @Callable
1050    public Map<String, Object> isEnabled(String populationId)
1051    {
1052        Map<String, Object> result = new LinkedHashMap<>();
1053        
1054        UserPopulation population = getUserPopulation(populationId);
1055        if (population != null)
1056        {
1057            result.put("enabled", population.isEnabled());
1058        }
1059        else
1060        {
1061            result.put("error", "unknown");
1062        }
1063        
1064        return result;
1065    }
1066    
1067    /**
1068     * Determines if a population can be removed
1069     * @param populationId The id of the population
1070     * @return A map, with the response as a boolean, or an error.
1071     */
1072    @Callable
1073    public Map<String, Object> canRemove(String populationId)
1074    {
1075        Map<String, Object> result = new HashMap<>();
1076        
1077        //Can remove only if not used, event if the population is disabled
1078        result.put("canRemove", !_getPopulationConsumerExtensionPoint().isInUse(populationId));
1079        
1080        return result;
1081    }
1082    
1083    /**
1084     * If needed, reads the config file representing the populations and then
1085     * reinitializes and updates the internal representation of the populations.
1086     * @param forceRead True to avoid the use of the cache and force the reading of the file
1087     */
1088    private synchronized void _readPopulations(boolean forceRead)
1089    {
1090        try
1091        {
1092            if (!__USER_POPULATIONS_FILE.exists())
1093            {
1094                if (getLogger().isDebugEnabled())
1095                {
1096                    getLogger().debug("No user population file found at {}. Creating a new one.", __USER_POPULATIONS_FILE.getAbsolutePath());
1097                }
1098                _createPopulationsFile(__USER_POPULATIONS_FILE);
1099            }
1100            
1101            // In Linux file systems, the precision of java.io.File.lastModified() is the second, so we need here to always have
1102            // this (bad!) precision by doing the truncation to second precision (/1000 * 1000) on the millis time value.
1103            // Therefore, the boolean outdated is computed with '>=' operator, and not '>', which will lead to sometimes (but rarely) unnecessarily re-read the file.
1104            long fileLastModified = (__USER_POPULATIONS_FILE.lastModified() / 1000) * 1000;
1105            if (forceRead || fileLastModified >= _lastFileReading)
1106            {
1107                getLogger().debug("Reading user population file at {}", __USER_POPULATIONS_FILE.getAbsolutePath());
1108                
1109                long lastFileReading = Instant.now().truncatedTo(ChronoUnit.SECONDS).toEpochMilli();
1110                Map<String, UserPopulation> userPopulations = new LinkedHashMap<>();
1111                Set<String> ignoredPopulations = new HashSet<>();
1112                Set<String> misconfiguredUserPopulations = new HashSet<>();
1113
1114                Configuration cfg = new DefaultConfigurationBuilder().buildFromFile(__USER_POPULATIONS_FILE);
1115                for (Configuration childCfg : cfg.getChildren("userPopulation"))
1116                {
1117                    try
1118                    {
1119                        UserPopulation up = _configurePopulation(childCfg, misconfiguredUserPopulations);
1120                        userPopulations.put(up.getId(), up);
1121                    }
1122                    catch (ConfigurationException e)
1123                    {
1124                        getLogger().error("Fatal configuration error for population of id '{}'. The population will be ignored.", childCfg.getAttribute("id", ""), e);
1125                        ignoredPopulations.add(childCfg.getAttribute("id", ""));
1126                    }
1127                }
1128                
1129                getLogger().debug("User population file read. Found {} valid population(s), {} invalid population(s) and {} misconfigured population(s)", userPopulations.size(), ignoredPopulations.size(), misconfiguredUserPopulations.size());
1130                
1131                // Release previous components
1132                this.dispose();
1133                
1134                _lastFileReading = lastFileReading;
1135                _userPopulations = userPopulations;
1136                _ignoredPopulations = ignoredPopulations;
1137                _misconfiguredUserPopulations = misconfiguredUserPopulations;
1138            }
1139            else if (getLogger().isDebugEnabled())
1140            {
1141                getLogger().debug("No need to reload the user population file. The file modified at {} was not modified since {}", fileLastModified, _lastFileReading);
1142            }
1143        }
1144        catch (Exception e)
1145        {
1146            getLogger().error("Failed to retrieve user populations from the configuration file " + __USER_POPULATIONS_FILE, e);
1147        }
1148    }
1149    
1150    private void _createPopulationsFile(File file) throws IOException, TransformerConfigurationException, SAXException
1151    {
1152        file.createNewFile();
1153        try (OutputStream os = new FileOutputStream(file))
1154        {
1155            // create a transformer for saving sax into a file
1156            TransformerHandler th = ((SAXTransformerFactory) TransformerFactory.newInstance()).newTransformerHandler();
1157            
1158            StreamResult result = new StreamResult(os);
1159            th.setResult(result);
1160
1161            // create the format of result
1162            Properties format = new Properties();
1163            format.put(OutputKeys.METHOD, "xml");
1164            format.put(OutputKeys.INDENT, "yes");
1165            format.put(OutputKeys.ENCODING, "UTF-8");
1166            format.put(OutputPropertiesFactory.S_KEY_INDENT_AMOUNT, "4");
1167            th.getTransformer().setOutputProperties(format);
1168            th.startDocument();
1169            XMLUtils.createElement(th, "userPopulations");
1170            th.endDocument();
1171        }
1172        
1173        if (getLogger().isDebugEnabled())
1174        {
1175            getLogger().debug("Empty file for user populations created");
1176        }
1177    }
1178    
1179    private UserPopulation _configurePopulation(Configuration configuration, Set<String> misconfiguredUserPopulations) throws ConfigurationException
1180    {
1181        UserPopulation up = new UserPopulation();
1182        
1183        String upId = configuration.getAttribute("id");
1184        up.setId(upId);
1185        up.setLabel(new I18nizableText(configuration.getChild("label").getValue()));
1186        
1187        List<UserDirectory> userDirectories = _configureUserDirectories(configuration, upId, misconfiguredUserPopulations);
1188        up.setUserDirectories(userDirectories);
1189        
1190        List<CredentialProvider> credentialProviders = _configureCredentialProviders(configuration, upId);
1191        up.setCredentialProviders(credentialProviders);
1192        
1193        boolean enabled = configuration.getAttributeAsBoolean("enabled", true) && userDirectories.size() > 0 && credentialProviders.size() > 0;
1194        up.enable(enabled);
1195        
1196        return up;
1197    }
1198    
1199    private List<UserDirectory> _configureUserDirectories(Configuration configuration, String upId, Set<String> misconfiguredUserPopulations) throws ConfigurationException
1200    {
1201        List<UserDirectory> userDirectories = new ArrayList<>();
1202        
1203        Configuration[] userDirectoriesConf = configuration.getChild("userDirectories").getChildren("userDirectory");
1204        for (Configuration userDirectoryConf : userDirectoriesConf)
1205        {
1206            String id = userDirectoryConf.getAttribute("id");
1207            String modelId = userDirectoryConf.getAttribute("modelId");
1208            String label = userDirectoryConf.getAttribute("label", null);
1209            
1210            try
1211            {
1212                Map<String, Object> paramValues = _getUDParametersFromConfiguration(userDirectoryConf, modelId, upId);
1213                UserDirectory ud = _getUserDirectoryFactory().createUserDirectory(id, modelId, paramValues, upId, label);
1214                if (ud != null)
1215                {
1216                    userDirectories.add(ud);
1217                }
1218            }
1219            catch (Exception e)
1220            {
1221                getLogger().warn("The population of id '" + upId + "' declares a user directory with an invalid configuration", e);
1222                misconfiguredUserPopulations.add(upId);
1223            }
1224        }
1225
1226        if (userDirectories.isEmpty())
1227        {
1228            misconfiguredUserPopulations.add(upId);
1229            getLogger().warn("The population of id '" + upId + "' does not have user directory with a valid configuration. It will be disabled until it will be fixed.");
1230        }
1231        
1232        return userDirectories;
1233    }
1234    
1235    private List<CredentialProvider> _configureCredentialProviders(Configuration configuration, String upId) throws ConfigurationException
1236    {
1237        List<CredentialProvider> credentialProviders = new ArrayList<>();
1238        
1239        Configuration[] credentialProvidersConf = configuration.getChild("credentialProviders").getChildren("credentialProvider");
1240        for (Configuration credentialProviderConf : credentialProvidersConf)
1241        {
1242            String id = credentialProviderConf.getAttribute("id");
1243            String modelId = credentialProviderConf.getAttribute("modelId");
1244            String additionnalLabel = credentialProviderConf.getAttribute("label", null);
1245            
1246            try
1247            {
1248                Map<String, Object> paramValues = _getCPParametersFromConfiguration(credentialProviderConf, modelId, upId);
1249                CredentialProvider cp = _getCredentialProviderFactory().createCredentialProvider(id, modelId, paramValues, additionnalLabel);
1250                if (cp != null)
1251                {
1252                    credentialProviders.add(cp);
1253                }
1254            }
1255            catch (Exception e)
1256            {
1257                getLogger().warn("The population of id '" + upId + "' declares a credential provider with an invalid configuration", e);
1258                _misconfiguredUserPopulations.add(upId);
1259            }
1260        }
1261        
1262        if (credentialProviders.isEmpty())
1263        {
1264            _misconfiguredUserPopulations.add(upId);
1265            getLogger().warn("The population of id '" + upId + "' does not have credential provider with a valid configuration. It will be disabled until it will be fixed.");
1266        }
1267        
1268        return credentialProviders;
1269    }
1270    
1271    /**
1272     * Get the typed parameters of a user directory used by a population
1273     * @param conf The user directory's configuration.
1274     * @param modelId The id of user directory model
1275     * @param populationId The id of population
1276     * @return The typed parameters
1277     * @throws IllegalArgumentException if the configured user directory references a non-existing user directory model
1278     * @throws ConfigurationException if a parameter is missing
1279     */
1280    private Map<String, Object> _getUDParametersFromConfiguration(Configuration conf, String modelId, String populationId) throws ConfigurationException, IllegalArgumentException
1281    {
1282        Map<String, Object> parameters = new LinkedHashMap<>();
1283        
1284        if (!_getUserDirectoryFactory().hasExtension(modelId))
1285        {
1286            throw new IllegalArgumentException(String.format("The population of id '%s' declares a non-existing user directory model with id '%s'. It will be ignored.", populationId, modelId));
1287        }
1288        
1289        Map<String, ? extends ElementDefinition> declaredParameters = _getUserDirectoryFactory().getExtension(modelId).getParameters();
1290        
1291        for (String udParamName : declaredParameters.keySet())
1292        {
1293            ElementType type = declaredParameters.get(udParamName).getType();
1294            assert type instanceof XMLElementType;
1295            Object typedValue = ((XMLElementType) type).read(conf, udParamName);
1296            
1297            if (typedValue == null)
1298            {
1299                throw new ConfigurationException(String.format("The population of id '%s' declares a user directory model with id '%s' but the parameter '%s' is missing. This user directory will be ignored.", populationId, modelId, udParamName));
1300            }
1301            else
1302            {
1303                parameters.put(udParamName, typedValue);
1304            }
1305        }
1306        
1307        return parameters;
1308    }
1309    
1310    /**
1311     * Get the typed parameters of a credential provider used by a population
1312     * @param conf The credential provider's configuration.
1313     * @param modelId The id of credential provider model
1314     * @param populationId The id of population
1315     * @return The typed parameters
1316     * @throws IllegalArgumentException if the configured credential provider references a non-existing credential provider model
1317     * @throws ConfigurationException if a parameter is missing
1318     */
1319    private Map<String, Object> _getCPParametersFromConfiguration(Configuration conf, String modelId, String populationId) throws ConfigurationException, IllegalArgumentException
1320    {
1321        Map<String, Object> parameters = new LinkedHashMap<>();
1322        
1323        if (!_getCredentialProviderFactory().hasExtension(modelId))
1324        {
1325            throw new IllegalArgumentException(String.format("The population of id '%s' declares a non-existing credential provider model with id '%s'. It will be ignored.", populationId, modelId));
1326        }
1327        
1328        Map<String, ? extends ElementDefinition> declaredParameters = _getCredentialProviderFactory().getExtension(modelId).getParameters();
1329        
1330        for (String cpParamName : declaredParameters.keySet())
1331        {
1332            ElementType type = declaredParameters.get(cpParamName).getType();
1333            assert type instanceof XMLElementType;
1334            Object typedValue = ((XMLElementType) type).read(conf, cpParamName);
1335
1336            if (typedValue == null)
1337            {
1338                throw new ConfigurationException(String.format("The population of id '%s' declares a credential provider model with id '%s' but the parameter '%s' is missing. This credential provider will be ignored.", populationId, modelId, cpParamName));
1339            }
1340            else
1341            {
1342                parameters.put(cpParamName, typedValue);
1343            }
1344        }
1345
1346        return parameters;
1347    }
1348    
1349    /**
1350     * Erases the config file representing the populations and rebuild it 
1351     * from the internal representation of the populations.
1352     * @return True if an error occured
1353     */
1354    private boolean _writePopulations()
1355    {
1356        File backup = new File(__USER_POPULATIONS_FILE.getPath() + ".tmp");
1357        boolean errorOccured = false;
1358        
1359        // Create a backup file
1360        try
1361        {
1362            Files.copy(__USER_POPULATIONS_FILE.toPath(), backup.toPath());
1363        }
1364        catch (IOException e)
1365        {
1366            getLogger().error("Error when creating backup '" + __USER_POPULATIONS_FILE + "' file", e);
1367        }
1368        
1369        // Do writing
1370        try (OutputStream os = new FileOutputStream(__USER_POPULATIONS_FILE))
1371        {
1372            // create a transformer for saving sax into a file
1373            TransformerHandler th = ((SAXTransformerFactory) TransformerFactory.newInstance()).newTransformerHandler();
1374            
1375            StreamResult result = new StreamResult(os);
1376            th.setResult(result);
1377
1378            // create the format of result
1379            Properties format = new Properties();
1380            format.put(OutputKeys.METHOD, "xml");
1381            format.put(OutputKeys.INDENT, "yes");
1382            format.put(OutputKeys.ENCODING, "UTF-8");
1383            format.put(OutputPropertiesFactory.S_KEY_INDENT_AMOUNT, "4");
1384            th.getTransformer().setOutputProperties(format);
1385
1386            // sax the config
1387            try
1388            {
1389                _toSAX(th);
1390            }
1391            catch (Exception e)
1392            {
1393                getLogger().error("Error when saxing the userPopulations", e);
1394                errorOccured = true;
1395            }
1396        }
1397        catch (IOException | TransformerConfigurationException | TransformerFactoryConfigurationError e)
1398        {
1399            getLogger().error("Error when trying to modify the user populations with the configuration file " + __USER_POPULATIONS_FILE, e);
1400        }
1401        
1402        // Restore the file if an error previously occured
1403        try
1404        {
1405            if (errorOccured)
1406            {
1407                // An error occured, restore the original
1408                Files.copy(backup.toPath(), __USER_POPULATIONS_FILE.toPath(), StandardCopyOption.REPLACE_EXISTING);
1409                // Force to reread the file
1410                _readPopulations(true);
1411            }
1412            Files.deleteIfExists(backup.toPath());
1413        }
1414        catch (IOException e)
1415        {
1416            getLogger().error("Error when restoring backup '" + __USER_POPULATIONS_FILE + "' file", e);
1417        }
1418        
1419        return errorOccured;
1420    }
1421    
1422    private void _toSAX(TransformerHandler handler)
1423    {
1424        try
1425        {
1426            handler.startDocument();
1427            XMLUtils.startElement(handler, "userPopulations");
1428            for (UserPopulation up : _userPopulations.values())
1429            {
1430                _saxUserPopulation(up, handler);
1431            }
1432            
1433            XMLUtils.endElement(handler, "userPopulations");
1434            handler.endDocument();
1435        }
1436        catch (SAXException e)
1437        {
1438            getLogger().error("Error when saxing the userPopulations", e);
1439        }
1440    }
1441    
1442    private void _saxUserPopulation(UserPopulation userPopulation, TransformerHandler handler)
1443    {
1444        try
1445        {
1446            AttributesImpl atts = new AttributesImpl();
1447            atts.addCDATAAttribute("id", userPopulation.getId());
1448            atts.addCDATAAttribute("enabled", Boolean.toString(userPopulation.isEnabled()));
1449            XMLUtils.startElement(handler, "userPopulation", atts);
1450            
1451            userPopulation.getLabel().toSAX(handler, "label");
1452            
1453            // SAX user directories
1454            XMLUtils.startElement(handler, "userDirectories");
1455            for (UserDirectory ud : userPopulation.getUserDirectories())
1456            {
1457                AttributesImpl attr = new AttributesImpl();
1458                attr.addCDATAAttribute("id", ud.getId());
1459                attr.addCDATAAttribute("modelId", ud.getUserDirectoryModelId());
1460                attr.addCDATAAttribute("label", ud.getLabel() != null ? ud.getLabel() : "");
1461                XMLUtils.startElement(handler, "userDirectory", attr);
1462                
1463                UserDirectoryModel udModel = _getUserDirectoryFactory().getExtension(ud.getUserDirectoryModelId());
1464                Map<String, ? extends ElementDefinition> definitions = udModel.getParameters();
1465                
1466                Map<String, Object> paramValues = ud.getParameterValues();
1467                for (String paramName : paramValues.keySet())
1468                {
1469                    Object value = paramValues.get(paramName);
1470                    ElementDefinition definition = definitions.get(paramName);
1471                    if (definition != null)
1472                    {
1473                        definition.getType().valueToSAX(handler, paramName, value, DataContext.newInstance());
1474                    }
1475                }
1476                XMLUtils.endElement(handler, "userDirectory");
1477            }
1478            XMLUtils.endElement(handler, "userDirectories");
1479            
1480            // SAX credential providers
1481            XMLUtils.startElement(handler, "credentialProviders");
1482            for (CredentialProvider cp : userPopulation.getCredentialProviders())
1483            {
1484                AttributesImpl attr = new AttributesImpl();
1485                attr.addCDATAAttribute("id", cp.getId());
1486                attr.addCDATAAttribute("modelId", cp.getCredentialProviderModelId());
1487                attr.addCDATAAttribute("label", cp.getLabel() != null ? cp.getLabel() : "");
1488                XMLUtils.startElement(handler, "credentialProvider", attr);
1489
1490                CredentialProviderModel cpModel = _getCredentialProviderFactory().getExtension(cp.getCredentialProviderModelId());
1491                Map<String, ? extends ElementDefinition> definitions = cpModel.getParameters();
1492                
1493                Map<String, Object> paramValues = cp.getParameterValues();
1494                for (String paramName : paramValues.keySet())
1495                {
1496                    Object value = paramValues.get(paramName);
1497                    ElementDefinition definition = definitions.get(paramName);
1498                    if (definition != null)
1499                    {
1500                        definition.getType().valueToSAX(handler, paramName, value, DataContext.newInstance());
1501                    }
1502                }
1503                XMLUtils.endElement(handler, "credentialProvider");
1504            }
1505            XMLUtils.endElement(handler, "credentialProviders");
1506            
1507            XMLUtils.endElement(handler, "userPopulation");
1508        }
1509        catch (SAXException | IOException e)
1510        {
1511            getLogger().error("Error when saxing the userPopulation " + userPopulation, e);
1512        }
1513    }
1514    
1515    @Override
1516    public void dispose()
1517    {
1518        for (UserPopulation up : _userPopulations.values())
1519        {
1520            up.dispose();
1521        }
1522        
1523        _userPopulations.clear();
1524        _lastFileReading = 0;
1525    }
1526}