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