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