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