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