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