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.plugins.core.impl.user.directory;
017
018import java.sql.Connection;
019import java.sql.PreparedStatement;
020import java.sql.ResultSet;
021import java.sql.SQLException;
022import java.util.ArrayList;
023import java.util.Arrays;
024import java.util.Collection;
025import java.util.Collections;
026import java.util.HashMap;
027import java.util.LinkedHashMap;
028import java.util.List;
029import java.util.Map;
030
031import org.apache.avalon.framework.activity.Disposable;
032import org.apache.avalon.framework.component.Component;
033import org.apache.avalon.framework.context.Context;
034import org.apache.avalon.framework.context.ContextException;
035import org.apache.avalon.framework.context.Contextualizable;
036import org.apache.avalon.framework.service.ServiceException;
037import org.apache.avalon.framework.service.ServiceManager;
038import org.apache.avalon.framework.service.Serviceable;
039import org.apache.commons.codec.digest.DigestUtils;
040import org.apache.commons.lang.RandomStringUtils;
041import org.apache.commons.lang.StringUtils;
042import org.apache.commons.lang3.ArrayUtils;
043import org.apache.excalibur.source.SourceResolver;
044
045import org.ametys.core.ObservationConstants;
046import org.ametys.core.cache.AbstractCacheManager;
047import org.ametys.core.cache.Cache;
048import org.ametys.core.datasource.ConnectionHelper;
049import org.ametys.core.migration.MigrationExtensionPoint;
050import org.ametys.core.migration.storage.VersionStorageExtensionPoint;
051import org.ametys.core.migration.storage.impl.SqlVersionStorage;
052import org.ametys.core.observation.Event;
053import org.ametys.core.observation.ObservationManager;
054import org.ametys.core.script.SQLScriptHelper;
055import org.ametys.core.user.CurrentUserProvider;
056import org.ametys.core.user.InvalidModificationException;
057import org.ametys.core.user.User;
058import org.ametys.core.user.UserIdentity;
059import org.ametys.core.user.directory.ModifiableUserDirectory;
060import org.ametys.core.user.directory.NotUniqueUserException;
061import org.ametys.core.util.Cacheable;
062import org.ametys.core.util.mail.SendMailHelper;
063import org.ametys.plugins.core.jdbc.JdbcParameterTypeExtensionPoint;
064import org.ametys.runtime.i18n.I18nizableTextParameter;
065import org.ametys.runtime.i18n.I18nizableText;
066import org.ametys.runtime.model.ElementDefinition;
067import org.ametys.runtime.model.ModelHelper;
068import org.ametys.runtime.model.ModelItem;
069import org.ametys.runtime.model.View;
070import org.ametys.runtime.model.type.ModelItemType;
071import org.ametys.runtime.model.type.ModelItemTypeConstants;
072import org.ametys.runtime.parameter.DefaultValidator;
073import org.ametys.runtime.parameter.Errors;
074import org.ametys.runtime.parameter.Validator;
075import org.ametys.runtime.plugin.PluginsManager;
076import org.ametys.runtime.plugin.component.AbstractLogEnabled;
077import org.ametys.runtime.plugin.component.PluginAware;
078
079/**
080 * Use a jdbc driver for getting the list of users, modifying them and also
081 * authenticate them.<br>
082 * Passwords need to be encrypted with MD5 and encoded in base64.<br>
083 */
084public class JdbcUserDirectory extends AbstractLogEnabled implements ModifiableUserDirectory, Component, Serviceable, Contextualizable, PluginAware, Cacheable, Disposable
085{
086    /** The base plugin (for i18n key) */
087    protected static final String BASE_PLUGIN_NAME = "core";
088    
089    static final String[] __COLUMNS = new String[] {"login", "password", "firstname", "lastname", "email"};
090    static final String[] __ORDERBY_COLUMNS = new String[] {"lastname", "firstname"};
091    
092    /** Name of the parameter holding the datasource id */
093    private static final String __DATASOURCE_PARAM_NAME = "runtime.users.jdbc.datasource";
094    /** Name of the parameter holding the table users' name */
095    private static final String __USERS_TABLE_PARAM_NAME = "runtime.users.jdbc.table";
096    
097    private static final String __COLUMN_LOGIN = "login";
098    private static final String __COLUMN_PASSWORD = "password";
099    private static final String __COLUMN_FIRSTNAME = "firstname";
100    private static final String __COLUMN_LASTNAME = "lastname";
101    private static final String __COLUMN_EMAIL = "email";
102    private static final String __COLUMN_SALT = "salt";
103    
104    private static final String __JDBC_USERDIRECTORY_USER_BY_LOGIN_CACHE_NAME_PREFIX = JdbcUserDirectory.class.getName() + "$by.login$";
105    private static final String __JDBC_USERDIRECTORY_USER_BY_MAIL_CACHE_NAME_PREFIX = JdbcUserDirectory.class.getName() + "$by.mail$";
106    
107    /** The identifier of data source */
108    protected String _dataSourceId;
109    /** The name of users' SQL table */
110    protected String _userTableName;
111    
112    /** Model */
113    protected Map<String, ElementDefinition> _model;
114    
115    /** Plugin name */
116    protected String _pluginName;
117    
118    /** The avalon service manager */
119    protected ServiceManager _manager;
120    
121    /** The avalon context */
122    protected Context _context;
123    
124    /** The cocoon source resolver */
125    protected SourceResolver _sourceResolver;
126
127    private ObservationManager _observationManager;
128    private CurrentUserProvider _currentUserProvider;
129    
130    private String _udModelId;
131    private Map<String, Object> _paramValues;
132    private String _populationId;
133    
134    private String _label;
135
136    private String _id;
137    
138    // Cannot use _populationId + "#" + _id as two UserDirectories with same id can co-exist during a short amount of time (during UserPopulationDAO#_readPopulations)
139    private final String _uniqueCacheSuffix = org.ametys.core.util.StringUtils.generateKey();
140
141    private boolean _lazyInitialized;
142
143    private JdbcParameterTypeExtensionPoint _jdbcParameterTypeExtensionPoint;
144    private AbstractCacheManager _cacheManager;
145
146    private View _view;
147    
148    private MigrationExtensionPoint _migrationEP;
149    
150    private SqlVersionStorage _sqlVersionStorage;
151    
152    @Override
153    public void setPluginInfo(String pluginName, String featureName, String id)
154    {
155        _pluginName = pluginName;
156    }
157    
158    @Override
159    public void contextualize(Context context) throws ContextException
160    {
161        _context = context;
162    }
163    
164    @Override
165    public void service(ServiceManager manager) throws ServiceException
166    {
167        _manager = manager;
168        _sourceResolver = (SourceResolver) manager.lookup(SourceResolver.ROLE);
169        _jdbcParameterTypeExtensionPoint = (JdbcParameterTypeExtensionPoint) manager.lookup(JdbcParameterTypeExtensionPoint.ROLE);
170        _cacheManager = (AbstractCacheManager) manager.lookup(AbstractCacheManager.ROLE);
171        _migrationEP = (MigrationExtensionPoint) manager.lookup(MigrationExtensionPoint.ROLE);
172        VersionStorageExtensionPoint versionStorageEP = (VersionStorageExtensionPoint) manager.lookup(VersionStorageExtensionPoint.ROLE);
173        _sqlVersionStorage = (SqlVersionStorage) versionStorageEP.getExtension("sql");
174    }
175    
176    public String getId()
177    {
178        return _id;
179    }
180    
181    public String getFamilyId()
182    {
183        return JdbcUserDirectory.class.getName();
184    }
185    
186    public String getLabel()
187    {
188        return _label;
189    }
190    
191    @Override
192    public void dispose()
193    {
194        removeCaches();
195    }
196    
197    @Override
198    public Collection<SingleCacheConfiguration> getManagedCaches()
199    {
200        return Arrays.asList(
201                SingleCacheConfiguration.of(
202                        __JDBC_USERDIRECTORY_USER_BY_LOGIN_CACHE_NAME_PREFIX + _uniqueCacheSuffix, 
203                        _buildI18n("PLUGINS_CORE_USERS_JDBC_CACHE_BY_LOGIN_LABEL"), 
204                        _buildI18n("PLUGINS_CORE_USERS_JDBC_CACHE_BY_LOGIN_DESC")),
205                SingleCacheConfiguration.of(
206                        __JDBC_USERDIRECTORY_USER_BY_MAIL_CACHE_NAME_PREFIX + _uniqueCacheSuffix, 
207                        _buildI18n("PLUGINS_CORE_USERS_JDBC_CACHE_BY_MAIL_LABEL"), 
208                        _buildI18n("PLUGINS_CORE_USERS_JDBC_CACHE_BY_MAIL_DESC"))
209        );
210    }
211    
212    private I18nizableText _buildI18n(String i18Key)
213    {
214        String catalogue = "plugin.core-impl";
215        I18nizableText userDirectoryId = new I18nizableText(getPopulationId() + "#" + getId());
216        Map<String, I18nizableTextParameter> params = Map.of("id", userDirectoryId);
217        return new I18nizableText(catalogue, i18Key, params);
218    }
219    
220    private Cache<String, User> _getCacheByLogin()
221    {
222        return getCache(__JDBC_USERDIRECTORY_USER_BY_LOGIN_CACHE_NAME_PREFIX + _uniqueCacheSuffix);
223    }
224    
225    private Cache<String, User> _getCacheByMail()
226    {
227        return getCache(__JDBC_USERDIRECTORY_USER_BY_MAIL_CACHE_NAME_PREFIX + _uniqueCacheSuffix);
228    }
229    
230    @Override
231    public AbstractCacheManager getCacheManager()
232    {
233        return _cacheManager;
234    }
235    
236    @Override
237    public void init(String id, String udModelId, Map<String, Object> paramValues, String label)
238    {
239        _id = id;
240        _udModelId = udModelId;
241        _paramValues = paramValues;
242        _label = label;
243        
244        _userTableName = (String) paramValues.get(__USERS_TABLE_PARAM_NAME);
245        _dataSourceId = (String) paramValues.get(__DATASOURCE_PARAM_NAME);
246        
247        _initModelParameters();
248        
249        createCaches();
250    }
251    
252    private void _initModelParameters()
253    {
254        _model = new LinkedHashMap<>();
255        
256        I18nizableText invalidLoginText = new I18nizableText("plugin." + BASE_PLUGIN_NAME, "PLUGINS_CORE_USERS_JDBC_FIELD_LOGIN_INVALID");
257        Validator loginValidator = new DefaultValidator("^[a-zA-Z0-9_\\-\\.@]{3,64}$", invalidLoginText, true);
258        _initModelParameter(__COLUMN_LOGIN, ModelItemTypeConstants.STRING_TYPE_ID, "PLUGINS_CORE_USERS_JDBC_FIELD_LOGIN_LABEL", "PLUGINS_CORE_USERS_JDBC_FIELD_LOGIN_DESCRIPTION", loginValidator);
259
260        _initModelParameter(__COLUMN_PASSWORD, ModelItemTypeConstants.PASSWORD_ELEMENT_TYPE_ID, "PLUGINS_CORE_USERS_JDBC_FIELD_PASSWORD_LABEL", "PLUGINS_CORE_USERS_JDBC_FIELD_PASSWORD_DESCRIPTION", null);
261        
262        _initModelParameter(__COLUMN_FIRSTNAME, ModelItemTypeConstants.STRING_TYPE_ID, "PLUGINS_CORE_USERS_JDBC_FIELD_FIRSTNAME_LABEL", "PLUGINS_CORE_USERS_JDBC_FIELD_FIRSTNAME_DESCRIPTION", null);
263        
264        _initModelParameter(__COLUMN_LASTNAME, ModelItemTypeConstants.STRING_TYPE_ID, "PLUGINS_CORE_USERS_JDBC_FIELD_LASTNAME_LABEL", "PLUGINS_CORE_USERS_JDBC_FIELD_LASTNAME_DESCRIPTION", null);
265
266        I18nizableText invalidEmailText = new I18nizableText("plugin." + BASE_PLUGIN_NAME, "PLUGINS_CORE_USERS_JDBC_FIELD_EMAIL_INVALID");
267        Validator emailValidator = new DefaultValidator(SendMailHelper.EMAIL_VALIDATION_REGEXP, invalidEmailText, false);
268        _initModelParameter(__COLUMN_EMAIL, ModelItemTypeConstants.STRING_TYPE_ID, "PLUGINS_CORE_USERS_JDBC_FIELD_EMAIL_LABEL", "PLUGINS_CORE_USERS_JDBC_FIELD_EMAIL_DESCRIPTION", emailValidator);
269    }
270    
271    private void _initModelParameter(String name, String parameterType, String labelKey, String descriptionKey, Validator validator)
272    {
273        ModelItemType modelItemType = _jdbcParameterTypeExtensionPoint.getExtension(parameterType);
274
275        ElementDefinition parameter = new ElementDefinition<>();
276        parameter.setPluginName(BASE_PLUGIN_NAME);
277        parameter.setType(modelItemType);
278        parameter.setName(name);
279        parameter.setLabel(new I18nizableText("plugin." + BASE_PLUGIN_NAME, labelKey));
280        parameter.setDescription(new I18nizableText("plugin." + BASE_PLUGIN_NAME, descriptionKey));
281        parameter.setValidator(validator != null ? validator : new DefaultValidator(null, true));
282        
283        _model.put(name, parameter);
284    }
285    
286    /**
287     * Lazy lookup the {@link ObservationManager}
288     * @return the observation manager
289     */
290    protected ObservationManager getObservationManager()
291    {
292        if (_observationManager == null)
293        {
294            try
295            {
296                _observationManager = (ObservationManager) _manager.lookup(ObservationManager.ROLE);
297            }
298            catch (ServiceException e)
299            {
300                // We may be in safe mode
301                if (PluginsManager.getInstance().isSafeMode())
302                {
303                    getLogger().debug("Unable to lookup ObservationManager component in safe mode", e);
304                }
305                else
306                {
307                    getLogger().error("Unable to lookup ObservationManager component", e);
308                }
309            }
310        }
311        return _observationManager;
312    }
313    
314    /**
315     * Lazy lookup the {@link CurrentUserProvider}
316     * @return the current user provider
317     */
318    protected CurrentUserProvider getCurrentUserProvider()
319    {
320        if (_currentUserProvider == null)
321        {
322            try
323            {
324                _currentUserProvider = (CurrentUserProvider) _manager.lookup(CurrentUserProvider.ROLE);
325            }
326            catch (ServiceException e)
327            {
328                throw new RuntimeException("Unable to lookup CurrentUserProvider component", e);
329            }
330        }
331        return _currentUserProvider;
332    }
333    
334    /**
335     * Get the connection to the database 
336     * @return the SQL connection
337     */
338    @SuppressWarnings("unchecked")
339    protected Connection getSQLConnection()
340    {
341        Connection connection = ConnectionHelper.getConnection(_dataSourceId);
342        
343        if (!_lazyInitialized)
344        {
345            try
346            {
347                String componentId = "user-directory.jdbc";
348                String versionId = "org.ametys.plugins.core.user.directory.Jdbc.upgrade_" + _userTableName;
349                SQLScriptHelper.createTableIfNotExists(_dataSourceId, _userTableName, "plugin:core://scripts/%s/jdbc_users.template.sql", _sourceResolver, 
350                        (Map) ArrayUtils.toMap(new String[][] {{"%TABLENAME%", _userTableName}}), componentId, versionId, _migrationEP, _sqlVersionStorage);
351            }
352            catch (Exception e)
353            {
354                getLogger().error("The tables requires by the " + this.getClass().getName() + " could not be created. A degraded behavior will occur", e);
355            }
356            
357            _lazyInitialized = true;
358        }
359        
360        return connection;
361    }
362    
363    @Override
364    public void setPopulationId(String populationId)
365    {
366        _populationId = populationId;
367    }
368    
369    @Override
370    public String getPopulationId()
371    {
372        return _populationId;
373    }
374    
375    @Override
376    public Map<String, Object> getParameterValues()
377    {
378        return _paramValues;
379    }
380    
381    @Override
382    public String getUserDirectoryModelId()
383    {
384        return _udModelId;
385    }
386    
387    @SuppressWarnings("unchecked")
388    @Override
389    public Collection<User> getUsers()
390    {
391        return getUsers(Integer.MAX_VALUE, 0, Collections.EMPTY_MAP);
392    }
393    
394    @Override
395    public List<User> getUsers(int count, int offset, Map<String, Object> parameters)
396    {
397        String pattern = StringUtils.defaultIfEmpty((String) parameters.get("pattern"), null);
398        int boundedCount = count >= 0 ? count : Integer.MAX_VALUE;
399        int boundedOffset = offset >= 0 ? offset : 0;
400        
401        SelectUsersJdbcQueryExecutor<List<User>> queryExecutor = new SelectUsersJdbcQueryExecutor<>(pattern, boundedCount, boundedOffset) 
402        {
403            @Override
404            protected List<User> processResultSet(ResultSet rs) throws SQLException
405            {
406                Cache<String, User> cache = isCachingEnabled() ? _getCacheByLogin() : null;
407                return _getUsersProcessResultSet(rs, cache);
408            }
409        };
410        
411        return queryExecutor.run();
412    }
413
414    @Override
415    public User getUser(String login)
416    {
417        if (isCachingEnabled() && _getCacheByLogin().hasKey(login))
418        {
419            User user = _getCacheByLogin().get(login);
420            return user;
421        }
422        
423        SelectUserJdbcQueryExecutor<User> queryExecutor = new SelectUserJdbcQueryExecutor<>(login)
424        {
425            @Override
426            protected User processResultSet(ResultSet rs) throws SQLException
427            {
428                Cache<String, User> cache = isCachingEnabled() ? _getCacheByLogin() : null;
429                return _getUserProcessResultSet(rs, login, cache);
430            }
431        };
432        
433        return queryExecutor.run();
434    }
435    
436    @Override
437    public User getUserByEmail(String email) throws NotUniqueUserException
438    {
439        if (isCachingEnabled() && _getCacheByMail().hasKey(email))
440        {
441            User user = _getCacheByMail().get(email);
442            return user;
443        }
444        
445        SelectUserJdbcQueryExecutor<List<User>> queryExecutor = new SelectUserJdbcQueryExecutor<>(email, __COLUMN_EMAIL)
446        {
447            @Override
448            protected List<User> processResultSet(ResultSet rs) throws SQLException
449            {
450                Cache<String, User> mailCache = isCachingEnabled() ? _getCacheByMail() : null;
451                return _getUsersProcessResultSet(rs, mailCache);
452            }
453        };
454        
455        List<User> users = queryExecutor.run();
456        if (users.size() == 1)
457        {
458            return users.get(0);
459        }
460        else if (users.isEmpty())
461        {
462            return null;
463        }
464        else
465        {
466            throw new NotUniqueUserException("Find " + users.size() + " users matching the email " + email);
467        }
468        
469    }
470    
471    @Override
472    public boolean checkCredentials(String login, String password)
473    {
474        boolean updateNeeded = false;
475        
476        Connection con = null;
477        PreparedStatement stmt = null;
478        ResultSet rs = null;
479        try
480        {
481            // Connect to the database with connection pool
482            con = getSQLConnection();
483
484            // Build request for authenticating the user
485            String sql = "SELECT " + __COLUMN_LOGIN + ", " + __COLUMN_PASSWORD + ", " + __COLUMN_SALT + " FROM " + _userTableName + " WHERE " + __COLUMN_LOGIN + " = ?";
486            if (getLogger().isDebugEnabled())
487            {
488                getLogger().debug(sql);
489            }
490
491            stmt = con.prepareStatement(sql);
492            stmt.setString(1, login);
493
494            // Do the request
495            rs = stmt.executeQuery();
496
497            if (rs.next()) 
498            {
499                String storedPassword = rs.getString(__COLUMN_PASSWORD);
500                String salt = rs.getString(__COLUMN_SALT);
501                
502                if (salt == null && _isMD5Encrypted(storedPassword))
503                {
504                    String encryptedPassword = org.ametys.core.util.StringUtils.md5Base64(password);
505                    
506                    if (encryptedPassword == null)
507                    {
508                        getLogger().error("Unable to encrypt password");
509                        return false;
510                    }
511                    
512                    if (storedPassword.equals(encryptedPassword))
513                    {
514                        updateNeeded = true;
515                        return true;
516                    }
517                    
518                    return false;
519                }
520                else
521                {
522                    String encryptedPassword = DigestUtils.sha512Hex(salt + password);
523                    
524                    if (encryptedPassword == null)
525                    {
526                        getLogger().error("Unable to encrypt password");
527                        return false;
528                    }
529                    
530                    return storedPassword.equalsIgnoreCase(encryptedPassword);
531                }
532            }
533            
534            return false;
535        }
536        catch (SQLException e)
537        {
538            getLogger().error("Error during the connection to the database", e);
539            return false;
540        }
541        finally
542        {
543            // Close connections
544            ConnectionHelper.cleanup(rs);
545            ConnectionHelper.cleanup(stmt);
546            ConnectionHelper.cleanup(con);
547            
548            if (updateNeeded)
549            {
550                _updateToSSHAPassword(login, password);
551            }
552        }
553    }
554
555    @Override
556    public void add(Map<String, String> userInformation) throws InvalidModificationException
557    {
558        Connection con = null;
559        PreparedStatement stmt = null;
560
561        if (getLogger().isDebugEnabled())
562        {
563            getLogger().debug("Starting adding a new user");
564        }
565        
566        // Check the presence of all parameters
567        Map<String, Errors> errorFields = validate(userInformation);
568        
569        if (errorFields.size() > 0)
570        {
571            throw new InvalidModificationException("The creation of user failed because of invalid parameter values", errorFields);
572        }
573        
574        String login = userInformation.get("login");
575
576        try
577        {
578            // Connect to the database with connection pool
579            con = getSQLConnection();
580
581            stmt = createAddStatement(con, userInformation);
582
583            // Do the request and check the result
584            if (stmt.executeUpdate() != 1)
585            {
586                if (getLogger().isWarnEnabled())
587                {
588                    getLogger().warn("The user to remove '" + login + "' was not removed.");
589                }
590                throw new InvalidModificationException("Error no user inserted");
591            }
592
593            if (getObservationManager() != null)
594            {
595                // Observation manager can be null in safe mode
596                Map<String, Object> eventParams = new HashMap<>();
597                eventParams.put(ObservationConstants.ARGS_USER, new UserIdentity(login, _populationId));
598                getObservationManager().notify(new Event(ObservationConstants.EVENT_USER_ADDED, getCurrentUserProvider().getUser(), eventParams));
599            }
600        }
601        catch (SQLException e)
602        {
603            getLogger().error("Error communication with database", e);
604            throw new InvalidModificationException("Error during the communication with the database", e);
605        }
606        finally
607        {
608            // Close connections
609            ConnectionHelper.cleanup(stmt);
610            ConnectionHelper.cleanup(con);
611        }
612        
613    }
614    
615    @Override
616    public Map<String, Errors> validate(Map<String, String> userInformation)
617    {
618        Map<String, Errors> errorFields = new HashMap<>();
619        for (ElementDefinition parameter : _model.values())
620        {
621            Object typedValue = parameter.getType().castValue(userInformation.get(parameter.getName()));
622            Errors errors = new Errors();
623            
624            List<I18nizableText> errorsList = ModelHelper.validateValue(parameter, typedValue);
625            for (I18nizableText error : errorsList)
626            {
627                errors.addError(error);
628            }
629            
630            if (errors.hasErrors())
631            {
632                if (getLogger().isDebugEnabled())
633                {
634                    getLogger().debug("The field '" + parameter.getName() + "' is not valid");
635                }
636                errorFields.put(parameter.getName(), errors);
637            }
638        }
639        return errorFields;
640    }
641
642    @Override
643    public void update(Map<String, String> userInformation) throws InvalidModificationException
644    {
645        Connection con = null;
646        PreparedStatement stmt = null;
647        
648        Map<String, Errors> errorFields = new HashMap<>();
649        for (String id : userInformation.keySet())
650        {
651            ElementDefinition parameter = _model.get(id);
652            if (parameter != null)
653            {
654                Object typedValue = parameter.getType().castValue(userInformation.get(parameter.getName()));
655                List<I18nizableText> errorsList = ModelHelper.validateValue(parameter, typedValue);
656                Errors errors = new Errors();
657                for (I18nizableText error : errorsList)
658                {
659                    errors.addError(error);
660                }
661                
662                if (errors.hasErrors())
663                {
664                    if (getLogger().isDebugEnabled())
665                    {
666                        getLogger().debug("The field '" + parameter.getName() + "' is not valid");
667                    }
668                    errorFields.put(parameter.getName(), errors);
669                }
670            }
671        }
672        
673        if (errorFields.size() > 0)
674        {
675            throw new InvalidModificationException("The modification of user failed because of invalid parameter values", errorFields);
676        }
677
678        String login = userInformation.get("login");
679        if (StringUtils.isEmpty(login))
680        {
681            throw new InvalidModificationException("Cannot update without login information");
682        }
683
684        try
685        {
686            // Connect to the database with connection pool
687            con = getSQLConnection();
688
689            stmt = createModifyStatement(con, userInformation);
690
691            // Do the request
692            if (stmt.executeUpdate() != 1)
693            {
694                throw new InvalidModificationException("Error. User '" + login + "' not updated");
695            }
696
697            if (getObservationManager() != null)
698            {
699                // Observation manager can be null in safe mode
700                Map<String, Object> eventParams = new HashMap<>();
701                eventParams.put(ObservationConstants.ARGS_USER, new UserIdentity(login, _populationId));
702                getObservationManager().notify(new Event(ObservationConstants.EVENT_USER_UPDATED, getCurrentUserProvider().getUser(), eventParams));
703            }
704            
705            if (isCachingEnabled())
706            {
707                _getCacheByLogin().invalidate(login);
708            }
709        }
710        catch (SQLException e)
711        {
712            getLogger().error("Error communication with database", e);
713            throw new InvalidModificationException("Error communication with database", e);
714        }
715        finally
716        {
717            // Close connections
718            ConnectionHelper.cleanup(stmt);
719            ConnectionHelper.cleanup(con);
720        }
721    }
722
723    @Override
724    public void remove(String login) throws InvalidModificationException
725    {
726        Connection con = null;
727        PreparedStatement stmt = null;
728
729        try
730        {
731            // Connect to the database with connection pool
732            con = getSQLConnection();
733
734            // Build request for removing the user
735            String sqlRequest = "DELETE FROM " + _userTableName + " WHERE " + __COLUMN_LOGIN + " = ?";
736            if (getLogger().isDebugEnabled())
737            {
738                getLogger().debug(sqlRequest);
739            }
740
741            stmt = con.prepareStatement(sqlRequest);
742            stmt.setString(1, login);
743
744            // Do the request and check the result
745            if (stmt.executeUpdate() != 1)
746            {
747                throw new InvalidModificationException("Error user was not deleted");
748            }
749
750            if (getObservationManager() != null)
751            {
752                // Observation manager can be null in safe mode
753                Map<String, Object> eventParams = new HashMap<>();
754                eventParams.put(ObservationConstants.ARGS_USER, new UserIdentity(login, _populationId));
755                getObservationManager().notify(new Event(ObservationConstants.EVENT_USER_DELETED, getCurrentUserProvider().getUser(), eventParams));
756            }
757            
758            if (isCachingEnabled())
759            {
760                _getCacheByLogin().invalidate(login);
761            }
762        }
763        catch (SQLException e)
764        {
765            throw new InvalidModificationException("Error during the communication with the database", e);
766        }
767        finally
768        {
769            // Close connections
770            ConnectionHelper.cleanup(stmt);
771            ConnectionHelper.cleanup(con);
772        }
773    }
774    
775    public Collection< ? extends ModelItem> getModelItems()
776    {
777        return Collections.unmodifiableCollection(_model.values());
778    }
779    
780    /**
781     * Get the mandatory predicate to use when querying users by pattern.
782     * @param pattern The pattern to match, can be null.
783     * @return a {@link JdbcPredicate}, can be null.
784     */
785    protected JdbcPredicate _getMandatoryPredicate(String pattern)
786    {
787        return null;
788    }
789    
790    /**
791     * Get the pattern to match user login
792     * @param pattern the pattern
793     * @return the pattern to match user login
794     */
795    protected String _getPatternToMatch(String pattern)
796    {
797        if (pattern != null)
798        {
799            return "%" + pattern + "%";
800        }
801        return null;
802    }
803    
804    /**
805     * Determines if the password is encrypted with MD5 algorithm
806     * @param password The encrypted password
807     * @return true if the password is encrypted with MD5 algorithm
808     */
809    protected boolean _isMD5Encrypted(String password)
810    {
811        return password.length() == 24;
812    }
813    
814    /**
815     * Generate a salt key and encrypt the password with the sha2 algorithm
816     * @param login The user login
817     * @param password The user pasword
818     */
819    protected void _updateToSSHAPassword(String login, String password)
820    {
821        Connection con = null;
822        PreparedStatement stmt = null;
823        ResultSet rs = null;
824
825        try
826        {
827            con = getSQLConnection();
828
829            String generateSaltKey = RandomStringUtils.randomAlphanumeric(48);
830            String newEncryptedPassword = DigestUtils.sha512Hex(generateSaltKey + password);
831
832            String sqlUpdate = "UPDATE " + _userTableName + " SET " + __COLUMN_PASSWORD + " = ?, " + __COLUMN_SALT + " = ? WHERE " + __COLUMN_LOGIN + " = ?";
833            if (getLogger().isDebugEnabled())
834            {
835                getLogger().debug(sqlUpdate);
836            }
837
838            stmt = con.prepareStatement(sqlUpdate);
839            stmt.setString(1, newEncryptedPassword);
840            stmt.setString(2, generateSaltKey);
841            stmt.setString(3, login);
842
843            stmt.execute();
844        }
845        catch (SQLException e)
846        {
847            getLogger().error("Error during the connection to the database", e);
848        }
849        finally
850        {
851            // Close connections
852            ConnectionHelper.cleanup(rs);
853            ConnectionHelper.cleanup(stmt);
854            ConnectionHelper.cleanup(con);
855        }
856    }
857     
858     /**
859      * Create Add statement
860      * @param con The sql connection
861      * @param userInformation the user informations
862      * @return The statement
863      * @throws SQLException if an error occurred
864      */
865    protected PreparedStatement createAddStatement(Connection con, Map<String, String> userInformation) throws SQLException
866    {
867        String beginClause = "INSERT INTO " + _userTableName + " (";
868        String middleClause = ") VALUES (";
869        String endClause = ")";
870
871        StringBuffer intoClause = new StringBuffer();
872        StringBuffer valueClause = new StringBuffer();
873
874        intoClause.append(__COLUMN_SALT);
875        valueClause.append("?");
876
877        for (String column : __COLUMNS)
878        {
879            intoClause.append(", " + column);
880            valueClause.append(", ?");
881        }
882
883        String sqlRequest = beginClause + intoClause.toString() + middleClause + valueClause + endClause;
884        if (getLogger().isDebugEnabled())
885        {
886            getLogger().debug(sqlRequest);
887        }
888
889        PreparedStatement stmt = con.prepareStatement(sqlRequest);
890
891        int i = 1;
892        boolean clearText = !userInformation.containsKey("clearText") || !"false".equals(userInformation.get("clearText")) || !userInformation.containsKey(__COLUMN_SALT);
893        String generatedSaltKey = clearText ? RandomStringUtils.randomAlphanumeric(48) : userInformation.get(__COLUMN_SALT);
894
895        stmt.setString(i++, generatedSaltKey);
896
897        for (String column : __COLUMNS)
898        {
899            if ("password".equals(column))
900            {
901                String encryptedPassword;
902                if (clearText)
903                {
904                    encryptedPassword = DigestUtils.sha512Hex(generatedSaltKey + userInformation.get(column));
905                    if (encryptedPassword == null)
906                    {
907                        String message = "Cannot encode password";
908                        getLogger().error(message);
909                        throw new SQLException(message);
910                    }
911                }
912                else
913                {
914                    encryptedPassword = userInformation.get(column);
915                }
916                stmt.setString(i++, encryptedPassword);
917            }
918            else
919            {
920                stmt.setString(i++, userInformation.get(column));
921            }
922        }
923
924        return stmt;
925    }
926     
927     /**
928      * Create statement to update database
929      * @param con The sql connection
930      * @param userInformation The user information
931      * @return The statement
932      * @throws SQLException if an error occurred
933      */
934    protected PreparedStatement createModifyStatement(Connection con, Map<String, String> userInformation) throws SQLException
935    {
936        // Build request for editing the user
937        String beginClause = "UPDATE " + _userTableName + " SET ";
938        String endClause = " WHERE " + __COLUMN_LOGIN + " = ?";
939
940        StringBuffer columnNames = new StringBuffer("");
941
942        boolean passwordUpdate = false;
943        for (String id : userInformation.keySet())
944        {
945            if (ArrayUtils.contains(__COLUMNS, id) && !"login".equals(id) && !("password".equals(id) && (userInformation.get(id) == null)))
946            {
947                if ("password".equals(id))
948                {
949                    passwordUpdate = true;
950                }
951
952                if (columnNames.length() > 0)
953                {
954                    columnNames.append(", ");
955                }
956                columnNames.append(id + " = ?");
957            }
958        }
959
960        if (passwordUpdate)
961        {
962            columnNames.append(", " + __COLUMN_SALT + " = ?");
963        }
964
965        String sqlRequest = beginClause + columnNames.toString() + endClause;
966        if (getLogger().isDebugEnabled())
967        {
968            getLogger().debug(sqlRequest);
969        }
970
971        PreparedStatement stmt = con.prepareStatement(sqlRequest);
972        _fillModifyStatement(stmt, userInformation);
973
974        return stmt;
975    }
976     
977     /**
978      * Fill the statement with the user informations
979      * @param stmt The statement of the sql request
980      * @param userInformation the user informations
981      * @throws SQLException if an error occurred
982      */
983    protected void _fillModifyStatement(PreparedStatement stmt, Map<String, String> userInformation) throws SQLException
984    {
985        int index = 1;
986
987        boolean clearText = !userInformation.containsKey("clearText") || !"false".equals(userInformation.get("clearText")) || !userInformation.containsKey(__COLUMN_SALT);
988        String generatedSaltKey = clearText ? RandomStringUtils.randomAlphanumeric(48) : userInformation.get(__COLUMN_SALT);
989
990        boolean passwordUpdate = false;
991
992        for (String id : userInformation.keySet())
993        {
994            if (ArrayUtils.contains(__COLUMNS, id) && !"login".equals(id))
995            {
996                if ("password".equals(id))
997                {
998                    if (userInformation.get(id) != null)
999                    {
1000                        String encryptedPassword;
1001                        if (clearText)
1002                        {
1003                            encryptedPassword = DigestUtils.sha512Hex(generatedSaltKey + userInformation.get(id));
1004                            if (encryptedPassword == null)
1005                            {
1006                                String message = "Cannot encrypt password";
1007                                getLogger().error(message);
1008                                throw new SQLException(message);
1009                            }
1010                        }
1011                        else
1012                        {
1013                            encryptedPassword = userInformation.get(id);
1014                        }
1015                        stmt.setString(index++, encryptedPassword);
1016                        passwordUpdate = true;
1017                    }
1018                }
1019                else
1020                {
1021                    stmt.setString(index++, userInformation.get(id));
1022                }
1023            }
1024        }
1025
1026        if (passwordUpdate)
1027        {
1028            stmt.setString(index++, generatedSaltKey);
1029        }
1030
1031        stmt.setString(index++, userInformation.get("login"));
1032    }
1033     
1034     /**
1035      * Populate the user list with the result set
1036      * @param rs The result set
1037      * @param cache the cache to use. Is null if caching is not enabled
1038      * @return The user list
1039      * @throws SQLException If an SQL exception occurs
1040      */
1041    protected List<User> _getUsersProcessResultSet(ResultSet rs, Cache<String, User> cache) throws SQLException
1042    {
1043        List<User> users = new ArrayList<>();
1044
1045        while (rs.next())
1046        {
1047            User user = null;
1048
1049            // Try to get in cache
1050            if (isCachingEnabled())
1051            {
1052                String login = rs.getString(__COLUMN_LOGIN);
1053                user = cache.hasKey(login) ? cache.get(login) : null;
1054            }
1055
1056            // Or create from result set
1057            if (user == null)
1058            {
1059                user = _createUserFromResultSet(rs);
1060
1061                if (isCachingEnabled())
1062                {
1063                    cache.put(user.getIdentity().getLogin(), user);
1064                }
1065            }
1066
1067            users.add(user);
1068        }
1069
1070        return users;
1071    }
1072     
1073     /**
1074      * Create the user implementation from the result set of the request
1075      * 
1076      * @param rs The result set where you can use get methods
1077      * @return The user reflecting the current cursor position in the result set
1078      * @throws SQLException if an error occurred
1079      */
1080    protected User _createUserFromResultSet(ResultSet rs) throws SQLException
1081    {
1082        String login = rs.getString(__COLUMN_LOGIN);
1083        String lastName = rs.getString(__COLUMN_LASTNAME);
1084        String firstName = rs.getString(__COLUMN_FIRSTNAME);
1085        String email = rs.getString(__COLUMN_EMAIL);
1086
1087        return new User(new UserIdentity(login, _populationId), lastName, firstName, email, this);
1088    }
1089     
1090     /**
1091      * Retrieve an user from a result set
1092      * @param rs The result set
1093      * @param login The user login
1094      * @param cache the cache to use. Is null if caching is not enabled
1095      * @return The retrieved user or null if not found
1096      * @throws SQLException If an SQL Exception occurs
1097      */
1098    protected User _getUserProcessResultSet(ResultSet rs, String login, Cache<String, User> cache) throws SQLException
1099    {
1100        if (rs.next())
1101        {
1102            // Retrieve information of the user
1103            User user = _createUserFromResultSet(rs);
1104
1105            if (isCachingEnabled())
1106            {
1107                cache.put(login, user);
1108            }
1109
1110            return user;
1111        }
1112        else
1113        {
1114            // no user with this login in the database
1115            return null;
1116        }
1117    }
1118    
1119    @Override
1120    public View getView()
1121    {
1122        if (_view == null)
1123        {
1124            _view = View.of(this, __COLUMNS);
1125        }
1126        return _view;
1127    }
1128     
1129     // ------------------------------
1130     //          INNER CLASSE
1131     // ------------------------------
1132     /**
1133      * An internal query executor.
1134      * @param <T> The type of the queried object
1135      */
1136    protected abstract class AbstractJdbcQueryExecutor<T>
1137    {
1138        /**
1139         * Main function, run the query process. Will not throw exception. Use
1140         * runWithException to catch non SQL exception thrown by
1141         * {@link #processResultSet(ResultSet)}
1142         * @return The queried object or null
1143         */
1144        @SuppressWarnings("synthetic-access")
1145        public T run()
1146        {
1147            try
1148            {
1149                return runWithException();
1150            }
1151            catch (Exception e)
1152            {
1153                getLogger().error("Exception during a query execution", e);
1154                throw new RuntimeException("Exception during a query execution", e);
1155            }
1156        }
1157
1158        /**
1159         * Main function, run the query process.
1160         * @return The queried object or null
1161         * @throws Exception All non SQLException will be thrown
1162         */
1163        @SuppressWarnings("synthetic-access")
1164        public T runWithException() throws Exception
1165        {
1166            Connection connection = null;
1167            PreparedStatement stmt = null;
1168            ResultSet rs = null;
1169
1170            try
1171            {
1172                connection = getSQLConnection();
1173                
1174                String sql = getSqlQuery(connection);
1175
1176                if (getLogger().isDebugEnabled())
1177                {
1178                    getLogger().debug("Executing SQL query: " + sql);
1179                }
1180
1181                stmt = prepareStatement(connection, sql);
1182                rs = executeQuery(stmt);
1183
1184                return processResultSet(rs);
1185            }
1186            catch (SQLException e)
1187            {
1188                getLogger().error("Error during the communication with the database", e);
1189                throw new RuntimeException("Error during the communication with the database", e);
1190            }
1191            finally
1192            {
1193                ConnectionHelper.cleanup(rs);
1194                ConnectionHelper.cleanup(stmt);
1195                ConnectionHelper.cleanup(connection);
1196            }
1197        }
1198
1199        /**
1200         * Must return the SQL query to execute
1201         * @param connection The pool connection
1202         * @return The SQL query
1203         */
1204        protected abstract String getSqlQuery(Connection connection);
1205
1206        /**
1207         * Prepare the statement to execute
1208         * @param connection The pool connection
1209         * @param sql The SQL query
1210         * @return The prepared statement, ready to be executed
1211         * @throws SQLException If an SQL Exception occurs
1212         */
1213        protected PreparedStatement prepareStatement(Connection connection, String sql) throws SQLException
1214        {
1215            return connection.prepareStatement(sql);
1216        }
1217
1218        /**
1219         * Execute the prepared statement and retrieves the result set.
1220         * @param stmt The prepared statement
1221         * @return The result set
1222         * @throws SQLException If an SQL Exception occurs 
1223         */
1224        protected ResultSet executeQuery(PreparedStatement stmt) throws SQLException
1225        {
1226            return stmt.executeQuery();
1227        }
1228
1229        /**
1230         * Process the result set
1231         * @param rs The result set
1232         * @return The queried object or null
1233         * @throws SQLException If an SQL exception occurs
1234         * @throws Exception Other exception will be thrown when using {@link #runWithException()}
1235         */
1236        protected T processResultSet(ResultSet rs) throws SQLException, Exception
1237        {
1238            return null;
1239        }
1240    }
1241
1242    /**
1243     * Query executor in order to select an user
1244     * @param <T> The type of the queried object
1245     */
1246    protected class SelectUserJdbcQueryExecutor<T> extends AbstractJdbcQueryExecutor<T>
1247    {
1248        /** The user login */
1249        protected String _value;
1250        /** The search column */
1251        protected String _searchColumn;
1252        
1253        /** 
1254         * The constructor
1255         * @param value The strict value to search for
1256         */
1257        protected SelectUserJdbcQueryExecutor(String value)
1258        {
1259            _value = value;
1260            _searchColumn = __COLUMN_LOGIN;
1261        }
1262        
1263        /** 
1264         * The constructor
1265         * @param value The strict value to search for
1266         * @param searchColumn The name of search column
1267         */
1268        protected SelectUserJdbcQueryExecutor(String value, String searchColumn)
1269        {
1270            _value = value;
1271            _searchColumn = searchColumn;
1272        }
1273
1274        @Override
1275        protected String getSqlQuery(Connection connection)
1276        {
1277            // Build SQL request
1278            StringBuilder selectClause = new StringBuilder();
1279            for (String id : __COLUMNS)
1280            {
1281                if (selectClause.length() > 0)
1282                {
1283                    selectClause.append(", ");
1284                }
1285                selectClause.append(id);
1286            }
1287
1288            StringBuilder sql = new StringBuilder("SELECT ");
1289            sql.append(selectClause).append(" FROM ").append(_userTableName);
1290            sql.append(" WHERE ").append(_searchColumn).append(" = ?");
1291
1292            return sql.toString();
1293        }
1294
1295        @Override
1296        protected PreparedStatement prepareStatement(Connection connection, String sql) throws SQLException
1297        {
1298            PreparedStatement stmt = super.prepareStatement(connection, sql);
1299
1300            stmt.setString(1, _value);
1301            return stmt;
1302        }
1303    }
1304     
1305    /**
1306     * Query executor in order to select users
1307     * @param <T> The type of the queried object
1308     */
1309    protected class SelectUsersJdbcQueryExecutor<T> extends AbstractJdbcQueryExecutor<T>
1310    {
1311        /** The pattern to match (none if null) */
1312        protected String _pattern;
1313        /** The maximum number of users to select */
1314        protected int _length;
1315        /** The offset to start with, first is 0 */
1316        protected int _offset;
1317
1318        /** The mandatory predicate to use when querying users by pattern */
1319        protected JdbcPredicate _mandatoryPredicate;
1320        /** The pattern to match, extracted from the pattern */
1321        protected String _patternToMatch;
1322
1323        /** 
1324         * The constructor
1325         * @param pattern The pattern to match (none if null).
1326         * @param length The maximum number of users to select.
1327         * @param offset The offset to start with, first is 0.
1328         */
1329        protected SelectUsersJdbcQueryExecutor(String pattern, int length, int offset)
1330        {
1331            _pattern = pattern;
1332            _length = length;
1333            _offset = offset;
1334        }
1335
1336        @Override
1337        protected String getSqlQuery(Connection connection)
1338        {
1339            // Build SQL request
1340            StringBuilder selectClause = new StringBuilder();
1341            for (String column : __COLUMNS)
1342            {
1343                if (selectClause.length() > 0)
1344                {
1345                    selectClause.append(", ");
1346                }
1347                selectClause.append(column);
1348            }
1349
1350            StringBuilder sql = new StringBuilder("SELECT ");
1351            sql.append(selectClause).append(" FROM ").append(_userTableName);
1352
1353            // Add the pattern
1354            _mandatoryPredicate = _getMandatoryPredicate(_pattern);
1355            if (_mandatoryPredicate != null)
1356            {
1357                sql.append(" WHERE ").append(_mandatoryPredicate.getPredicate());
1358            }
1359
1360            _patternToMatch = _getPatternToMatch(_pattern);
1361            if (_patternToMatch != null)
1362            {
1363                if (ConnectionHelper.DATABASE_DERBY.equals(ConnectionHelper.getDatabaseType(connection)))
1364                {
1365                    // The LIKE operator in Derby is case sensitive
1366                    sql.append(_mandatoryPredicate != null ? " AND (" : " WHERE ")
1367                    .append("UPPER(").append(__COLUMN_LOGIN).append(") LIKE UPPER(?) OR ")
1368                    .append("UPPER(").append(__COLUMN_LASTNAME).append(") LIKE UPPER(?) OR ")
1369                    .append("UPPER(").append(__COLUMN_FIRSTNAME).append(") LIKE UPPER(?)");
1370                }
1371                else
1372                {
1373                    sql.append(_mandatoryPredicate != null ? " AND (" : " WHERE ")
1374                    .append(__COLUMN_LOGIN).append(" LIKE ? OR ")
1375                    .append(__COLUMN_LASTNAME).append(" LIKE ? OR ")
1376                    .append(__COLUMN_FIRSTNAME).append(" LIKE ?");
1377                }
1378
1379                if (_mandatoryPredicate != null)
1380                {
1381                    sql.append(')');
1382                }
1383            }
1384            
1385            StringBuilder orderByClause = new StringBuilder();
1386            for (String column : __ORDERBY_COLUMNS)
1387            {
1388                orderByClause.append(orderByClause.length() == 0 ? " ORDER BY " : ", ");
1389                orderByClause.append(column);
1390            }
1391            
1392            sql.append(orderByClause);
1393
1394            // Add length filters
1395            sql = _addQuerySize(_length, _offset, connection, selectClause, sql);
1396
1397            return sql.toString();
1398        }
1399
1400        @SuppressWarnings("synthetic-access")
1401        private StringBuilder _addQuerySize(int length, int offset, Connection con, StringBuilder selectClause, StringBuilder sql)
1402        {
1403            // Do not add anything if not necessary
1404            if (length == Integer.MAX_VALUE && offset == 0)
1405            {
1406                return sql;
1407            }
1408
1409            String dbType = ConnectionHelper.getDatabaseType(con);
1410
1411            if (ConnectionHelper.DATABASE_MYSQL.equals(dbType) || ConnectionHelper.DATABASE_POSTGRES.equals(dbType) || ConnectionHelper.DATABASE_HSQLDB.equals(dbType))
1412            {
1413                sql.append(" LIMIT " + length + " OFFSET " + offset);
1414                return sql;
1415            }
1416            else if (ConnectionHelper.DATABASE_ORACLE.equals(dbType))
1417            {
1418                return new StringBuilder("select " + selectClause.toString() + " from (select rownum r, " + selectClause.toString() + " from (" + sql.toString()
1419                        + ")) where r BETWEEN " + (offset + 1) + " AND " + (offset + length));
1420            }
1421            else if (ConnectionHelper.DATABASE_DERBY.equals(dbType))
1422            {
1423                return new StringBuilder("select ").append(selectClause)
1424                        .append(" from (select ROW_NUMBER() OVER () AS ROWNUM, ").append(selectClause.toString())
1425                        .append(" from (").append(sql.toString()).append(") AS TR ) AS TRR where ROWNUM BETWEEN ")
1426                        .append(offset + 1).append(" AND ").append(offset + length);
1427            }
1428            else if (getLogger().isWarnEnabled())
1429            {
1430                getLogger().warn("The request will not have the limit and offset set, since its type is unknown");
1431            }
1432
1433            return sql;
1434        }
1435
1436        @Override
1437        protected PreparedStatement prepareStatement(Connection connection, String sql) throws SQLException
1438        {
1439            PreparedStatement stmt = super.prepareStatement(connection, sql);
1440
1441            int i = 1;
1442            // Value the parameters if there is a pattern
1443            if (_mandatoryPredicate != null)
1444            {
1445                for (String value : _mandatoryPredicate.getValues())
1446                {
1447                    stmt.setString(i++, value);
1448                }
1449            }
1450
1451            if (_patternToMatch != null)
1452            {
1453                // One for the login, one for the lastname.
1454                stmt.setString(i++, _patternToMatch);
1455                stmt.setString(i++, _patternToMatch);
1456                // FIXME
1457                //if (_parameters.containsKey("firstname"))
1458                //{
1459                stmt.setString(i++, _patternToMatch);
1460                //}
1461            }
1462
1463            return stmt;
1464        }
1465    }
1466
1467    /**
1468     * Class representing a SQL predicate (to use in a WHERE or HAVING clause),
1469     * with optional string parameters.
1470     */
1471    public class JdbcPredicate
1472    {
1473
1474        /** The predicate string with optional "?" placeholders. */
1475        protected String _predicate;
1476        /** The predicate parameter values. */
1477        protected List<String> _predicateParamValues;
1478
1479        /**
1480         * Build a JDBC predicate.
1481         * @param predicate the predicate string.
1482         * @param values the parameter values.
1483         */
1484        public JdbcPredicate(String predicate, String... values)
1485        {
1486            this(predicate, Arrays.asList(values));
1487        }
1488
1489        /**
1490         * Build a JDBC predicate.
1491         * @param predicate the predicate string.
1492         * @param values the parameter values.
1493         */
1494        public JdbcPredicate(String predicate, List<String> values)
1495        {
1496            this._predicate = predicate;
1497            this._predicateParamValues = values;
1498        }
1499
1500        /**
1501         * Get the predicate.
1502         * @return the predicate
1503         */
1504        public String getPredicate()
1505        {
1506            return _predicate;
1507        }
1508
1509        /**
1510         * Set the predicate.
1511         * @param predicate the predicate to set
1512         */
1513        public void setPredicate(String predicate)
1514        {
1515            this._predicate = predicate;
1516        }
1517
1518        /**
1519         * Get the parameter values.
1520         * @return the parameter values.
1521         */
1522        public List<String> getValues()
1523        {
1524            return _predicateParamValues;
1525        }
1526
1527        /**
1528         * Set the parameter values.
1529         * @param values the parameter values to set.
1530         */
1531        public void setValues(List<String> values)
1532        {
1533            this._predicateParamValues = values;
1534        }
1535    }
1536
1537}